diff --git a/.gitignore b/.gitignore index bcc52fede8..9bb5162a9b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,8 @@ *~ target/ + +# IntelliJ IDEA +.idea + +# MacOS +.DS_Store \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/.gitignore b/doc/guacamole-frontend-extension-example/.gitignore new file mode 100644 index 0000000000..0711527ef9 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/.gitignore @@ -0,0 +1,42 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/doc/guacamole-frontend-extension-example/README.md b/doc/guacamole-frontend-extension-example/README.md new file mode 100644 index 0000000000..70f68257af --- /dev/null +++ b/doc/guacamole-frontend-extension-example/README.md @@ -0,0 +1,47 @@ +# Frontend Extension Example + +Adds global css styles, new routes (/guacamole/guacamole-angular-example and /guacamole/guacamole-angular-example/about) +and button below the "Logout" user menu. + +[//]: # (TODO: Update for native federation) +## Setup + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) +version 16.1.1 using the following steps: + +```bash +# Create a new minimal Angular project +ng new guacamole-frontend-extension-example \ +--minimal \ +--prefix guac \ +--routing \ +--skip-tests \ +--skip-install \ +--standalone \ +--view-encapsulation None + +# ... manually set the version of @angular dependencies to "16.1.1" in the package.json + +cd guacamole-frontend-extension-example + +npm install + +# Add support for module federation +ng add @angular-architects/module-federation --project guacamole-frontend-extension-example --type remote --port 4202 + +# Add the frontend extension library +npm install ..\..\guacamole\src\main\guacamole-frontend\dist\guacamole-frontend-ext-lib + +``` + +Add `"src/app/extension.config.ts"` to `tsconfig.app.json` `files` array. + + +## Development server + +Run `ng serve` for a dev server. Navigate to `http://localhost:4202/`. The application will automatically reload if you change any of the source files. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. +To package the artifacts for guacamole run `node ./package-extension.js`. diff --git a/doc/guacamole-frontend-extension-example/angular.json b/doc/guacamole-frontend-extension-example/angular.json new file mode 100644 index 0000000000..34668225af --- /dev/null +++ b/doc/guacamole-frontend-extension-example/angular.json @@ -0,0 +1,142 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "guacamole-frontend-extension-example": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true, + "standalone": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true, + "standalone": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true, + "standalone": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "guac", + "architect": { + "build": { + "builder": "@angular-architects/native-federation:build", + "options": {}, + "configurations": { + "production": { + "target": "guacamole-frontend-extension-example:esbuild:production" + }, + "development": { + "target": "guacamole-frontend-extension-example:esbuild:development", + "dev": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-architects/native-federation:build", + "options": { + "target": "guacamole-frontend-extension-example:serve-original:development", + "rebuildDelay": 0, + "dev": true, + "port": 0 + } + }, + "extract-i18n": { + "builder": "ngx-build-plus:extract-i18n", + "options": { + "browserTarget": "guacamole-frontend-extension-example:build", + "extraWebpackConfig": "webpack.config.js" + } + }, + "esbuild": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": { + "base": "dist/guacamole-frontend-extension-example", + "browser": "" + }, + "index": "src/index.html", + "polyfills": [ + "zone.js", + "es-module-shims" + ], + "tsConfig": "tsconfig.app.json", + "assets": [ + { + "glob": "*.html", + "input": "src/html", + "output": "html" + } + ], + "styles": [ + "src/styles.css" + ], + "scripts": [], + "browser": "src/main.ts" + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "outputHashing": "all" + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve-original": { + "builder": "@angular/build:dev-server", + "configurations": { + "production": { + "buildTarget": "guacamole-frontend-extension-example:esbuild:production" + }, + "development": { + "buildTarget": "guacamole-frontend-extension-example:esbuild:development" + } + }, + "defaultConfiguration": "development", + "options": { + "port": 4202 + } + } + } + } + } +} \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/federation.config.js b/doc/guacamole-frontend-extension-example/federation.config.js new file mode 100644 index 0000000000..30d5a65bce --- /dev/null +++ b/doc/guacamole-frontend-extension-example/federation.config.js @@ -0,0 +1,27 @@ +const { withNativeFederation, shareAll } = require('@angular-architects/native-federation/config'); + +module.exports = withNativeFederation({ + + name: 'guacamole-frontend-extension-example', + + exposes: { + 'bootsrapExtension': './src/app/extension.config.ts', + 'AboutExtensionButtonComponent': './src/app/components/about-extension-button.component.ts' + }, + + shared: { + ...shareAll({ singleton: true, strictVersion: true, requiredVersion: 'auto' }), + }, + + // Packages that should not be shared or are not needed at runtime + skip: [ + 'rxjs/ajax', + 'rxjs/fetch', + 'rxjs/testing', + 'rxjs/webSocket' + ] + + // Please read our FAQ about sharing libs: + // https://shorturl.at/jmzH0 + +}); diff --git a/doc/guacamole-frontend-extension-example/package-extension.js b/doc/guacamole-frontend-extension-example/package-extension.js new file mode 100755 index 0000000000..18d374b61a --- /dev/null +++ b/doc/guacamole-frontend-extension-example/package-extension.js @@ -0,0 +1,182 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +const fs = require('fs'); +const path = require('path'); +const {exec} = require('child_process'); +const {promisify} = require('util'); + +// Configuration +const CONFIG = { + sourceDir: 'dist/guacamole-frontend-extension-example', + outputFile: 'guacamole-angular-extension.jar', + manifestFile: 'guac-manifest.json', + nativeFederationConfiguration: { + bootstrapFunctionName: 'bootsrapExtension', + pageTitle: 'Angular Extension', + routePath: 'angular-extension' + }, + manifestTemplate: { + guacamoleVersion: '*', + name: 'Guacamole Angular Example', + namespace: 'guacamole-angular-example', + css: [], + html: [], + resources: {}, + nativeFederationConfiguration: 'nativeFederationConfiguration.json' + } +}; + +// MIME type mapping +const MIME_TYPES = { + // JavaScript files + '.js': 'application/javascript', + + // JSON files + '.json': 'application/json', + + // Default fallback + 'default': 'application/octet-stream' +}; + +// Promisify fs functions for cleaner async/await usage +const readdir = promisify(fs.readdir); +const stat = promisify(fs.stat); +const writeFile = promisify(fs.writeFile); +const execAsync = promisify(exec); + +/** + * Recursively gets all files in a directory + * @param {string} dir - Directory to scan + * @returns {Promise} Array of file paths relative to sourceDir + */ +async function getAllFiles(dir) { + const files = await readdir(dir); + const allFiles = []; + + for (const file of files) { + const filePath = path.join(dir, file); + const stats = await stat(filePath); + + if (stats.isDirectory()) { + const subFiles = await getAllFiles(filePath); + allFiles.push(...subFiles); + } else { + allFiles.push(filePath); + } + } + + return allFiles; +} + +/** + * Gets the MIME type for a file based on its extension + * @param {string} filepath - Path to the file + * @returns {string} MIME type + */ +function getMimeType(filepath) { + const ext = path.extname(filepath).toLowerCase(); + return MIME_TYPES[ext] || MIME_TYPES.default; +} + +/** + * Generates the manifest file based on build artifacts + * @param {string[]} files - Array of file paths + * @returns {Object} Manifest object + */ +function generateManifest(files) { + const manifest = {...CONFIG.manifestTemplate}; + + // Convert absolute paths to relative paths + const relativePaths = files.map(file => + path.relative(CONFIG.sourceDir, file) + ); + + // Sort files into css and resources + relativePaths.forEach(file => { + if (file.startsWith('html/')) { + manifest.html.push(file); + } else if (file.endsWith('.css')) { + manifest.css.push(file); + } else if (file.endsWith('.js') || file.endsWith('.json') || + /\.(otf|ttf|woff|woff2|eot|png|jpg|jpeg|gif|svg|ico)$/i.test(file)) { + manifest.resources[file] = getMimeType(file); + } + }); + + return manifest; +} + +/** + * Creates a JAR (ZIP) file containing all build artifacts and manifest + * @param {string[]} files - Array of file paths + * @param {Object} manifest - Manifest object + */ +async function createJarFile(files, manifest) { + // Write manifest to the source directory + const manifestPath = path.join(CONFIG.sourceDir, CONFIG.manifestFile); + await writeFile(manifestPath, JSON.stringify(manifest, null, 4)); + + // Write native federation configuration + const nativeFederationConfigurationPath = path.join(CONFIG.sourceDir, CONFIG.manifestTemplate.nativeFederationConfiguration); + await writeFile(nativeFederationConfigurationPath, JSON.stringify(CONFIG.nativeFederationConfiguration, null, 4)); + + // Create zip file using zip command (available on macOS and Linux) + const currentDir = process.cwd(); + process.chdir(CONFIG.sourceDir); + + try { + await execAsync(`zip -r "${path.join(currentDir, CONFIG.outputFile)}" ./*`); + console.log(`Successfully created ${CONFIG.outputFile}`); + } catch (error) { + console.error('Error creating zip file:', error); + throw error; + } finally { + process.chdir(currentDir); + // Clean up manifest file + // TODO fs.unlinkSync(manifestPath); + } +} + +/** + * Main function to orchestrate the packaging process + */ +async function main() { + try { + // Verify source directory exists + if (!fs.existsSync(CONFIG.sourceDir)) { + throw new Error(`Source directory ${CONFIG.sourceDir} does not exist`); + } + + // Get all files in the build directory + const files = await getAllFiles(CONFIG.sourceDir); + + // Generate manifest + const manifest = generateManifest(files); + + // Create JAR file + await createJarFile(files, manifest); + + } catch (error) { + console.error('Error during packaging:', error); + process.exit(1); + } +} + +main(); \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/package-lock.json b/doc/guacamole-frontend-extension-example/package-lock.json new file mode 100644 index 0000000000..2dc5278575 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/package-lock.json @@ -0,0 +1,26125 @@ +{ + "name": "guacamole-frontend-extension-example", + "version": "0.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "guacamole-frontend-extension-example", + "version": "0.0.0", + "dependencies": { + "@angular-architects/native-federation": "^19.0.5", + "@angular/animations": "19.0.6", + "@angular/common": "19.0.6", + "@angular/compiler": "19.0.6", + "@angular/core": "19.0.6", + "@angular/forms": "19.0.6", + "@angular/platform-browser": "19.0.6", + "@angular/platform-browser-dynamic": "19.0.6", + "@angular/router": "19.0.6", + "@softarc/native-federation-node": "^2.0.10", + "es-module-shims": "^1.5.12", + "guacamole-frontend-ext-lib": "file:../../guacamole/src/main/guacamole-frontend/dist/guacamole-frontend-ext-lib", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular/build": "^19.1.1", + "@angular/cli": "~19.0.7", + "@angular/compiler-cli": "19.0.6", + "ngx-build-plus": "^19.0.0", + "typescript": "~5.6.3" + } + }, + "../../guacamole/src/main/guacamole-frontend/dist/guacamole-frontend-ext-lib": { + "version": "0.0.1", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "16.1.1", + "@angular/core": "16.1.1" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-architects/native-federation": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@angular-architects/native-federation/-/native-federation-19.0.5.tgz", + "integrity": "sha512-si09fH1WqXa0oRKqwpemsSF6iTlqvm91lRzdXTSxDQ1JnpKmn6eYYL1zeoVv2kOTp+nD/8vZxOW2G/NIvFjY6A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.0", + "@chialab/esbuild-plugin-commonjs": "^0.18.0", + "@softarc/native-federation": "2.0.18", + "@softarc/native-federation-runtime": "2.0.18", + "@types/browser-sync": "^2.29.0", + "browser-sync": "^3.0.2", + "esbuild": "^0.19.5", + "mrmime": "^1.0.1", + "npmlog": "^6.0.2", + "process": "0.11.10" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", + "integrity": "sha512-3dRV0IB+MbNYbAGbYEFMcABkMphqcTvn5MG79dQkwcf2a9QZxCq2slwf/rIleWoDUcFm9r1NnVPYrTYNYJaqQg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.0.7.tgz", + "integrity": "sha512-R0vpJ+P5xBqF82zOMq2FvOP7pJz5NZ7PwHAIFuQ6z50SHLW/VcUA19ZoFKwxBX6A/Soyb66QXTcjZ5wbRqMm8w==", + "dev": true, + "peer": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/build-webpack": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular/build": "19.0.7", + "@babel/core": "7.26.0", + "@babel/generator": "7.26.2", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.9", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.25.9", + "@babel/preset-env": "7.26.0", + "@babel/runtime": "7.26.0", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.0.7", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.24.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.3", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "postcss": "8.4.49", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.80.7", + "sass-loader": "16.0.3", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.36.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.96.1", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.1.0", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.24.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.0.7", + "@web/test-runner": "^0.19.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.5 <5.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular/build": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.0.7.tgz", + "integrity": "sha512-AFvhRa6sfXG8NmS8AN7TvE8q2kVcMw+zXMZzo981cqwnOwJy4VHU0htqm5OZQnohVJM0pP8SBAuROWO4yRrxCA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.0.2", + "@vitejs/plugin-basic-ssl": "1.1.0", + "beasties": "0.1.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.12", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "rollup": "4.26.0", + "sass": "1.80.7", + "semver": "7.6.3", + "vite": "5.4.11", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.1.5" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0", + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.0.7", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.5 <5.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.1.5.tgz", + "integrity": "sha512-ue5PSOzHMCIYrfvPP/MRS6hsKKLzqqhcdAvJCO8uFlDdj598EhgnacuOTuqA6uBK5rgiZXfDWyb7DVZSiBKxBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.1.5.tgz", + "integrity": "sha512-CGhsb0R5vE6mMNCoSfxHFD8QTvBHM51gs4DBeigTYHWnYv2V5YpJkC4rMo5qAAFifuUcc0+a8a3SIU0c9NrfNw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.1.5.tgz", + "integrity": "sha512-3WeW328DN+xB5PZdhSWmqE+t3+44xWXEbqQ+caWJEZfOFdLp9yklBZEbVqVdqzznkoaXJYxTCp996KD6HmANeg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.1.5.tgz", + "integrity": "sha512-LAjaoOcBHGj6fiYB8ureiqPoph4eygbXu4vcOF+hsxiY74n8ilA7rJMmGUT0K0JOB5lmRQHSmor3mytRjS4qeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.1.5.tgz", + "integrity": "sha512-k/IklElP70qdCXOQixclSl2GPLFiopynGoKX1FqDd1/H0E3Fo1oPwjY2rEVu+0nS3AOw1sryStdXk8CW3cVIsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.1.5.tgz", + "integrity": "sha512-KYar6W8nraZfSJspcK7Kp7hdj238X/FNauYbZyrqPBrtsXI1hvI4/KcRcRGP50aQoV7fkKDyJERlrQGMGTZUsA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.26.0.tgz", + "integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-android-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.26.0.tgz", + "integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.26.0.tgz", + "integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-darwin-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.26.0.tgz", + "integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.26.0.tgz", + "integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.26.0.tgz", + "integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.26.0.tgz", + "integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.26.0.tgz", + "integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.26.0.tgz", + "integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.26.0.tgz", + "integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.26.0.tgz", + "integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.26.0.tgz", + "integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.26.0.tgz", + "integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.26.0.tgz", + "integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.26.0.tgz", + "integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.26.0.tgz", + "integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.26.0.tgz", + "integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.26.0.tgz", + "integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@angular-devkit/build-angular/node_modules/beasties": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.1.0.tgz", + "integrity": "sha512-+Ssscd2gVG24qRNC+E2g88D+xsQW4xwakWtKAiGEQ3Pw54/FGdyo9RrfxhGhEv6ilFVbB7r3Lgx+QnAxnSpECw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/lmdb": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.1.5.tgz", + "integrity": "sha512-46Mch5Drq+A93Ss3gtbg+Xuvf5BOgIuvhKDWoGa3HcPHI6BL2NCOkRdSx1D4VfzwrxhnsjbyIVsLRlQHu6URvw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.1.5", + "@lmdb/lmdb-darwin-x64": "3.1.5", + "@lmdb/lmdb-linux-arm": "3.1.5", + "@lmdb/lmdb-linux-arm64": "3.1.5", + "@lmdb/lmdb-linux-x64": "3.1.5", + "@lmdb/lmdb-win32-x64": "3.1.5" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/rollup": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.26.0.tgz", + "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.26.0", + "@rollup/rollup-android-arm64": "4.26.0", + "@rollup/rollup-darwin-arm64": "4.26.0", + "@rollup/rollup-darwin-x64": "4.26.0", + "@rollup/rollup-freebsd-arm64": "4.26.0", + "@rollup/rollup-freebsd-x64": "4.26.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.26.0", + "@rollup/rollup-linux-arm-musleabihf": "4.26.0", + "@rollup/rollup-linux-arm64-gnu": "4.26.0", + "@rollup/rollup-linux-arm64-musl": "4.26.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.26.0", + "@rollup/rollup-linux-riscv64-gnu": "4.26.0", + "@rollup/rollup-linux-s390x-gnu": "4.26.0", + "@rollup/rollup-linux-x64-gnu": "4.26.0", + "@rollup/rollup-linux-x64-musl": "4.26.0", + "@rollup/rollup-win32-arm64-msvc": "4.26.0", + "@rollup/rollup-win32-ia32-msvc": "4.26.0", + "@rollup/rollup-win32-x64-msvc": "4.26.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "peer": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1900.7.tgz", + "integrity": "sha512-F0S0iyspo/9w9rP5F9wmL+ZkBr48YQIWiFu+PaQ0in/lcdRmY/FjVHTMa5BMnlew9VCtFHPvpoN9x4u8AIoWXA==", + "dev": true, + "peer": true, + "dependencies": { + "@angular-devkit/architect": "0.1900.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.7.tgz", + "integrity": "sha512-VyuORSitT6LIaGUEF0KEnv2TwNaeWl6L3/4L4stok0BJ23B4joVca2DYVcrLC1hSzz8V4dwVgSlbNIgjgGdVpg==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/core/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.7.tgz", + "integrity": "sha512-BHXQv6kMc9xo4TH9lhwMv8nrZXHkLioQvLun2qYjwvOsyzt3qd+sUM9wpHwbG6t+01+FIQ05iNN9ox+Cvpndgg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/animations": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.0.6.tgz", + "integrity": "sha512-dlXrFcw7RQNze1zjmrbwqcFd6zgEuqKwuExtEN1Fy26kQ+wqKIhYO6IG7PZGef53XpwN5DT16yve6UihJ2XeNg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6" + } + }, + "node_modules/@angular/build": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.1.tgz", + "integrity": "sha512-E4jvi48zRCLkwcThQQm1Q1vq9aypf3+xSMQQMDcHzvt/89sucWTKkwgNHSW/Fi8qH23GhijCLXBHa4dHSoCB/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1901.1", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.1", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.2.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.2", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.17", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.30.1", + "sass": "1.83.1", + "semver": "7.6.3", + "vite": "6.0.7", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.2.2" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0", + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.1.1", + "less": "^4.2.0", + "ng-packagr": "^19.0.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.5 <5.8" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "less": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@angular-devkit/architect": { + "version": "0.1901.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.1.tgz", + "integrity": "sha512-fhRID3z4Va1nvP4QS7iraMP+J0zpvsqax0MtoaHXiaSvruwcPbcYSvWj9aO/oo9cq2XTd1zVigrKUwfhabXRVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/core": "19.1.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/build/node_modules/@angular-devkit/core": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.1.tgz", + "integrity": "sha512-CAqst7WEasPHR4OFdbxxX3+NVqNTvYk3vtPbXT/jZ0L2EZRICQta2EClkdhSIiMkiMf0/2LNT05rYD7k4NHIQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular/build/node_modules/@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@inquirer/confirm": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", + "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@inquirer/core": "^10.1.2", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.21.3" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" + } + }, + "node_modules/@angular/build/node_modules/esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "node_modules/@angular/build/node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/@angular/build/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@angular/build/node_modules/magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/@angular/build/node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@angular/build/node_modules/piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "dev": true, + "license": "MIT", + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/@angular/build/node_modules/sass": { + "version": "1.83.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", + "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/@angular/build/node_modules/vite": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.24.2", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "jiti": ">=1.21.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.0.7.tgz", + "integrity": "sha512-y6C4B4XdiZwe2+OADLWXyKqUVvW/XDzTuJ2mZ5PhTnSiiXDN4zRWId1F5wA8ve8vlbUKApPHXRQuaqiQJmA24g==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "@inquirer/prompts": "7.1.0", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.0.7", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.0", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.0.6.tgz", + "integrity": "sha512-r9IDD0+UGkrQkjyX+pApeDmIJ9INpr1uYlgmmlWNBJCVNr9SKKIVZV60sssgadew6bGynKN9dW4mGsmEzzb5BA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.0.6.tgz", + "integrity": "sha512-g8A6QOsiCJnRi5Hz0sASIpRQoAGxEgnjz0JanfrMNRedY4MpdIS1V0AeCSKTsMRlV7tQl3ng2Gse/tsb51HI3Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.0.6.tgz", + "integrity": "sha512-fHtwI5rCe3LmKDoaqlqLAPdNmLrbeCiMYVe+X1BHgApaqNCyAwcuJxuf8Q5R5su7nHiLmlmB74o1ZS/V+0cQ+g==", + "dev": true, + "dependencies": { + "@babel/core": "7.26.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.0.6", + "typescript": ">=5.5 <5.7" + } + }, + "node_modules/@angular/core": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.0.6.tgz", + "integrity": "sha512-9N7FmdRHtS7zfXC/wnyap/reX7fgiOrWpVivayHjWP4RkLYXJAzJIpLyew0jrx4vf8r3lZnC0Zmq0PW007Ngjw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.0.6.tgz", + "integrity": "sha512-HogauPvgDQHw2xxqKBaFgKTRRcc1xWeI/PByDCf3U6YsaqpF53Mz2CJh8X2bg2bY1RGKb67MZw7DBGFRvXx4bg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.6.tgz", + "integrity": "sha512-MWiToGy7Pa0rR61sgnEuu7dfZXpAw0g7nkSnw4xdjUf974OOOfI1LS9O9YevJibtdW8sPa1HaoXXwcb7N03B5A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.0.6", + "@angular/common": "19.0.6", + "@angular/core": "19.0.6" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.0.6.tgz", + "integrity": "sha512-5TGLOwPlLHXJ1+Hs9b3dEmGdTpb7dfLYalVmiMUZOFBry1sMaRuw+nyqjmWn1GP3yD156hzt5QDzWA8A134AfQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/compiler": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6" + } + }, + "node_modules/@angular/router": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.0.6.tgz", + "integrity": "sha512-G1oz+TclPk48h6b6B4s5J3DfrDVJrrxKOA+KWeVQP4e1B8ld7/dCMf5nn3yqS4BGs4yLecxMxyvbOvOiZ//lxw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "dependencies": { + "@babel/types": "^7.26.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", + "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dev": true, + "peer": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chialab/cjs-to-esm": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/cjs-to-esm/-/cjs-to-esm-0.18.0.tgz", + "integrity": "sha512-fm8X9NhPO5pyUB7gxOZgwxb8lVq1UD4syDJCpqh6x4zGME6RTck7BguWZ4Zgv3GML4fQ4KZtyRwP5eoDgNGrmA==", + "license": "MIT", + "dependencies": { + "@chialab/estransform": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-plugin-commonjs": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-commonjs/-/esbuild-plugin-commonjs-0.18.0.tgz", + "integrity": "sha512-qZjIsNr1dVEJk6NLyza3pJLHeY7Fz0xjmYteKXElCnlFSKR7vVg6d18AsxVpRnP5qNbvx3XlOvs9U8j97ZQ6bw==", + "license": "MIT", + "dependencies": { + "@chialab/cjs-to-esm": "^0.18.0", + "@chialab/esbuild-rna": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-rna": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", + "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", + "license": "MIT", + "dependencies": { + "@chialab/estransform": "^0.18.0", + "@chialab/node-resolve": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/estransform": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", + "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", + "license": "MIT", + "dependencies": { + "@parcel/source-map": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/node-resolve": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", + "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", + "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", + "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", + "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/core/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", + "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", + "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", + "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", + "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", + "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", + "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", + "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^4.0.2", + "@inquirer/confirm": "^5.0.2", + "@inquirer/editor": "^4.1.0", + "@inquirer/expand": "^4.0.2", + "@inquirer/input": "^4.0.2", + "@inquirer/number": "^3.0.2", + "@inquirer/password": "^4.0.2", + "@inquirer/rawlist": "^4.0.2", + "@inquirer/search": "^3.0.2", + "@inquirer/select": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", + "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", + "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", + "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", + "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "peer": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "peer": true + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz", + "integrity": "sha512-WBSJT9Z7DTol5viq+DZD2TapeWOw7mlwXxiSBHgAzqVwsaVb0h/ekMD9iu/jDD8MUA20tO9N0WEdnT06fsUp+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.2.tgz", + "integrity": "sha512-4S13kUtR7c/j/MzkTIBJCXv52hQ41LG2ukeaqw4Eng9K0pNKLFjo1sDSz96/yKhwykxrWDb13ddJ/ZqD3rAhUA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.2.tgz", + "integrity": "sha512-uW31JmfuPAaLUYW7NsEU8gzwgDAzpGPwjvkxnKlcWd8iDutoPKDJi8Wk9lFmPEZRxVSB0j1/wDQ7N2qliR9UFA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.2.tgz", + "integrity": "sha512-4hdgZtWI1idQlWRp+eleWXD9KLvObgboRaVoBj2POdPEYvsKANllvMW0El8tEQwtw74yB9NT6P8ENBB5UJf5+g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.2.tgz", + "integrity": "sha512-A0zjf4a2vM4B4GAx78ncuOTZ8Ka1DbTaG1Axf1e00Sa7f5coqlWiLg1PX7Gxvyibc2YqtqB+8tg1KKrE8guZVw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.2.tgz", + "integrity": "sha512-Y0qoSCAja+xZE7QQ0LCHoYAuyI1n9ZqukQJa8lv9X3yCvWahFF7OYHAgVH1ejp43XWstj3U89/PAAzcowgF/uQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngtools/webpack": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.0.7.tgz", + "integrity": "sha512-jWyMuqtLKZB8Jnuqo27mG2cCQdl71lhM1oEdq3x7Z/QOrm2I+8EfyAzOLxB1f1vXt85O1bz3nf66CkuVCVGGTQ==", + "dev": true, + "peer": true, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0", + "typescript": ">=5.5 <5.7", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", + "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.0.tgz", + "integrity": "sha512-t6G+6ZInT4X+tqj2i+wlLIeCKnKOTuz9/VFYDtj+TGTur5q7sp/OYrQA19LdBbWfXDOi0Y4jtedV6xtB8zQ9ug==", + "dev": true, + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "normalize-package-data": "^7.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "dev": true, + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.0.0.tgz", + "integrity": "sha512-/1uFzjVcfzqrgCeGW7+SZ4hv0qLWmKXVzFahZGJ6QuJBj6Myt9s17+JL86i76NV9YSnJRcGXJYQbAU0rn1YTCQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.0.2.tgz", + "integrity": "sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/source-map/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rspack/binding": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.1.8.tgz", + "integrity": "sha512-+/JzXx1HctfgPj+XtsCTbRkxiaOfAXGZZLEvs7jgp04WgWRSZ5u97WRCePNPvy+sCfOEH/2zw2ZK36Z7oQRGhQ==", + "dev": true, + "optional": true, + "peer": true, + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.1.8", + "@rspack/binding-darwin-x64": "1.1.8", + "@rspack/binding-linux-arm64-gnu": "1.1.8", + "@rspack/binding-linux-arm64-musl": "1.1.8", + "@rspack/binding-linux-x64-gnu": "1.1.8", + "@rspack/binding-linux-x64-musl": "1.1.8", + "@rspack/binding-win32-arm64-msvc": "1.1.8", + "@rspack/binding-win32-ia32-msvc": "1.1.8", + "@rspack/binding-win32-x64-msvc": "1.1.8" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.1.8.tgz", + "integrity": "sha512-I7avr471ghQ3LAqKm2fuXuJPLgQ9gffn5Q4nHi8rsukuZUtiLDPfYzK1QuupEp2JXRWM1gG5lIbSUOht3cD6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.1.8.tgz", + "integrity": "sha512-vfqf/c+mcx8rr1M8LnqKmzDdnrgguflZnjGerBLjNerAc+dcUp3lCvNxRIvZ2TkSZZBW8BpCMgjj3n70CZ4VLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.8.tgz", + "integrity": "sha512-lZlO/rAJSeozi+qtVLkGSXfe+riPawCwM4FsrflELfNlvvEXpANwtrdJ+LsaNVXcgvhh50ZX2KicTdmx9G2b6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.8.tgz", + "integrity": "sha512-bX7exULSZwy8xtDh6Z65b6sRC4uSxGuyvSLCEKyhmG6AnJkg0gQMxk3hoO0hWnyGEZgdJEn+jEhk0fjl+6ZRAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.8.tgz", + "integrity": "sha512-2Prw2USgTJ3aLdLExfik8pAwAHbX4MZrACBGEmR7Vbb56kLjC+++fXkciRc50pUDK4JFr1VQ7eNZrJuDR6GG6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.8.tgz", + "integrity": "sha512-bnVGB/mQBKEdzOU/CPmcOE3qEXxGOGGW7/i6iLl2MamVOykJq8fYjL9j86yi6L0r009ja16OgWckykQGc4UqGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.8.tgz", + "integrity": "sha512-u+na3gxhzeksm4xZyAzn1+XWo5a5j7hgWA/KcFPDQ8qQNkRknx4jnQMxVtcZ9pLskAYV4AcOV/AIximx7zvv8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.1.8.tgz", + "integrity": "sha512-FijUxym1INd5fFHwVCLuVP8XEAb4Sk1sMwEEQUlugiDra9ZsLaPw4OgPGxbxkD6SB0DeUz9Zq46Xbcf6d3OgfA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.8.tgz", + "integrity": "sha512-SBzIcND4qpDt71jlu1MCDxt335tqInT3YID9V4DoQ4t8wgM/uad7EgKOWKTK6vc2RRaOIShfS2XzqjNUxPXh4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/core": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.1.8.tgz", + "integrity": "sha512-pcZtcj5iXLCuw9oElTYC47bp/RQADm/MMEb3djHdwJuSlFWfWPQi5QFgJ/lJAxIW9UNHnTFrYtytycfjpuoEcA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime-tools": "0.5.1", + "@rspack/binding": "1.1.8", + "@rspack/lite-tapable": "1.0.1", + "caniuse-lite": "^1.0.30001616" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz", + "integrity": "sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/sdk": "0.5.1" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz", + "integrity": "sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.5.1", + "@module-federation/webpack-bundler-runtime": "0.5.1" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/sdk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.5.1.tgz", + "integrity": "sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz", + "integrity": "sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.5.1", + "@module-federation/sdk": "0.5.1" + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", + "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@schematics/angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.7.tgz", + "integrity": "sha512-1WtTqKFPuEaV99VIP+y/gf/XW3TVJh/NbJbbEF4qYpp7qQiJ4ntF4klVZmsJcQzFucZSzlg91QVMPQKev5WZGA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", + "integrity": "sha512-XDUYX56iMPAn/cdgh/DTJxz5RWmqKV4pwvUAEKEWJl+HzKdCd/24wUa9JYNMlDSCb7SUHAdtksxYX779Nne/Zg==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.0.0.tgz", + "integrity": "sha512-UjhDMQOkyDoktpXoc5YPJpJK6IooF2gayAr5LvXI4EL7O0vd58okgfRcxuaH+YTdhvb5aa1Q9f+WJ0c2sVuYIw==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.0.0.tgz", + "integrity": "sha512-9Xxy/8U5OFJu7s+OsHzI96IX/OzjF/zj0BSSaWhgJgTqtlBhQIV2xdrQI5qxLD7+CWWDepadnXAxzaZ3u9cvRw==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.0.0.tgz", + "integrity": "sha512-Ggtq2GsJuxFNUvQzLoXqRwS4ceRfLAJnrIHUDrzAD0GgnOhwujJkKkxM/s5Bako07c3WtAs/sZo5PJq7VHjeDg==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, + "node_modules/@softarc/native-federation": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation/-/native-federation-2.0.18.tgz", + "integrity": "sha512-2QhZZjm3YGTKi7VHlYC4GyBwx+fuc+SM2djl5CAionap/WuBzsucxOmo3ECbLS6tfoc0kGeYSVMX1EcwWa+F5A==", + "license": "MIT", + "dependencies": { + "@softarc/native-federation-runtime": "2.0.18", + "json5": "^2.2.0", + "npmlog": "^6.0.2" + } + }, + "node_modules/@softarc/native-federation-node": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-node/-/native-federation-node-2.0.18.tgz", + "integrity": "sha512-2bV/oB2BpGibs62/PmNxH6VIngsAWdZGLUl4oLjzrwgxmEw+rvIW9EhJ+S6zZz0bmz/YZCFxEowBETpP779fxQ==" + }, + "node_modules/@softarc/native-federation-runtime": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-runtime/-/native-federation-runtime-2.0.18.tgz", + "integrity": "sha512-kCfegbei6FVbGkCZfWe5IrNuRcNZGU517HeRwaoCtXvk2/2zudGRD9ftWmhDAV1N85N3tvXEsO65YFSI7Ccr5g==", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/browser-sync": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/@types/browser-sync/-/browser-sync-2.29.0.tgz", + "integrity": "sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==", + "license": "MIT", + "dependencies": { + "@types/micromatch": "^2", + "@types/node": "*", + "@types/serve-static": "*", + "chokidar": "^3.0.0" + } + }, + "node_modules/@types/browser-sync/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@types/browser-sync/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "peer": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "peer": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "peer": true + }, + "node_modules/@types/micromatch": { + "version": "2.3.35", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.35.tgz", + "integrity": "sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==", + "license": "MIT", + "dependencies": { + "@types/parse-glob": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-glob": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@types/parse-glob/-/parse-glob-3.0.32.tgz", + "integrity": "sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "peer": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "peer": true + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "peer": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "peer": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "peer": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "peer": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "peer": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/accepts/node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "peer": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "peer": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "peer": true, + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "peer": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "peer": true + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "peer": true, + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "peer": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "node_modules/beasties": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", + "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.1.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true, + "license": "ISC" + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", + "license": "Apache-2.0", + "dependencies": { + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.8", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "license": "ISC", + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "license": "Apache-2.0", + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/browser-sync-ui/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browser-sync/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/browser-sync/node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/browser-sync/node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/browser-sync/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browser-sync/node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/browser-sync/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/browser-sync/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "peer": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "peer": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "peer": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "peer": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/connect/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/connect/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "peer": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "peer": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "peer": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "peer": true, + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "peer": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "license": "MIT", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "peer": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/cross-spawn/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "peer": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "peer": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "peer": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "peer": true + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "peer": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.82", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", + "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "peer": true + }, + "node_modules/es-module-shims": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.10.1.tgz", + "integrity": "sha512-HSSkRLkqFEyX6GrCAHrSOR5iz/QzQJRqZUF7bFJOZ4aoSw0WoSggfsTIGN2yFbF8v6xjQFhT4HGP6b+0qQZmEQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.0.tgz", + "integrity": "sha512-xhNn5tL1AhkPg4ft59yXT6FkwKXiPSYyz1IeinJHUJpjvOHOIPvdmFQc0pGdjxlKSbzZc2mNmtVOWAR1EF/JAg==", + "dev": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "peer": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "peer": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "peer": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "node_modules/fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "peer": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "node_modules/finalhandler/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "peer": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "peer": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "peer": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "peer": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "peer": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "peer": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/guacamole-frontend-ext-lib": { + "resolved": "../../guacamole/src/main/guacamole-frontend/dist/guacamole-frontend-ext-lib", + "link": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "peer": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", + "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "peer": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "peer": true + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "peer": true + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "dev": true, + "peer": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/http-proxy/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "peer": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "peer": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "license": "MIT", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "peer": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "peer": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "license": "ISC", + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "peer": true + }, + "node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "peer": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "peer": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "peer": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "peer": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "peer": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "peer": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.2.tgz", + "integrity": "sha512-LriG93la4PbmPMwI7Hbv8W+0ncLK7549w4sbZSi4QGDjnnxnmNMgxUkaQTEMzH8TpwsfFvgEjpLX7V8B/I9e3g==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.2.2", + "@lmdb/lmdb-darwin-x64": "3.2.2", + "@lmdb/lmdb-linux-arm": "3.2.2", + "@lmdb/lmdb-linux-arm64": "3.2.2", + "@lmdb/lmdb-linux-x64": "3.2.2", + "@lmdb/lmdb-win32-x64": "3.2.2" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "peer": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "peer": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "license": "MIT" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "peer": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "peer": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "peer": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "peer": true + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.0.tgz", + "integrity": "sha512-2v6aXUXwLP1Epd/gc32HAMIWoczx+fZwEPRHm/VwtrJzRGwR1qGZXEYV3Zp8ZjjbwaZhMrM6uHV4KVkk+XCc2w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "license": "MIT", + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "peer": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "peer": true + }, + "node_modules/ngx-build-plus": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-19.0.0.tgz", + "integrity": "sha512-JXzRvDZH1gQ2xGiePduHoMeR9kaK1cHHhT6d6npVBq8aWGFHs1iAg/5/gkieCsz2QfrcaoSF8ZBb9ZhcGX4Z2g==", + "dev": true, + "dependencies": { + "webpack-merge": "^5.0.0" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=19.0.0", + "@schematics/angular": ">=19.0.0", + "rxjs": ">= 6.0.0" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.0.0.tgz", + "integrity": "sha512-zQS+9MTTeCMgY0F3cWPyJyRFAkVltQ1uXm+xXu/ES6KFgC6Czo1Seb9vQW2wNxSX2OrDTiqL0ojtkFxBQ0ypIw==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "node_modules/nopt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.0.0.tgz", + "integrity": "sha512-1L/fTJ4UmV/lUxT2Uf006pfZKTvAgCF+chz+0OgBHO8u2Z67pE7AaAUUj7CJy0lXqHmymUvGFt6NE9R3HER0yw==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.0.tgz", + "integrity": "sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", + "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", + "dev": true, + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "peer": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "peer": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "peer": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "peer": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "node_modules/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "peer": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "peer": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "peer": true + }, + "node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.7.0.tgz", + "integrity": "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==", + "dev": true, + "peer": true, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "peer": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "license": "MIT", + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "peer": true, + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "peer": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "peer": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "peer": true, + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "peer": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "peer": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "peer": true + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "peer": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "peer": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "peer": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "peer": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "peer": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "peer": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "peer": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "peer": true + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "peer": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "peer": true + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "peer": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "peer": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "peer": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "peer": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/restore-cursor/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rollup": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "license": "Apache-2.0" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.80.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.7.tgz", + "integrity": "sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==", + "dev": true, + "peer": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.3.tgz", + "integrity": "sha512-gosNorT1RCkuCMyihv6FBRR7BMV06oKRAs+l4UMp1mlcVg9rWN6KMmUj3igjQwmYys4mDP3etEYJgiHRbgHCHA==", + "dev": true, + "peer": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "peer": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "peer": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "peer": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "peer": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "peer": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "peer": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "peer": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.0.0.tgz", + "integrity": "sha512-PHMifhh3EN4loMcHCz6l3v/luzgT3za+9f8subGgeMNjbJjzH4Ij/YoX3Gvu+kaouJRIlVdTHHCREADYf+ZteA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "peer": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "peer": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "peer": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "peer": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "dev": true, + "peer": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "peer": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "peer": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tuf-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "dev": true, + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "peer": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "peer": true + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "peer": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "peer": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "peer": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "peer": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "peer": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "peer": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", + "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "dev": true, + "peer": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.19.2", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "peer": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "peer": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peer": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "peer": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "peer": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "peer": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "peer": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==" + } + }, + "dependencies": { + "@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "requires": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@angular-architects/native-federation": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@angular-architects/native-federation/-/native-federation-19.0.5.tgz", + "integrity": "sha512-si09fH1WqXa0oRKqwpemsSF6iTlqvm91lRzdXTSxDQ1JnpKmn6eYYL1zeoVv2kOTp+nD/8vZxOW2G/NIvFjY6A==", + "requires": { + "@babel/core": "^7.19.0", + "@chialab/esbuild-plugin-commonjs": "^0.18.0", + "@softarc/native-federation": "2.0.18", + "@softarc/native-federation-runtime": "2.0.18", + "@types/browser-sync": "^2.29.0", + "browser-sync": "^3.0.2", + "esbuild": "^0.19.5", + "mrmime": "^1.0.1", + "npmlog": "^6.0.2", + "process": "0.11.10" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "optional": true + }, + "esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "requires": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==" + } + } + }, + "@angular-devkit/architect": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", + "integrity": "sha512-3dRV0IB+MbNYbAGbYEFMcABkMphqcTvn5MG79dQkwcf2a9QZxCq2slwf/rIleWoDUcFm9r1NnVPYrTYNYJaqQg==", + "dev": true, + "requires": { + "@angular-devkit/core": "19.0.7", + "rxjs": "7.8.1" + } + }, + "@angular-devkit/build-angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.0.7.tgz", + "integrity": "sha512-R0vpJ+P5xBqF82zOMq2FvOP7pJz5NZ7PwHAIFuQ6z50SHLW/VcUA19ZoFKwxBX6A/Soyb66QXTcjZ5wbRqMm8w==", + "dev": true, + "peer": true, + "requires": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/build-webpack": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular/build": "19.0.7", + "@babel/core": "7.26.0", + "@babel/generator": "7.26.2", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.9", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.25.9", + "@babel/preset-env": "7.26.0", + "@babel/runtime": "7.26.0", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.0.7", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild": "0.24.0", + "esbuild-wasm": "0.24.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.3", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "postcss": "8.4.49", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.80.7", + "sass-loader": "16.0.3", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.36.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.96.1", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.1.0", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "dependencies": { + "@angular/build": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.0.7.tgz", + "integrity": "sha512-AFvhRa6sfXG8NmS8AN7TvE8q2kVcMw+zXMZzo981cqwnOwJy4VHU0htqm5OZQnohVJM0pP8SBAuROWO4yRrxCA==", + "dev": true, + "peer": true, + "requires": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.0.2", + "@vitejs/plugin-basic-ssl": "1.1.0", + "beasties": "0.1.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "lmdb": "3.1.5", + "magic-string": "0.30.12", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "rollup": "4.26.0", + "sass": "1.80.7", + "semver": "7.6.3", + "vite": "5.4.11", + "watchpack": "2.4.2" + } + }, + "@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dev": true, + "peer": true, + "requires": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@lmdb/lmdb-darwin-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.1.5.tgz", + "integrity": "sha512-ue5PSOzHMCIYrfvPP/MRS6hsKKLzqqhcdAvJCO8uFlDdj598EhgnacuOTuqA6uBK5rgiZXfDWyb7DVZSiBKxBA==", + "dev": true, + "optional": true, + "peer": true + }, + "@lmdb/lmdb-darwin-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.1.5.tgz", + "integrity": "sha512-CGhsb0R5vE6mMNCoSfxHFD8QTvBHM51gs4DBeigTYHWnYv2V5YpJkC4rMo5qAAFifuUcc0+a8a3SIU0c9NrfNw==", + "dev": true, + "optional": true, + "peer": true + }, + "@lmdb/lmdb-linux-arm": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.1.5.tgz", + "integrity": "sha512-3WeW328DN+xB5PZdhSWmqE+t3+44xWXEbqQ+caWJEZfOFdLp9yklBZEbVqVdqzznkoaXJYxTCp996KD6HmANeg==", + "dev": true, + "optional": true, + "peer": true + }, + "@lmdb/lmdb-linux-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.1.5.tgz", + "integrity": "sha512-LAjaoOcBHGj6fiYB8ureiqPoph4eygbXu4vcOF+hsxiY74n8ilA7rJMmGUT0K0JOB5lmRQHSmor3mytRjS4qeQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@lmdb/lmdb-linux-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.1.5.tgz", + "integrity": "sha512-k/IklElP70qdCXOQixclSl2GPLFiopynGoKX1FqDd1/H0E3Fo1oPwjY2rEVu+0nS3AOw1sryStdXk8CW3cVIsw==", + "dev": true, + "optional": true, + "peer": true + }, + "@lmdb/lmdb-win32-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.1.5.tgz", + "integrity": "sha512-KYar6W8nraZfSJspcK7Kp7hdj238X/FNauYbZyrqPBrtsXI1hvI4/KcRcRGP50aQoV7fkKDyJERlrQGMGTZUsA==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.26.0.tgz", + "integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.26.0.tgz", + "integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.26.0.tgz", + "integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.26.0.tgz", + "integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.26.0.tgz", + "integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.26.0.tgz", + "integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.26.0.tgz", + "integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.26.0.tgz", + "integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.26.0.tgz", + "integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.26.0.tgz", + "integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.26.0.tgz", + "integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.26.0.tgz", + "integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.26.0.tgz", + "integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.26.0.tgz", + "integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.26.0.tgz", + "integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.26.0.tgz", + "integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.26.0.tgz", + "integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==", + "dev": true, + "optional": true, + "peer": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.26.0.tgz", + "integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==", + "dev": true, + "optional": true, + "peer": true + }, + "beasties": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.1.0.tgz", + "integrity": "sha512-+Ssscd2gVG24qRNC+E2g88D+xsQW4xwakWtKAiGEQ3Pw54/FGdyo9RrfxhGhEv6ilFVbB7r3Lgx+QnAxnSpECw==", + "dev": true, + "peer": true, + "requires": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-media-query-parser": "^0.2.3" + } + }, + "lmdb": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.1.5.tgz", + "integrity": "sha512-46Mch5Drq+A93Ss3gtbg+Xuvf5BOgIuvhKDWoGa3HcPHI6BL2NCOkRdSx1D4VfzwrxhnsjbyIVsLRlQHu6URvw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@lmdb/lmdb-darwin-arm64": "3.1.5", + "@lmdb/lmdb-darwin-x64": "3.1.5", + "@lmdb/lmdb-linux-arm": "3.1.5", + "@lmdb/lmdb-linux-arm64": "3.1.5", + "@lmdb/lmdb-linux-x64": "3.1.5", + "@lmdb/lmdb-win32-x64": "3.1.5", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + } + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "peer": true + }, + "postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "peer": true, + "requires": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "rollup": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.26.0.tgz", + "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", + "dev": true, + "peer": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.26.0", + "@rollup/rollup-android-arm64": "4.26.0", + "@rollup/rollup-darwin-arm64": "4.26.0", + "@rollup/rollup-darwin-x64": "4.26.0", + "@rollup/rollup-freebsd-arm64": "4.26.0", + "@rollup/rollup-freebsd-x64": "4.26.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.26.0", + "@rollup/rollup-linux-arm-musleabihf": "4.26.0", + "@rollup/rollup-linux-arm64-gnu": "4.26.0", + "@rollup/rollup-linux-arm64-musl": "4.26.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.26.0", + "@rollup/rollup-linux-riscv64-gnu": "4.26.0", + "@rollup/rollup-linux-s390x-gnu": "4.26.0", + "@rollup/rollup-linux-x64-gnu": "4.26.0", + "@rollup/rollup-linux-x64-musl": "4.26.0", + "@rollup/rollup-win32-arm64-msvc": "4.26.0", + "@rollup/rollup-win32-ia32-msvc": "4.26.0", + "@rollup/rollup-win32-x64-msvc": "4.26.0", + "@types/estree": "1.0.6", + "fsevents": "~2.3.2" + } + }, + "webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "peer": true, + "requires": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + } + } + } + }, + "@angular-devkit/build-webpack": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1900.7.tgz", + "integrity": "sha512-F0S0iyspo/9w9rP5F9wmL+ZkBr48YQIWiFu+PaQ0in/lcdRmY/FjVHTMa5BMnlew9VCtFHPvpoN9x4u8AIoWXA==", + "dev": true, + "peer": true, + "requires": { + "@angular-devkit/architect": "0.1900.7", + "rxjs": "7.8.1" + } + }, + "@angular-devkit/core": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.7.tgz", + "integrity": "sha512-VyuORSitT6LIaGUEF0KEnv2TwNaeWl6L3/4L4stok0BJ23B4joVca2DYVcrLC1hSzz8V4dwVgSlbNIgjgGdVpg==", + "dev": true, + "requires": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "dependencies": { + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + } + } + }, + "@angular-devkit/schematics": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.7.tgz", + "integrity": "sha512-BHXQv6kMc9xo4TH9lhwMv8nrZXHkLioQvLun2qYjwvOsyzt3qd+sUM9wpHwbG6t+01+FIQ05iNN9ox+Cvpndgg==", + "dev": true, + "requires": { + "@angular-devkit/core": "19.0.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" + } + }, + "@angular/animations": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.0.6.tgz", + "integrity": "sha512-dlXrFcw7RQNze1zjmrbwqcFd6zgEuqKwuExtEN1Fy26kQ+wqKIhYO6IG7PZGef53XpwN5DT16yve6UihJ2XeNg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/build": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.1.tgz", + "integrity": "sha512-E4jvi48zRCLkwcThQQm1Q1vq9aypf3+xSMQQMDcHzvt/89sucWTKkwgNHSW/Fi8qH23GhijCLXBHa4dHSoCB/A==", + "dev": true, + "requires": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1901.1", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.1.1", + "@vitejs/plugin-basic-ssl": "1.2.0", + "beasties": "0.2.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.2", + "fast-glob": "3.3.3", + "https-proxy-agent": "7.0.6", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "lmdb": "3.2.2", + "magic-string": "0.30.17", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.8.0", + "rollup": "4.30.1", + "sass": "1.83.1", + "semver": "7.6.3", + "vite": "6.0.7", + "watchpack": "2.4.2" + }, + "dependencies": { + "@angular-devkit/architect": { + "version": "0.1901.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.1.tgz", + "integrity": "sha512-fhRID3z4Va1nvP4QS7iraMP+J0zpvsqax0MtoaHXiaSvruwcPbcYSvWj9aO/oo9cq2XTd1zVigrKUwfhabXRVw==", + "dev": true, + "requires": { + "@angular-devkit/core": "19.1.1", + "rxjs": "7.8.1" + } + }, + "@angular-devkit/core": { + "version": "19.1.1", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.1.tgz", + "integrity": "sha512-CAqst7WEasPHR4OFdbxxX3+NVqNTvYk3vtPbXT/jZ0L2EZRICQta2EClkdhSIiMkiMf0/2LNT05rYD7k4NHIQA==", + "dev": true, + "requires": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + } + }, + "@esbuild/aix-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", + "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", + "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "dev": true, + "optional": true + }, + "@esbuild/android-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", + "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "dev": true, + "optional": true + }, + "@esbuild/android-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", + "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", + "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "dev": true, + "optional": true + }, + "@esbuild/darwin-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", + "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", + "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "dev": true, + "optional": true + }, + "@esbuild/freebsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", + "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", + "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "dev": true, + "optional": true + }, + "@esbuild/linux-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", + "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", + "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-loong64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", + "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "dev": true, + "optional": true + }, + "@esbuild/linux-mips64el": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", + "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-ppc64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", + "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-riscv64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", + "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "dev": true, + "optional": true + }, + "@esbuild/linux-s390x": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", + "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "dev": true, + "optional": true + }, + "@esbuild/linux-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", + "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", + "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", + "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "dev": true, + "optional": true + }, + "@esbuild/openbsd-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", + "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "dev": true, + "optional": true + }, + "@esbuild/sunos-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", + "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "dev": true, + "optional": true + }, + "@esbuild/win32-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", + "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "dev": true, + "optional": true + }, + "@esbuild/win32-ia32": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", + "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "dev": true, + "optional": true + }, + "@esbuild/win32-x64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", + "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "dev": true, + "optional": true + }, + "@inquirer/confirm": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", + "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.2", + "@inquirer/type": "^3.0.2" + } + }, + "@vitejs/plugin-basic-ssl": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", + "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", + "dev": true, + "requires": {} + }, + "esbuild": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", + "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "dev": true, + "requires": { + "@esbuild/aix-ppc64": "0.24.2", + "@esbuild/android-arm": "0.24.2", + "@esbuild/android-arm64": "0.24.2", + "@esbuild/android-x64": "0.24.2", + "@esbuild/darwin-arm64": "0.24.2", + "@esbuild/darwin-x64": "0.24.2", + "@esbuild/freebsd-arm64": "0.24.2", + "@esbuild/freebsd-x64": "0.24.2", + "@esbuild/linux-arm": "0.24.2", + "@esbuild/linux-arm64": "0.24.2", + "@esbuild/linux-ia32": "0.24.2", + "@esbuild/linux-loong64": "0.24.2", + "@esbuild/linux-mips64el": "0.24.2", + "@esbuild/linux-ppc64": "0.24.2", + "@esbuild/linux-riscv64": "0.24.2", + "@esbuild/linux-s390x": "0.24.2", + "@esbuild/linux-x64": "0.24.2", + "@esbuild/netbsd-arm64": "0.24.2", + "@esbuild/netbsd-x64": "0.24.2", + "@esbuild/openbsd-arm64": "0.24.2", + "@esbuild/openbsd-x64": "0.24.2", + "@esbuild/sunos-x64": "0.24.2", + "@esbuild/win32-arm64": "0.24.2", + "@esbuild/win32-ia32": "0.24.2", + "@esbuild/win32-x64": "0.24.2" + } + }, + "fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + } + }, + "https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "4" + } + }, + "magic-string": { + "version": "0.30.17", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", + "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true + }, + "piscina": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", + "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", + "dev": true, + "requires": { + "@napi-rs/nice": "^1.0.1" + } + }, + "sass": { + "version": "1.83.1", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", + "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", + "dev": true, + "requires": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "vite": { + "version": "6.0.7", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", + "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "dev": true, + "requires": { + "esbuild": "^0.24.2", + "fsevents": "~2.3.3", + "postcss": "^8.4.49", + "rollup": "^4.23.0" + } + } + } + }, + "@angular/cli": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.0.7.tgz", + "integrity": "sha512-y6C4B4XdiZwe2+OADLWXyKqUVvW/XDzTuJ2mZ5PhTnSiiXDN4zRWId1F5wA8ve8vlbUKApPHXRQuaqiQJmA24g==", + "dev": true, + "requires": { + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "@inquirer/prompts": "7.1.0", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.0.7", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.0", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + } + }, + "@angular/common": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.0.6.tgz", + "integrity": "sha512-r9IDD0+UGkrQkjyX+pApeDmIJ9INpr1uYlgmmlWNBJCVNr9SKKIVZV60sssgadew6bGynKN9dW4mGsmEzzb5BA==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.0.6.tgz", + "integrity": "sha512-g8A6QOsiCJnRi5Hz0sASIpRQoAGxEgnjz0JanfrMNRedY4MpdIS1V0AeCSKTsMRlV7tQl3ng2Gse/tsb51HI3Q==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/compiler-cli": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.0.6.tgz", + "integrity": "sha512-fHtwI5rCe3LmKDoaqlqLAPdNmLrbeCiMYVe+X1BHgApaqNCyAwcuJxuf8Q5R5su7nHiLmlmB74o1ZS/V+0cQ+g==", + "dev": true, + "requires": { + "@babel/core": "7.26.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + } + }, + "@angular/core": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.0.6.tgz", + "integrity": "sha512-9N7FmdRHtS7zfXC/wnyap/reX7fgiOrWpVivayHjWP4RkLYXJAzJIpLyew0jrx4vf8r3lZnC0Zmq0PW007Ngjw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/forms": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.0.6.tgz", + "integrity": "sha512-HogauPvgDQHw2xxqKBaFgKTRRcc1xWeI/PByDCf3U6YsaqpF53Mz2CJh8X2bg2bY1RGKb67MZw7DBGFRvXx4bg==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/platform-browser": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.6.tgz", + "integrity": "sha512-MWiToGy7Pa0rR61sgnEuu7dfZXpAw0g7nkSnw4xdjUf974OOOfI1LS9O9YevJibtdW8sPa1HaoXXwcb7N03B5A==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/platform-browser-dynamic": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.0.6.tgz", + "integrity": "sha512-5TGLOwPlLHXJ1+Hs9b3dEmGdTpb7dfLYalVmiMUZOFBry1sMaRuw+nyqjmWn1GP3yD156hzt5QDzWA8A134AfQ==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@angular/router": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.0.6.tgz", + "integrity": "sha512-G1oz+TclPk48h6b6B4s5J3DfrDVJrrxKOA+KWeVQP4e1B8ld7/dCMf5nn3yqS4BGs4yLecxMxyvbOvOiZ//lxw==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "requires": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + } + }, + "@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==" + }, + "@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "dependencies": { + "convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "requires": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "requires": { + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "requires": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==" + } + } + }, + "@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true + } + } + }, + "@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true + } + } + }, + "@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + } + }, + "@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "requires": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "requires": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true + }, + "@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + } + }, + "@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "peer": true, + "requires": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "requires": { + "@babel/types": "^7.24.7" + } + }, + "@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==" + }, + "@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==" + }, + "@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==" + }, + "@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "peer": true, + "requires": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "requires": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + } + }, + "@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "requires": { + "@babel/types": "^7.26.5" + } + }, + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + } + }, + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "peer": true, + "requires": {} + }, + "@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + } + }, + "@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + } + }, + "@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.26.5" + } + }, + "@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + } + }, + "@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + } + }, + "@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + } + }, + "@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + } + }, + "@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.26.5" + } + }, + "@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + } + }, + "@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + } + }, + "@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + } + }, + "@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + } + }, + "@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-runtime": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", + "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true + } + } + }, + "@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + } + }, + "@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + } + }, + "@babel/preset-env": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, + "peer": true, + "requires": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true + } + } + }, + "@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + } + }, + "@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dev": true, + "peer": true, + "requires": { + "regenerator-runtime": "^0.14.0" + } + }, + "@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "requires": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + } + }, + "@babel/traverse": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "requires": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + } + }, + "@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "requires": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + } + }, + "@chialab/cjs-to-esm": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/cjs-to-esm/-/cjs-to-esm-0.18.0.tgz", + "integrity": "sha512-fm8X9NhPO5pyUB7gxOZgwxb8lVq1UD4syDJCpqh6x4zGME6RTck7BguWZ4Zgv3GML4fQ4KZtyRwP5eoDgNGrmA==", + "requires": { + "@chialab/estransform": "^0.18.0" + } + }, + "@chialab/esbuild-plugin-commonjs": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-commonjs/-/esbuild-plugin-commonjs-0.18.0.tgz", + "integrity": "sha512-qZjIsNr1dVEJk6NLyza3pJLHeY7Fz0xjmYteKXElCnlFSKR7vVg6d18AsxVpRnP5qNbvx3XlOvs9U8j97ZQ6bw==", + "requires": { + "@chialab/cjs-to-esm": "^0.18.0", + "@chialab/esbuild-rna": "^0.18.0" + } + }, + "@chialab/esbuild-rna": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", + "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", + "requires": { + "@chialab/estransform": "^0.18.0", + "@chialab/node-resolve": "^0.18.0" + } + }, + "@chialab/estransform": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", + "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", + "requires": { + "@parcel/source-map": "^2.0.0" + } + }, + "@chialab/node-resolve": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", + "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==" + }, + "@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "peer": true + }, + "@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/netbsd-arm64": { + "version": "0.24.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", + "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "dev": true, + "optional": true + }, + "@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "dev": true, + "optional": true, + "peer": true + }, + "@inquirer/checkbox": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", + "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/confirm": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", + "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + } + }, + "@inquirer/core": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", + "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "dev": true, + "requires": { + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "dependencies": { + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + } + } + }, + "@inquirer/editor": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", + "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "external-editor": "^3.1.0" + } + }, + "@inquirer/expand": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", + "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/figures": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", + "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "dev": true + }, + "@inquirer/input": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", + "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + } + }, + "@inquirer/number": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", + "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + } + }, + "@inquirer/password": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", + "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2" + } + }, + "@inquirer/prompts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", + "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "dev": true, + "requires": { + "@inquirer/checkbox": "^4.0.2", + "@inquirer/confirm": "^5.0.2", + "@inquirer/editor": "^4.1.0", + "@inquirer/expand": "^4.0.2", + "@inquirer/input": "^4.0.2", + "@inquirer/number": "^3.0.2", + "@inquirer/password": "^4.0.2", + "@inquirer/rawlist": "^4.0.2", + "@inquirer/search": "^3.0.2", + "@inquirer/select": "^4.0.2" + } + }, + "@inquirer/rawlist": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", + "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/search": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", + "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/select": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", + "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "dev": true, + "requires": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + } + }, + "@inquirer/type": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", + "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "dev": true, + "requires": {} + }, + "@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "requires": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true + }, + "string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "requires": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + } + } + } + }, + "@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "requires": { + "minipass": "^7.0.4" + } + }, + "@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true + }, + "@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "requires": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==" + }, + "@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==" + }, + "@jridgewell/source-map": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.3.tgz", + "integrity": "sha512-b+fsZXeLYi9fEULmfBrhxn4IrPlINf8fiNarzTof004v3lFdntdwa9PF7vFJqm3mg7s+ScJMxXaE3Acp1irZcg==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/gen-mapping": "^0.3.0", + "@jridgewell/trace-mapping": "^0.3.9" + } + }, + "@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "requires": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "peer": true, + "requires": {} + }, + "@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "peer": true, + "requires": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + } + }, + "@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "peer": true, + "requires": {} + }, + "@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true, + "peer": true + }, + "@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "requires": { + "@inquirer/type": "^1.5.5" + }, + "dependencies": { + "@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "requires": { + "mute-stream": "^1.0.0" + } + }, + "mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true + } + } + }, + "@lmdb/lmdb-darwin-arm64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz", + "integrity": "sha512-WBSJT9Z7DTol5viq+DZD2TapeWOw7mlwXxiSBHgAzqVwsaVb0h/ekMD9iu/jDD8MUA20tO9N0WEdnT06fsUp+g==", + "dev": true, + "optional": true + }, + "@lmdb/lmdb-darwin-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.2.tgz", + "integrity": "sha512-4S13kUtR7c/j/MzkTIBJCXv52hQ41LG2ukeaqw4Eng9K0pNKLFjo1sDSz96/yKhwykxrWDb13ddJ/ZqD3rAhUA==", + "dev": true, + "optional": true + }, + "@lmdb/lmdb-linux-arm": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.2.tgz", + "integrity": "sha512-uW31JmfuPAaLUYW7NsEU8gzwgDAzpGPwjvkxnKlcWd8iDutoPKDJi8Wk9lFmPEZRxVSB0j1/wDQ7N2qliR9UFA==", + "dev": true, + "optional": true + }, + "@lmdb/lmdb-linux-arm64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.2.tgz", + "integrity": "sha512-4hdgZtWI1idQlWRp+eleWXD9KLvObgboRaVoBj2POdPEYvsKANllvMW0El8tEQwtw74yB9NT6P8ENBB5UJf5+g==", + "dev": true, + "optional": true + }, + "@lmdb/lmdb-linux-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.2.tgz", + "integrity": "sha512-A0zjf4a2vM4B4GAx78ncuOTZ8Ka1DbTaG1Axf1e00Sa7f5coqlWiLg1PX7Gxvyibc2YqtqB+8tg1KKrE8guZVw==", + "dev": true, + "optional": true + }, + "@lmdb/lmdb-win32-x64": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.2.tgz", + "integrity": "sha512-Y0qoSCAja+xZE7QQ0LCHoYAuyI1n9ZqukQJa8lv9X3yCvWahFF7OYHAgVH1ejp43XWstj3U89/PAAzcowgF/uQ==", + "dev": true, + "optional": true + }, + "@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "dev": true, + "optional": true, + "peer": true + }, + "@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "dev": true, + "optional": true, + "peer": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "dev": true, + "optional": true, + "peer": true + }, + "@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "dev": true, + "optional": true, + "peer": true + }, + "@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "dev": true, + "optional": true, + "peer": true + }, + "@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "optional": true, + "requires": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "dev": true, + "optional": true, + "peer": true + }, + "@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "dev": true, + "optional": true, + "peer": true + }, + "@ngtools/webpack": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.0.7.tgz", + "integrity": "sha512-jWyMuqtLKZB8Jnuqo27mG2cCQdl71lhM1oEdq3x7Z/QOrm2I+8EfyAzOLxB1f1vXt85O1bz3nf66CkuVCVGGTQ==", + "dev": true, + "peer": true, + "requires": {} + }, + "@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "requires": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + } + }, + "@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true + }, + "@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "requires": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + } + }, + "@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, + "@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "requires": { + "semver": "^7.3.5" + } + }, + "@npmcli/git": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", + "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "dev": true, + "requires": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, + "@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "requires": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + } + }, + "@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true + }, + "@npmcli/package-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.0.tgz", + "integrity": "sha512-t6G+6ZInT4X+tqj2i+wlLIeCKnKOTuz9/VFYDtj+TGTur5q7sp/OYrQA19LdBbWfXDOi0Y4jtedV6xtB8zQ9ug==", + "dev": true, + "requires": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "normalize-package-data": "^7.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3" + } + }, + "@npmcli/promise-spawn": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "dev": true, + "requires": { + "which": "^5.0.0" + } + }, + "@npmcli/redact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.0.0.tgz", + "integrity": "sha512-/1uFzjVcfzqrgCeGW7+SZ4hv0qLWmKXVzFahZGJ6QuJBj6Myt9s17+JL86i76NV9YSnJRcGXJYQbAU0rn1YTCQ==", + "dev": true + }, + "@npmcli/run-script": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.0.2.tgz", + "integrity": "sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw==", + "dev": true, + "requires": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + } + }, + "@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "requires": { + "detect-libc": "^1.0.3" + }, + "dependencies": { + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==" + } + } + }, + "@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "optional": true, + "requires": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0", + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "dependencies": { + "detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true + }, + "node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + } + } + }, + "@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "dev": true, + "optional": true, + "peer": true + }, + "@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "dev": true, + "optional": true, + "peer": true + }, + "@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm-eabi": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", + "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-android-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", + "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", + "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", + "dev": true, + "optional": true + }, + "@rollup/rollup-darwin-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", + "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-arm64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", + "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-freebsd-x64": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", + "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", + "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm-musleabihf": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", + "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", + "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-arm64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", + "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-loongarch64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", + "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", + "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-riscv64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", + "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-s390x-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", + "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-gnu": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", + "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", + "dev": true, + "optional": true + }, + "@rollup/rollup-linux-x64-musl": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", + "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-arm64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", + "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-ia32-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", + "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", + "dev": true, + "optional": true + }, + "@rollup/rollup-win32-x64-msvc": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", + "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", + "dev": true, + "optional": true + }, + "@rspack/binding": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.1.8.tgz", + "integrity": "sha512-+/JzXx1HctfgPj+XtsCTbRkxiaOfAXGZZLEvs7jgp04WgWRSZ5u97WRCePNPvy+sCfOEH/2zw2ZK36Z7oQRGhQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@rspack/binding-darwin-arm64": "1.1.8", + "@rspack/binding-darwin-x64": "1.1.8", + "@rspack/binding-linux-arm64-gnu": "1.1.8", + "@rspack/binding-linux-arm64-musl": "1.1.8", + "@rspack/binding-linux-x64-gnu": "1.1.8", + "@rspack/binding-linux-x64-musl": "1.1.8", + "@rspack/binding-win32-arm64-msvc": "1.1.8", + "@rspack/binding-win32-ia32-msvc": "1.1.8", + "@rspack/binding-win32-x64-msvc": "1.1.8" + } + }, + "@rspack/binding-darwin-arm64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.1.8.tgz", + "integrity": "sha512-I7avr471ghQ3LAqKm2fuXuJPLgQ9gffn5Q4nHi8rsukuZUtiLDPfYzK1QuupEp2JXRWM1gG5lIbSUOht3cD6Ug==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-darwin-x64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.1.8.tgz", + "integrity": "sha512-vfqf/c+mcx8rr1M8LnqKmzDdnrgguflZnjGerBLjNerAc+dcUp3lCvNxRIvZ2TkSZZBW8BpCMgjj3n70CZ4VLQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-linux-arm64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.8.tgz", + "integrity": "sha512-lZlO/rAJSeozi+qtVLkGSXfe+riPawCwM4FsrflELfNlvvEXpANwtrdJ+LsaNVXcgvhh50ZX2KicTdmx9G2b6Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-linux-arm64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.8.tgz", + "integrity": "sha512-bX7exULSZwy8xtDh6Z65b6sRC4uSxGuyvSLCEKyhmG6AnJkg0gQMxk3hoO0hWnyGEZgdJEn+jEhk0fjl+6ZRAQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-linux-x64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.8.tgz", + "integrity": "sha512-2Prw2USgTJ3aLdLExfik8pAwAHbX4MZrACBGEmR7Vbb56kLjC+++fXkciRc50pUDK4JFr1VQ7eNZrJuDR6GG6Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-linux-x64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.8.tgz", + "integrity": "sha512-bnVGB/mQBKEdzOU/CPmcOE3qEXxGOGGW7/i6iLl2MamVOykJq8fYjL9j86yi6L0r009ja16OgWckykQGc4UqGw==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-win32-arm64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.8.tgz", + "integrity": "sha512-u+na3gxhzeksm4xZyAzn1+XWo5a5j7hgWA/KcFPDQ8qQNkRknx4jnQMxVtcZ9pLskAYV4AcOV/AIximx7zvv8A==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-win32-ia32-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.1.8.tgz", + "integrity": "sha512-FijUxym1INd5fFHwVCLuVP8XEAb4Sk1sMwEEQUlugiDra9ZsLaPw4OgPGxbxkD6SB0DeUz9Zq46Xbcf6d3OgfA==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/binding-win32-x64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.8.tgz", + "integrity": "sha512-SBzIcND4qpDt71jlu1MCDxt335tqInT3YID9V4DoQ4t8wgM/uad7EgKOWKTK6vc2RRaOIShfS2XzqjNUxPXh4w==", + "dev": true, + "optional": true, + "peer": true + }, + "@rspack/core": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.1.8.tgz", + "integrity": "sha512-pcZtcj5iXLCuw9oElTYC47bp/RQADm/MMEb3djHdwJuSlFWfWPQi5QFgJ/lJAxIW9UNHnTFrYtytycfjpuoEcA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@module-federation/runtime-tools": "0.5.1", + "@rspack/binding": "1.1.8", + "@rspack/lite-tapable": "1.0.1", + "caniuse-lite": "^1.0.30001616" + }, + "dependencies": { + "@module-federation/runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz", + "integrity": "sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@module-federation/sdk": "0.5.1" + } + }, + "@module-federation/runtime-tools": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz", + "integrity": "sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@module-federation/runtime": "0.5.1", + "@module-federation/webpack-bundler-runtime": "0.5.1" + } + }, + "@module-federation/sdk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.5.1.tgz", + "integrity": "sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==", + "dev": true, + "optional": true, + "peer": true + }, + "@module-federation/webpack-bundler-runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz", + "integrity": "sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@module-federation/runtime": "0.5.1", + "@module-federation/sdk": "0.5.1" + } + } + } + }, + "@rspack/lite-tapable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", + "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", + "dev": true, + "optional": true, + "peer": true + }, + "@schematics/angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.7.tgz", + "integrity": "sha512-1WtTqKFPuEaV99VIP+y/gf/XW3TVJh/NbJbbEF4qYpp7qQiJ4ntF4klVZmsJcQzFucZSzlg91QVMPQKev5WZGA==", + "dev": true, + "requires": { + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "jsonc-parser": "3.3.1" + } + }, + "@sigstore/bundle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", + "integrity": "sha512-XDUYX56iMPAn/cdgh/DTJxz5RWmqKV4pwvUAEKEWJl+HzKdCd/24wUa9JYNMlDSCb7SUHAdtksxYX779Nne/Zg==", + "dev": true, + "requires": { + "@sigstore/protobuf-specs": "^0.3.2" + } + }, + "@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true + }, + "@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true + }, + "@sigstore/sign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.0.0.tgz", + "integrity": "sha512-UjhDMQOkyDoktpXoc5YPJpJK6IooF2gayAr5LvXI4EL7O0vd58okgfRcxuaH+YTdhvb5aa1Q9f+WJ0c2sVuYIw==", + "dev": true, + "requires": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + } + }, + "@sigstore/tuf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.0.0.tgz", + "integrity": "sha512-9Xxy/8U5OFJu7s+OsHzI96IX/OzjF/zj0BSSaWhgJgTqtlBhQIV2xdrQI5qxLD7+CWWDepadnXAxzaZ3u9cvRw==", + "dev": true, + "requires": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" + } + }, + "@sigstore/verify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.0.0.tgz", + "integrity": "sha512-Ggtq2GsJuxFNUvQzLoXqRwS4ceRfLAJnrIHUDrzAD0GgnOhwujJkKkxM/s5Bako07c3WtAs/sZo5PJq7VHjeDg==", + "dev": true, + "requires": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + } + }, + "@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "peer": true + }, + "@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "@softarc/native-federation": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation/-/native-federation-2.0.18.tgz", + "integrity": "sha512-2QhZZjm3YGTKi7VHlYC4GyBwx+fuc+SM2djl5CAionap/WuBzsucxOmo3ECbLS6tfoc0kGeYSVMX1EcwWa+F5A==", + "requires": { + "@softarc/native-federation-runtime": "2.0.18", + "json5": "^2.2.0", + "npmlog": "^6.0.2" + } + }, + "@softarc/native-federation-node": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-node/-/native-federation-node-2.0.18.tgz", + "integrity": "sha512-2bV/oB2BpGibs62/PmNxH6VIngsAWdZGLUl4oLjzrwgxmEw+rvIW9EhJ+S6zZz0bmz/YZCFxEowBETpP779fxQ==" + }, + "@softarc/native-federation-runtime": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-runtime/-/native-federation-runtime-2.0.18.tgz", + "integrity": "sha512-kCfegbei6FVbGkCZfWe5IrNuRcNZGU517HeRwaoCtXvk2/2zudGRD9ftWmhDAV1N85N3tvXEsO65YFSI7Ccr5g==", + "requires": { + "tslib": "^2.3.0" + } + }, + "@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true + }, + "@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "requires": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + } + }, + "@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "peer": true, + "requires": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/browser-sync": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/@types/browser-sync/-/browser-sync-2.29.0.tgz", + "integrity": "sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==", + "requires": { + "@types/micromatch": "^2", + "@types/node": "*", + "@types/serve-static": "*", + "chokidar": "^3.0.0" + }, + "dependencies": { + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + } + } + }, + "@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "peer": true, + "requires": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "requires": { + "@types/node": "*" + } + }, + "@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "peer": true, + "requires": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "peer": true, + "requires": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "peer": true, + "requires": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + }, + "dependencies": { + "@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + } + } + }, + "@types/express-serve-static-core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "peer": true + }, + "@types/micromatch": { + "version": "2.3.35", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.35.tgz", + "integrity": "sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==", + "requires": { + "@types/parse-glob": "*" + } + }, + "@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "requires": { + "undici-types": "~6.20.0" + } + }, + "@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/parse-glob": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@types/parse-glob/-/parse-glob-3.0.32.tgz", + "integrity": "sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==" + }, + "@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true, + "peer": true + }, + "@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "peer": true + }, + "@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true, + "peer": true + }, + "@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "requires": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "peer": true, + "requires": { + "@types/express": "*" + } + }, + "@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "requires": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*" + } + }, + "@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "peer": true, + "requires": {} + }, + "@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "peer": true + }, + "@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "peer": true, + "requires": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "peer": true, + "requires": { + "@xtuc/long": "4.2.2" + } + }, + "@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "peer": true + }, + "@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "peer": true, + "requires": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "peer": true + }, + "@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "peer": true + }, + "@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true + }, + "accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "requires": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "dependencies": { + "negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==" + } + } + }, + "acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "peer": true + }, + "adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "peer": true, + "requires": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "peer": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, + "agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true + }, + "ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + } + }, + "ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "requires": { + "ajv": "^8.0.0" + } + }, + "ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3" + } + }, + "ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "peer": true + }, + "ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "requires": { + "type-fest": "^0.21.3" + } + }, + "ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "peer": true + }, + "ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==" + }, + "ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "requires": { + "color-convert": "^2.0.1" + } + }, + "anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "requires": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + } + }, + "aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==" + }, + "are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "requires": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + } + }, + "argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "peer": true + }, + "array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true, + "peer": true + }, + "async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "requires": { + "lodash": "^4.17.14" + } + }, + "async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==" + }, + "autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "peer": true, + "requires": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + } + }, + "babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "peer": true, + "requires": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + } + }, + "babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "peer": true, + "requires": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "peer": true + } + } + }, + "babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + } + }, + "babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "peer": true, + "requires": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + } + }, + "balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true + }, + "base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" + }, + "batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "beasties": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", + "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "dev": true, + "requires": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.1.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.49", + "postcss-media-query-parser": "^0.2.3" + } + }, + "big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "peer": true + }, + "binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==" + }, + "bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "requires": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "peer": true, + "requires": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "peer": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true + } + } + }, + "bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "requires": { + "balanced-match": "^1.0.0" + } + }, + "braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "requires": { + "fill-range": "^7.1.1" + } + }, + "browser-sync": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", + "requires": { + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.8", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "dependencies": { + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + }, + "fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==" + }, + "jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "requires": { + "graceful-fs": "^4.1.6" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "requires": { + "picomatch": "^2.2.1" + } + }, + "universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" + } + } + }, + "browser-sync-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "requires": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + } + }, + "browser-sync-ui": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "requires": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + }, + "dependencies": { + "connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==" + }, + "immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==" + } + } + }, + "browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "requires": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + } + }, + "bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==" + }, + "buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "requires": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "peer": true + }, + "bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "peer": true, + "requires": { + "run-applescript": "^7.0.0" + } + }, + "bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==" + }, + "cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "requires": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "dependencies": { + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true + }, + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true + }, + "tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + } + }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true + } + } + }, + "call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "peer": true, + "requires": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + } + }, + "call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "peer": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + } + }, + "callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "peer": true + }, + "caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==" + }, + "chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "requires": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + } + }, + "chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "requires": { + "readdirp": "^4.0.1" + } + }, + "chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true + }, + "chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "peer": true + }, + "cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "requires": { + "restore-cursor": "^3.1.0" + } + }, + "cli-spinners": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.0.tgz", + "integrity": "sha512-4/aL9X3Wh0yiMQlE+eeRhWP6vclO3QRtw1JHKIT0FFUs5FjpFmESqtMvYZ0+lbzBw900b95mS0hohy+qn2VK/g==", + "dev": true + }, + "cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "requires": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + } + } + }, + "cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true + }, + "cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "requires": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + } + }, + "clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "dev": true + }, + "clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "requires": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "dependencies": { + "is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "requires": { + "isobject": "^3.0.1" + } + } + } + }, + "color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "requires": { + "color-name": "~1.1.4" + } + }, + "color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==" + }, + "colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true, + "peer": true + }, + "compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "peer": true, + "requires": { + "mime-db": ">= 1.43.0 < 2" + } + }, + "compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "peer": true, + "requires": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "peer": true + } + } + }, + "concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "requires": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "requires": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "requires": { + "ee-first": "1.1.1" + } + }, + "statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==" + } + } + }, + "connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "peer": true + }, + "console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==" + }, + "content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "peer": true, + "requires": { + "safe-buffer": "5.2.1" + } + }, + "content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "peer": true + }, + "convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "peer": true + }, + "cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true, + "peer": true + }, + "copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "peer": true, + "requires": { + "is-what": "^3.14.1" + } + }, + "copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "peer": true, + "requires": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "dependencies": { + "glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "peer": true, + "requires": { + "is-glob": "^4.0.3" + } + } + } + }, + "core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "peer": true, + "requires": { + "browserslist": "^4.24.3" + } + }, + "core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "peer": true + }, + "cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "requires": { + "object-assign": "^4", + "vary": "^1" + } + }, + "cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "peer": true, + "requires": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "dependencies": { + "isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "requires": { + "isexe": "^2.0.0" + } + } + } + }, + "css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "peer": true, + "requires": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + } + }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true + }, + "cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "peer": true + }, + "debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "requires": { + "ms": "^2.1.3" + } + }, + "default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "peer": true, + "requires": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + } + }, + "default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "peer": true + }, + "defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dev": true, + "requires": { + "clone": "^1.0.2" + } + }, + "define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "peer": true + }, + "delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" + }, + "destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==" + }, + "detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "optional": true + }, + "detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true, + "peer": true + }, + "dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==" + }, + "dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "peer": true, + "requires": { + "@leichtgewicht/ip-codec": "^2.0.1" + } + }, + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "peer": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + } + }, + "eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true + }, + "easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "requires": { + "lodash": "^4.17.10" + } + }, + "eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "requires": { + "chalk": "4.1.2" + } + }, + "ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "electron-to-chromium": { + "version": "1.5.82", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", + "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==" + }, + "emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "peer": true + }, + "encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==" + }, + "encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "requires": { + "iconv-lite": "^0.6.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "requires": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "dependencies": { + "cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==" + }, + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } + }, + "engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } + }, + "engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==" + }, + "enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "peer": true, + "requires": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true + }, + "env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true + }, + "environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true + }, + "err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "prr": "~1.0.1" + } + }, + "error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "peer": true, + "requires": { + "is-arrayish": "^0.2.1" + } + }, + "es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "peer": true + }, + "es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "peer": true + }, + "es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true, + "peer": true + }, + "es-module-shims": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.10.1.tgz", + "integrity": "sha512-HSSkRLkqFEyX6GrCAHrSOR5iz/QzQJRqZUF7bFJOZ4aoSw0WoSggfsTIGN2yFbF8v6xjQFhT4HGP6b+0qQZmEQ==" + }, + "es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "peer": true, + "requires": { + "es-errors": "^1.3.0" + } + }, + "esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "peer": true, + "requires": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "esbuild-wasm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.0.tgz", + "integrity": "sha512-xhNn5tL1AhkPg4ft59yXT6FkwKXiPSYyz1IeinJHUJpjvOHOIPvdmFQc0pGdjxlKSbzZc2mNmtVOWAR1EF/JAg==", + "dev": true, + "peer": true + }, + "escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==" + }, + "escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "peer": true, + "requires": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + } + }, + "esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "peer": true, + "requires": { + "estraverse": "^5.2.0" + }, + "dependencies": { + "estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "peer": true + } + } + }, + "estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "peer": true + }, + "esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "peer": true + }, + "etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==" + }, + "eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "peer": true + }, + "exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "peer": true, + "requires": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "peer": true + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "peer": true, + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true + } + } + }, + "external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "requires": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + } + }, + "fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "peer": true, + "requires": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + } + }, + "fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "peer": true + }, + "fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true + }, + "fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "requires": { + "reusify": "^1.0.4" + } + }, + "faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "peer": true, + "requires": { + "websocket-driver": ">=0.5.1" + } + }, + "fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "requires": { + "to-regex-range": "^5.0.1" + } + }, + "finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "peer": true, + "requires": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "peer": true, + "requires": { + "ms": "2.0.0" + } + }, + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "peer": true + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "peer": true + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "peer": true + } + } + }, + "find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "peer": true, + "requires": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + } + }, + "find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "peer": true, + "requires": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + } + }, + "flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true + }, + "follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==" + }, + "foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dev": true, + "requires": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + } + }, + "forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "peer": true + }, + "fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "peer": true + }, + "fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==" + }, + "fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "optional": true + }, + "function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true + }, + "gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "requires": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "dependencies": { + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + } + } + }, + "gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==" + }, + "get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + }, + "get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true + }, + "get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "peer": true, + "requires": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + } + }, + "get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "peer": true, + "requires": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + } + }, + "glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "requires": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + } + }, + "glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "requires": { + "is-glob": "^4.0.1" + } + }, + "glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==" + }, + "globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "peer": true, + "requires": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + } + }, + "gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "peer": true + }, + "graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "guacamole-frontend-ext-lib": { + "version": "file:../../guacamole/src/main/guacamole-frontend/dist/guacamole-frontend-ext-lib", + "requires": { + "tslib": "^2.3.0" + } + }, + "handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true, + "peer": true + }, + "has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" + }, + "has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "peer": true + }, + "has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==" + }, + "hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "requires": { + "function-bind": "^1.1.2" + } + }, + "hosted-git-info": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", + "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "dev": true, + "requires": { + "lru-cache": "^10.0.1" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, + "hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "peer": true, + "requires": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + }, + "dependencies": { + "readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "peer": true, + "requires": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "peer": true + }, + "string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "peer": true, + "requires": { + "safe-buffer": "~5.1.0" + } + } + } + }, + "html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "peer": true + }, + "htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true, + "peer": true + }, + "http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "dev": true, + "peer": true + }, + "http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "requires": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "dependencies": { + "eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + } + } + }, + "http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "requires": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + } + }, + "http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "peer": true, + "requires": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + } + }, + "https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "requires": { + "agent-base": "^7.0.2", + "debug": "4" + } + }, + "hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "peer": true + }, + "iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "requires": { + "safer-buffer": ">= 2.1.2 < 3" + } + }, + "icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "peer": true, + "requires": {} + }, + "ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true + }, + "ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "peer": true + }, + "ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "requires": { + "minimatch": "^9.0.0" + } + }, + "image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "peer": true + }, + "immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true + }, + "import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "peer": true, + "requires": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + } + }, + "imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true + }, + "inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true + }, + "ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "requires": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + } + }, + "ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "peer": true + }, + "is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "peer": true + }, + "is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "requires": { + "binary-extensions": "^2.0.0" + } + }, + "is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "requires": { + "hasown": "^2.0.2" + } + }, + "is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "peer": true + }, + "is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==" + }, + "is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" + }, + "is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "requires": { + "is-extglob": "^2.1.1" + } + }, + "is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "peer": true, + "requires": { + "is-docker": "^3.0.0" + } + }, + "is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true + }, + "is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "peer": true + }, + "is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" + }, + "is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "requires": { + "lodash.isfinite": "^3.3.2" + } + }, + "is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "peer": true + }, + "is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "peer": true + }, + "is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true + }, + "is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true, + "peer": true + }, + "is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "peer": true, + "requires": { + "is-inside-container": "^1.0.0" + } + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "peer": true + }, + "isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true + }, + "isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true + }, + "istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true + }, + "istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "requires": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + } + }, + "jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "requires": { + "@isaacs/cliui": "^8.0.2", + "@pkgjs/parseargs": "^0.11.0" + } + }, + "jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "peer": true, + "requires": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "dependencies": { + "supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "peer": true, + "requires": { + "has-flag": "^4.0.0" + } + } + } + }, + "jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "peer": true + }, + "js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "peer": true, + "requires": { + "argparse": "^2.0.1" + } + }, + "jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==" + }, + "json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true + }, + "json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" + }, + "jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true + }, + "karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "peer": true, + "requires": { + "source-map-support": "^0.5.5" + } + }, + "kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true + }, + "launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "peer": true, + "requires": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "peer": true, + "requires": { + "copy-anything": "^2.0.1", + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "parse-node-version": "^1.0.1", + "source-map": "~0.6.0", + "tslib": "^2.3.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "peer": true, + "requires": {} + }, + "license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "peer": true, + "requires": { + "webpack-sources": "^3.0.0" + } + }, + "limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "peer": true + }, + "listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "requires": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, + "lmdb": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.2.tgz", + "integrity": "sha512-LriG93la4PbmPMwI7Hbv8W+0ncLK7549w4sbZSi4QGDjnnxnmNMgxUkaQTEMzH8TpwsfFvgEjpLX7V8B/I9e3g==", + "dev": true, + "optional": true, + "requires": { + "@lmdb/lmdb-darwin-arm64": "3.2.2", + "@lmdb/lmdb-darwin-x64": "3.2.2", + "@lmdb/lmdb-linux-arm": "3.2.2", + "@lmdb/lmdb-linux-arm64": "3.2.2", + "@lmdb/lmdb-linux-x64": "3.2.2", + "@lmdb/lmdb-win32-x64": "3.2.2", + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + } + }, + "loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "peer": true + }, + "loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "peer": true + }, + "locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "peer": true, + "requires": { + "p-locate": "^6.0.0" + } + }, + "lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true, + "peer": true + }, + "lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==" + }, + "log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "requires": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + } + }, + "log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "requires": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "dependencies": { + "ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "requires": { + "environment": "^1.0.0" + } + }, + "ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true + }, + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "requires": { + "restore-cursor": "^5.0.0" + } + }, + "emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "requires": { + "get-east-asian-width": "^1.0.0" + } + }, + "onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "requires": { + "mimic-function": "^5.0.0" + } + }, + "restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "requires": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + } + }, + "slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + } + }, + "string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "requires": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + } + }, + "strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "requires": { + "ansi-regex": "^6.0.1" + } + }, + "wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "requires": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + } + } + } + }, + "lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "requires": { + "yallist": "^3.0.2" + } + }, + "magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "peer": true + } + } + }, + "make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "requires": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + } + }, + "math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "peer": true + }, + "media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "peer": true + }, + "memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "peer": true, + "requires": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + } + }, + "merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "peer": true + }, + "merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "peer": true + }, + "merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true + }, + "methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "peer": true + }, + "micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "requires": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + } + }, + "mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==" + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==" + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "requires": { + "mime-db": "1.52.0" + } + }, + "mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true + }, + "mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true + }, + "mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "peer": true, + "requires": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + } + }, + "minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true, + "peer": true + }, + "minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "requires": { + "brace-expansion": "^2.0.1" + } + }, + "minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true + }, + "minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "minipass-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.0.tgz", + "integrity": "sha512-2v6aXUXwLP1Epd/gc32HAMIWoczx+fZwEPRHm/VwtrJzRGwR1qGZXEYV3Zp8ZjjbwaZhMrM6uHV4KVkk+XCc2w==", + "dev": true, + "requires": { + "encoding": "^0.1.13", + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + } + }, + "minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "requires": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + } + }, + "mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==" + }, + "mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true + }, + "mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true + }, + "ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "optional": true, + "requires": { + "msgpackr-extract": "^3.0.2" + } + }, + "msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3", + "node-gyp-build-optional-packages": "5.2.2" + } + }, + "multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "peer": true, + "requires": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + } + }, + "mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true + }, + "nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true + }, + "needle": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.2.0.tgz", + "integrity": "sha512-oUvzXnyLiVyVGoianLijF9O/RecZUf7TkBfimjGrLM4eQhXyeJwM6GeAWccwfQ9aa4gMCZKqhAOuLaMIcQxajQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "debug": "^3.2.6", + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "dependencies": { + "debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "ms": "^2.1.1" + } + }, + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true + }, + "neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "peer": true + }, + "ngx-build-plus": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-19.0.0.tgz", + "integrity": "sha512-JXzRvDZH1gQ2xGiePduHoMeR9kaK1cHHhT6d6npVBq8aWGFHs1iAg/5/gkieCsz2QfrcaoSF8ZBb9ZhcGX4Z2g==", + "dev": true, + "requires": { + "webpack-merge": "^5.0.0" + } + }, + "node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "optional": true + }, + "node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "peer": true + }, + "node-gyp": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.0.0.tgz", + "integrity": "sha512-zQS+9MTTeCMgY0F3cWPyJyRFAkVltQ1uXm+xXu/ES6KFgC6Czo1Seb9vQW2wNxSX2OrDTiqL0ojtkFxBQ0ypIw==", + "dev": true, + "requires": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "dependencies": { + "chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true + }, + "mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true + }, + "tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "requires": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + } + }, + "yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true + } + } + }, + "node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "optional": true, + "requires": { + "detect-libc": "^2.0.1" + } + }, + "node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "nopt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.0.0.tgz", + "integrity": "sha512-1L/fTJ4UmV/lUxT2Uf006pfZKTvAgCF+chz+0OgBHO8u2Z67pE7AaAUUj7CJy0lXqHmymUvGFt6NE9R3HER0yw==", + "dev": true, + "requires": { + "abbrev": "^2.0.0" + } + }, + "normalize-package-data": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.0.tgz", + "integrity": "sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==", + "dev": true, + "requires": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + } + }, + "normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" + }, + "normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "peer": true + }, + "npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "requires": { + "npm-normalize-package-bin": "^4.0.0" + } + }, + "npm-install-checks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "dev": true, + "requires": { + "semver": "^7.1.1" + } + }, + "npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true + }, + "npm-package-arg": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", + "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", + "dev": true, + "requires": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + } + }, + "npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "requires": { + "ignore-walk": "^7.0.0" + } + }, + "npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "requires": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + } + }, + "npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "requires": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + } + }, + "npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "requires": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + } + }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "requires": { + "boolbase": "^1.0.0" + } + }, + "object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==" + }, + "object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "peer": true + }, + "obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true, + "peer": true + }, + "on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "requires": { + "ee-first": "1.1.1" + } + }, + "on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "peer": true + }, + "onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "requires": { + "mimic-fn": "^2.1.0" + } + }, + "open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "peer": true, + "requires": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + } + }, + "opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "requires": { + "is-wsl": "^1.1.0" + }, + "dependencies": { + "is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==" + } + } + }, + "ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, + "requires": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + } + }, + "ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "optional": true + }, + "os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true + }, + "p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "peer": true, + "requires": { + "yocto-queue": "^1.0.0" + } + }, + "p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "peer": true, + "requires": { + "p-limit": "^4.0.0" + } + }, + "p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true + }, + "p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "peer": true, + "requires": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "dependencies": { + "retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "peer": true + } + } + }, + "package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true + }, + "pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "requires": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + } + }, + "parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "peer": true, + "requires": { + "callsites": "^3.0.0" + } + }, + "parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "peer": true, + "requires": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "dependencies": { + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + } + } + }, + "parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "peer": true + }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dev": true, + "requires": { + "entities": "^4.4.0" + } + }, + "parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "requires": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + } + }, + "parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "requires": { + "parse5": "^7.0.0" + } + }, + "parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" + }, + "path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "peer": true + }, + "path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true + }, + "path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "requires": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "dependencies": { + "lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + } + } + }, + "path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "peer": true + }, + "path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "peer": true + }, + "picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==" + }, + "pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "peer": true + }, + "piscina": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.7.0.tgz", + "integrity": "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==", + "dev": true, + "peer": true, + "requires": { + "@napi-rs/nice": "^1.0.1" + } + }, + "pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "peer": true, + "requires": { + "find-up": "^6.3.0" + } + }, + "portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "requires": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + } + }, + "postcss": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", + "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "dev": true, + "requires": { + "nanoid": "^3.3.8", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + } + }, + "postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "peer": true, + "requires": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + } + }, + "postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "peer": true, + "requires": {} + }, + "postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "peer": true, + "requires": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + } + }, + "postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "peer": true, + "requires": { + "postcss-selector-parser": "^7.0.0" + } + }, + "postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "peer": true, + "requires": { + "icss-utils": "^5.0.0" + } + }, + "postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "peer": true, + "requires": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + } + }, + "postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "peer": true + }, + "proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true + }, + "process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==" + }, + "process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "peer": true + }, + "promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "requires": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + } + }, + "proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "peer": true, + "requires": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "dependencies": { + "ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "peer": true + } + } + }, + "prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true, + "peer": true + }, + "punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "peer": true + }, + "qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "peer": true, + "requires": { + "side-channel": "^1.0.6" + } + }, + "queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true + }, + "randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "peer": true, + "requires": { + "safe-buffer": "^5.1.0" + } + }, + "range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + }, + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "requires": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "dependencies": { + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "requires": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + } + }, + "readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true + }, + "reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true, + "peer": true + }, + "regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "peer": true, + "requires": { + "regenerate": "^1.4.2" + } + }, + "regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true, + "peer": true + }, + "regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "peer": true, + "requires": { + "@babel/runtime": "^7.8.4" + } + }, + "regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true, + "peer": true + }, + "regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "peer": true, + "requires": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + } + }, + "regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true, + "peer": true + }, + "regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "peer": true, + "requires": { + "jsesc": "~3.0.2" + }, + "dependencies": { + "jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "peer": true + } + } + }, + "require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==" + }, + "require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true + }, + "requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "requires": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + } + }, + "resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "peer": true + }, + "resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "peer": true, + "requires": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "peer": true, + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + }, + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true + } + } + }, + "resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "requires": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "dependencies": { + "brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "requires": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "requires": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "dependencies": { + "signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true + } + } + }, + "retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true + }, + "reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true + }, + "rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "requires": { + "glob": "^10.3.7" + } + }, + "rollup": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", + "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", + "dev": true, + "requires": { + "@rollup/rollup-android-arm-eabi": "4.30.1", + "@rollup/rollup-android-arm64": "4.30.1", + "@rollup/rollup-darwin-arm64": "4.30.1", + "@rollup/rollup-darwin-x64": "4.30.1", + "@rollup/rollup-freebsd-arm64": "4.30.1", + "@rollup/rollup-freebsd-x64": "4.30.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", + "@rollup/rollup-linux-arm-musleabihf": "4.30.1", + "@rollup/rollup-linux-arm64-gnu": "4.30.1", + "@rollup/rollup-linux-arm64-musl": "4.30.1", + "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", + "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", + "@rollup/rollup-linux-riscv64-gnu": "4.30.1", + "@rollup/rollup-linux-s390x-gnu": "4.30.1", + "@rollup/rollup-linux-x64-gnu": "4.30.1", + "@rollup/rollup-linux-x64-musl": "4.30.1", + "@rollup/rollup-win32-arm64-msvc": "4.30.1", + "@rollup/rollup-win32-ia32-msvc": "4.30.1", + "@rollup/rollup-win32-x64-msvc": "4.30.1", + "@types/estree": "1.0.6", + "fsevents": "~2.3.2" + } + }, + "run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "peer": true + }, + "run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "requires": { + "queue-microtask": "^1.2.2" + } + }, + "rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==" + }, + "rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "requires": { + "tslib": "^2.1.0" + } + }, + "safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + }, + "safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "sass": { + "version": "1.80.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.7.tgz", + "integrity": "sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==", + "dev": true, + "peer": true, + "requires": { + "@parcel/watcher": "^2.4.1", + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + } + }, + "sass-loader": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.3.tgz", + "integrity": "sha512-gosNorT1RCkuCMyihv6FBRR7BMV06oKRAs+l4UMp1mlcVg9rWN6KMmUj3igjQwmYys4mDP3etEYJgiHRbgHCHA==", + "dev": true, + "peer": true, + "requires": { + "neo-async": "^2.6.2" + } + }, + "sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "dev": true, + "optional": true, + "peer": true + }, + "schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "peer": true, + "requires": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "dependencies": { + "ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "peer": true, + "requires": { + "ajv": "^8.0.0" + } + } + } + }, + "select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true, + "peer": true + }, + "selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "peer": true, + "requires": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + } + }, + "semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true + }, + "send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "requires": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + }, + "dependencies": { + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + } + } + }, + "http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "requires": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + } + }, + "statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" + } + } + }, + "serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "peer": true, + "requires": { + "randombytes": "^2.1.0" + } + }, + "serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "requires": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "dependencies": { + "debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "requires": { + "ms": "2.0.0" + } + }, + "depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==" + }, + "http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "requires": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + } + }, + "inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + } + } + }, + "serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "requires": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "dependencies": { + "encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==" + } + } + }, + "server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==" + }, + "set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" + }, + "setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "requires": { + "kind-of": "^6.0.2" + } + }, + "shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "requires": { + "shebang-regex": "^3.0.0" + } + }, + "shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true + }, + "shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "peer": true + }, + "side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "peer": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + } + }, + "side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "peer": true, + "requires": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + } + }, + "side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "peer": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + } + }, + "side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "peer": true, + "requires": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + } + }, + "signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true + }, + "sigstore": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.0.0.tgz", + "integrity": "sha512-PHMifhh3EN4loMcHCz6l3v/luzgT3za+9f8subGgeMNjbJjzH4Ij/YoX3Gvu+kaouJRIlVdTHHCREADYf+ZteA==", + "dev": true, + "requires": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" + } + }, + "slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "peer": true + }, + "slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "requires": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "dependencies": { + "ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true + }, + "is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true + } + } + }, + "smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true + }, + "socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "requires": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "requires": { + "debug": "~4.3.4", + "ws": "~8.17.1" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + }, + "ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "requires": {} + } + } + }, + "socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "requires": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "dependencies": { + "debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "requires": { + "ms": "^2.1.3" + } + } + } + }, + "sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "peer": true, + "requires": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "requires": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + } + }, + "socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "requires": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + } + }, + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + }, + "source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true + }, + "source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "peer": true, + "requires": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "dependencies": { + "iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "peer": true, + "requires": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + } + } + } + }, + "source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "peer": true, + "requires": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + }, + "dependencies": { + "source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "peer": true + } + } + }, + "spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "requires": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "requires": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true + }, + "spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "peer": true, + "requires": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + } + }, + "spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "peer": true, + "requires": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "requires": { + "minipass": "^7.0.3" + } + }, + "statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==" + }, + "stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "requires": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + } + }, + "string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "requires": { + "safe-buffer": "~5.2.0" + } + }, + "string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "string-width-cjs": { + "version": "npm:string-width@4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "requires": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + } + }, + "strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "strip-ansi-cjs": { + "version": "npm:strip-ansi@6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "requires": { + "ansi-regex": "^5.0.1" + } + }, + "supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "requires": { + "has-flag": "^4.0.0" + } + }, + "supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true + }, + "symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true + }, + "tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "peer": true + }, + "tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "requires": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "dependencies": { + "fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "requires": { + "minipass": "^3.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true + }, + "minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "requires": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "dependencies": { + "minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "requires": { + "yallist": "^4.0.0" + } + } + } + }, + "yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + } + } + }, + "terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + } + }, + "terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "dev": true, + "peer": true, + "requires": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + } + }, + "thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "peer": true, + "requires": {} + }, + "thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true, + "peer": true + }, + "tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "requires": { + "os-tmpdir": "~1.0.2" + } + }, + "to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "requires": { + "is-number": "^7.0.0" + } + }, + "toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==" + }, + "tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "peer": true, + "requires": {} + }, + "tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "peer": true + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "tuf-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "dev": true, + "requires": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + } + }, + "type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true + }, + "type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "peer": true, + "requires": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + } + }, + "typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true, + "peer": true + }, + "typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "dev": true + }, + "ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==" + }, + "undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "peer": true + }, + "unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "peer": true, + "requires": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + } + }, + "unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "peer": true + }, + "unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "peer": true + }, + "unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "peer": true + }, + "unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "requires": { + "unique-slug": "^5.0.0" + } + }, + "unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "requires": { + "imurmurhash": "^0.1.4" + } + }, + "unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==" + }, + "update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "peer": true, + "requires": { + "punycode": "^2.1.0" + } + }, + "util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==" + }, + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "peer": true + }, + "validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "requires": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "validate-npm-package-name": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "dev": true + }, + "vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==" + }, + "vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "peer": true, + "requires": { + "esbuild": "^0.21.3", + "fsevents": "~2.3.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "dependencies": { + "@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "dev": true, + "optional": true, + "peer": true + }, + "@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "dev": true, + "optional": true, + "peer": true + }, + "esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "peer": true, + "requires": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + } + } + }, + "watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "requires": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + } + }, + "wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "peer": true, + "requires": { + "minimalistic-assert": "^1.0.0" + } + }, + "wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dev": true, + "requires": { + "defaults": "^1.0.3" + } + }, + "weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "optional": true + }, + "webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "peer": true, + "requires": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "dependencies": { + "ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "peer": true, + "requires": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + } + }, + "ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peer": true, + "requires": {} + }, + "json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "peer": true + }, + "json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "peer": true + }, + "schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "peer": true, + "requires": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + } + } + } + }, + "webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "peer": true, + "requires": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + } + }, + "webpack-dev-server": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", + "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "dev": true, + "peer": true, + "requires": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.19.2", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "dependencies": { + "chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "peer": true, + "requires": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "fsevents": "~2.3.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + } + }, + "http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "peer": true, + "requires": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + } + }, + "readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "peer": true, + "requires": { + "picomatch": "^2.2.1" + } + } + } + }, + "webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "requires": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + } + }, + "webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "peer": true + }, + "webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "peer": true, + "requires": { + "typed-assert": "^1.0.8" + } + }, + "websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "peer": true, + "requires": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + } + }, + "websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "peer": true + }, + "which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "requires": { + "isexe": "^3.1.1" + } + }, + "wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "requires": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "wrap-ansi-cjs": { + "version": "npm:wrap-ansi@7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } + }, + "ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "peer": true, + "requires": {} + }, + "xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==" + }, + "y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" + }, + "yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "requires": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + } + }, + "yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==" + }, + "yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "peer": true + }, + "yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true + }, + "zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==" + } + } +} diff --git a/doc/guacamole-frontend-extension-example/package.json b/doc/guacamole-frontend-extension-example/package.json new file mode 100644 index 0000000000..0cb5eed7d6 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/package.json @@ -0,0 +1,36 @@ +{ + "name": "guacamole-frontend-extension-example", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "watch": "ng build --watch --configuration development", + "run:all": "node node_modules/@angular-architects/module-federation/src/server/mf-dev-server.js" + }, + "private": true, + "dependencies": { + "@angular-architects/native-federation": "^19.0.5", + "@angular/animations": "19.0.6", + "@angular/common": "19.0.6", + "@angular/compiler": "19.0.6", + "@angular/core": "19.0.6", + "@angular/forms": "19.0.6", + "@angular/platform-browser": "19.0.6", + "@angular/platform-browser-dynamic": "19.0.6", + "@angular/router": "19.0.6", + "@softarc/native-federation-node": "^2.0.10", + "es-module-shims": "^1.5.12", + "guacamole-frontend-ext-lib": "file:../../guacamole/src/main/guacamole-frontend/dist/guacamole-frontend-ext-lib", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular/build": "^19.1.1", + "@angular/cli": "~19.0.7", + "@angular/compiler-cli": "19.0.6", + "ngx-build-plus": "^19.0.0", + "typescript": "~5.6.3" + } +} diff --git a/doc/guacamole-frontend-extension-example/src/app/app.component.ts b/doc/guacamole-frontend-extension-example/src/app/app.component.ts new file mode 100644 index 0000000000..3785ac38ce --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/app.component.ts @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterOutlet } from '@angular/router'; + +@Component({ + selector: 'guac-root', + imports: [CommonModule, RouterOutlet], + template: ` + + ` +}) +export class AppComponent { + title = 'guacamole-frontend-extension-example'; +} diff --git a/guacamole/src/main/frontend/src/app/clipboard/clipboardModule.js b/doc/guacamole-frontend-extension-example/src/app/app.config.ts similarity index 78% rename from guacamole/src/main/frontend/src/app/clipboard/clipboardModule.js rename to doc/guacamole-frontend-extension-example/src/app/app.config.ts index b7528a4d33..7eb7be507f 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/clipboardModule.js +++ b/doc/guacamole-frontend-extension-example/src/app/app.config.ts @@ -17,7 +17,11 @@ * under the License. */ -/** - * The module for code used to manipulate/observe the clipboard. - */ -angular.module('clipboard', []); +import { ApplicationConfig } from '@angular/core'; +import { provideRouter } from '@angular/router'; + +import { routes } from './app.routes'; + +export const appConfig: ApplicationConfig = { + providers: [provideRouter(routes) ] +}; diff --git a/doc/guacamole-frontend-extension-example/src/app/app.routes.ts b/doc/guacamole-frontend-extension-example/src/app/app.routes.ts new file mode 100644 index 0000000000..8d94036c3e --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/app.routes.ts @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Routes } from '@angular/router'; +import { HomePageComponent } from "./components/home-page.component"; + +export const routes: Routes = [ + { + path: '', + component: HomePageComponent, + }, + + { + path: 'about', + loadComponent: () => import('./components/about-extension-page.component') + .then(m => m.AboutExtensionPageComponent), + title: 'About The Extension' + }, + + // fallback route when the url is not found + {path: '**', redirectTo: '', pathMatch: 'full'} +]; diff --git a/guacamole/src/main/frontend/src/app/form/controllers/redirectFieldController.js b/doc/guacamole-frontend-extension-example/src/app/components/about-extension-button.component.ts similarity index 68% rename from guacamole/src/main/frontend/src/app/form/controllers/redirectFieldController.js rename to doc/guacamole-frontend-extension-example/src/app/components/about-extension-button.component.ts index 3ec9326c79..b7b48ce257 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/redirectFieldController.js +++ b/doc/guacamole-frontend-extension-example/src/app/components/about-extension-button.component.ts @@ -17,16 +17,19 @@ * under the License. */ +import { Component, ViewEncapsulation } from '@angular/core'; + /** - * Controller for the redirect field, which redirects the user to the provided - * URL. + * Displays a button to navigate to the about page. */ -angular.module('form').controller('redirectFieldController', ['$scope','$window', - function redirectFieldController($scope,$window) { - - /** - * Redirect the user to the provided URL. - */ - $window.location.href = $scope.field.redirectUrl; +@Component({ + selector: 'guac-about-extension-button', + template: ` + Goto Extension Page + `, + imports: [], + encapsulation: ViewEncapsulation.None +}) +export class AboutExtensionButtonComponent { -}]); +} diff --git a/doc/guacamole-frontend-extension-example/src/app/components/about-extension-page.component.ts b/doc/guacamole-frontend-extension-example/src/app/components/about-extension-page.component.ts new file mode 100644 index 0000000000..059b8d703f --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/components/about-extension-page.component.ts @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { RouterLink } from "@angular/router"; + +/** + * Displays the about-extension page. + */ +@Component({ + selector: 'guac-about-extension-page', + imports: [CommonModule, RouterLink], + template: ` +

+ This is the about extension page. + + Go back to the extension page +

+ `, + encapsulation: ViewEncapsulation.None +}) +export class AboutExtensionPageComponent { + +} diff --git a/doc/guacamole-frontend-extension-example/src/app/components/home-page.component.ts b/doc/guacamole-frontend-extension-example/src/app/components/home-page.component.ts new file mode 100644 index 0000000000..780825cb57 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/components/home-page.component.ts @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { AboutExtensionButtonComponent } from "./about-extension-button.component"; +import { FormControl } from "@angular/forms"; +import { + FormField +} from "guacamole-frontend-ext-lib/lib/form/FormField"; +import { RouterLink } from "@angular/router"; +import { PlanetChooserFieldComponent } from "./planet-chooser-field.component"; + +/** + * The home page of the extension. + */ +@Component({ + selector: 'guac-home-page', + imports: [CommonModule, AboutExtensionButtonComponent, PlanetChooserFieldComponent, RouterLink, PlanetChooserFieldComponent], + template: ` +

+ guacamole-frontend-extension-example works! +

+

Planet Chooser Field

+

+ + Your selected planet is: {{control.value}} +

+ Home + + `, + encapsulation: ViewEncapsulation.None +}) +export class HomePageComponent { + control = new FormControl(''); + field = {} as FormField; +} diff --git a/doc/guacamole-frontend-extension-example/src/app/components/planet-chooser-field.component.ts b/doc/guacamole-frontend-extension-example/src/app/components/planet-chooser-field.component.ts new file mode 100644 index 0000000000..88d6c5a23c --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/components/planet-chooser-field.component.ts @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { + FormField, + FormFieldComponentData +} from "guacamole-frontend-ext-lib"; +import { FormControl, ReactiveFormsModule } from "@angular/forms"; + +export const PLANET_CHOOSER_FIELD_TYPE = 'planet-chooser'; + +@Component({ + selector: 'app-planet-chooser-field', + imports: [CommonModule, ReactiveFormsModule], + template: ` + + + + `, + encapsulation: ViewEncapsulation.None +}) +export class PlanetChooserFieldComponent implements FormFieldComponentData { + + @Input({required: true}) disabled!: boolean; + @Input({required: true}) field!: FormField; + @Input({required: true}) focused!: boolean; + @Input({required: true}) namespace!: string | undefined; + @Input() control?: FormControl; + + readonly planets = [ + 'Mercury', + 'Venus', + 'Earth', + 'Mars', + 'Jupiter', + 'Saturn', + 'Uranus', + 'Neptune' + ]; + +} diff --git a/doc/guacamole-frontend-extension-example/src/app/extension.config.ts b/doc/guacamole-frontend-extension-example/src/app/extension.config.ts new file mode 100644 index 0000000000..1892d5b4b5 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/app/extension.config.ts @@ -0,0 +1,48 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + BootsrapExtensionFunction, + FieldTypeService +} from "guacamole-frontend-ext-lib"; +import { inject, Injector, runInInjectionContext } from "@angular/core"; +import { Routes } from "@angular/router"; + +import { routes } from "./app.routes"; +import { PLANET_CHOOSER_FIELD_TYPE, PlanetChooserFieldComponent } from "./components/planet-chooser-field.component"; + + +// noinspection JSUnusedGlobalSymbols +/** + * Called by the frontend to initialize the extension. + */ +export const bootsrapExtension: BootsrapExtensionFunction = (injector: Injector): Routes => { + + runInInjectionContext(injector, () => { + + const fieldTypeService = inject(FieldTypeService); + + // Register a new field type + fieldTypeService.registerFieldType(PLANET_CHOOSER_FIELD_TYPE, PlanetChooserFieldComponent); + + }); + + // Return the routes of the extension which will be added to the frontend routes + return routes; +} \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/src/bootstrap.ts b/doc/guacamole-frontend-extension-example/src/bootstrap.ts new file mode 100644 index 0000000000..980d52f478 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/bootstrap.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { bootstrapApplication } from '@angular/platform-browser'; +import { appConfig } from './app/app.config'; +import { AppComponent } from './app/app.component'; + +bootstrapApplication(AppComponent, appConfig) + .catch((err) => console.error(err)); diff --git a/doc/guacamole-frontend-extension-example/src/html/button.html b/doc/guacamole-frontend-extension-example/src/html/button.html new file mode 100644 index 0000000000..52ab750eb4 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/html/button.html @@ -0,0 +1,3 @@ + + +
diff --git a/doc/guacamole-frontend-extension-example/src/index.html b/doc/guacamole-frontend-extension-example/src/index.html new file mode 100644 index 0000000000..938d4d54da --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/index.html @@ -0,0 +1,30 @@ + + + + + + GuacamoleFrontendExtensionExample + + + + + + + diff --git a/doc/guacamole-frontend-extension-example/src/main.ts b/doc/guacamole-frontend-extension-example/src/main.ts new file mode 100644 index 0000000000..ed18d47821 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/src/main.ts @@ -0,0 +1,6 @@ +import { initFederation } from '@angular-architects/native-federation'; + +initFederation() + .catch(err => console.error(err)) + .then(_ => import('./bootstrap')) + .catch(err => console.error(err)); diff --git a/guacamole/src/main/frontend/src/app/form/formModule.js b/doc/guacamole-frontend-extension-example/src/styles.css similarity index 87% rename from guacamole/src/main/frontend/src/app/form/formModule.js rename to doc/guacamole-frontend-extension-example/src/styles.css index 1135118424..322f15da09 100644 --- a/guacamole/src/main/frontend/src/app/form/formModule.js +++ b/doc/guacamole-frontend-extension-example/src/styles.css @@ -17,10 +17,6 @@ * under the License. */ -/** - * Module for displaying dynamic forms. - */ -angular.module('form', [ - 'locale', - 'rest' -]); +body > guac-root > div > guac-login > div > div > div > form > div.version > div.app-name { + color: crimson; +} \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/tsconfig.app.json b/doc/guacamole-frontend-extension-example/tsconfig.app.json new file mode 100644 index 0000000000..fc2818ef54 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/tsconfig.app.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/app/extension.config.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/tsconfig.federation.json b/doc/guacamole-frontend-extension-example/tsconfig.federation.json new file mode 100644 index 0000000000..fc2818ef54 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/tsconfig.federation.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/app/extension.config.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/doc/guacamole-frontend-extension-example/tsconfig.json b/doc/guacamole-frontend-extension-example/tsconfig.json new file mode 100644 index 0000000000..a35eafb847 --- /dev/null +++ b/doc/guacamole-frontend-extension-example/tsconfig.json @@ -0,0 +1,32 @@ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "esModuleInterop": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} \ No newline at end of file diff --git a/guacamole/.ratignore b/guacamole/.ratignore index 548fcb76c0..2fe5e3e78b 100644 --- a/guacamole/.ratignore +++ b/guacamole/.ratignore @@ -1 +1,4 @@ src/main/frontend/dist/**/* +**/.idea/**/* +**/.editorconfig +**/guacamole-common-js/all.min.js \ No newline at end of file diff --git a/guacamole/pom.xml b/guacamole/pom.xml index b0743b0fd6..71e7e9a500 100644 --- a/guacamole/pom.xml +++ b/guacamole/pom.xml @@ -1,22 +1,22 @@ + ~ Licensed to the Apache Software Foundation (ASF) under one + ~ or more contributor license agreements. See the NOTICE file + ~ distributed with this work for additional information + ~ regarding copyright ownership. The ASF licenses this file + ~ to you 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. + --> - - --openssl-legacy-provider - - + + true + install-node-and-npm install-node-and-npm - v18.18.0 - 9.8.1 + v22.13.0 + + - npm-ci + npm-ci-lib npm @@ -96,46 +123,97 @@ ci + + - npm-build + npm-build-lib generate-resources npm - run build + run build guacamole-frontend-lib - - - - - org.apache.maven.plugins - maven-resources-plugin - + + + npm-test-lib + test + + npm + + + run test-guacamole-frontend-lib:ci + + + + + + npm-build-ext-lib + generate-resources + + npm + + + run build guacamole-frontend-ext-lib + + + + - copy-npm-dependency-list + npm-build-app generate-resources - copy-resources + npm - ${dependency.list.directory} - - - src/main/frontend/dist - - npm-dependencies.txt - - - + run build:app + + + + npm-test-app + test + + npm + + + run test-guacamole-frontend:ci + + + + + + + + + + + + + + + + + + + + + + + + + + + + + org.codehaus.mojo @@ -158,22 +236,20 @@ - src/main/frontend/dist + src/main/guacamole-frontend/dist/guacamole-frontend translations/*.json index.html - verifyCachedVersion.js - src/main/frontend/dist + src/main/guacamole-frontend/dist/guacamole-frontend true translations/*.json index.html - verifyCachedVersion.js @@ -185,15 +261,13 @@ - - - - org.apache.guacamole - guacamole-common-js - zip - - - + + + ${*} + + #{*} + + @@ -323,7 +397,7 @@ com.google.inject.extensions guice-servlet - + org.glassfish.jersey.containers @@ -359,7 +433,7 @@ jsr250-api 1.0 - + com.google.guava diff --git a/guacamole/src/main/frontend/.gitignore b/guacamole/src/main/frontend/.gitignore deleted file mode 100644 index 70f460b2e3..0000000000 --- a/guacamole/src/main/frontend/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -*~ -node_modules -dist diff --git a/guacamole/src/main/frontend/package-lock.json b/guacamole/src/main/frontend/package-lock.json deleted file mode 100644 index 7ad1e32ce7..0000000000 --- a/guacamole/src/main/frontend/package-lock.json +++ /dev/null @@ -1,10612 +0,0 @@ -{ - "name": "frontend", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "frontend", - "dependencies": { - "@simonwep/pickr": "^1.8.2", - "angular": "^1.8.3", - "angular-route": "^1.8.3", - "angular-templatecache-webpack-plugin": "^1.0.1", - "angular-translate": "^2.19.1", - "angular-translate-interpolation-messageformat": "^2.19.1", - "angular-translate-loader-static-files": "^2.19.1", - "blob-polyfill": "^9.0.20240710", - "csv": "^6.3.9", - "d3-path": "^3.1.0", - "d3-shape": "^3.2.0", - "datalist-polyfill": "^1.25.1", - "file-saver": "^2.0.5", - "fuzzysort": "^3.0.2", - "jquery": "^3.7.1", - "jstz": "^2.1.1", - "lodash": "^4.17.21", - "yaml": "^2.5.0" - }, - "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "babel-loader": "^8.3.0", - "clean-webpack-plugin": "^4.0.0", - "closure-webpack-plugin": "^2.6.1", - "copy-webpack-plugin": "^5.1.2", - "css-loader": "^5.2.7", - "css-minimizer-webpack-plugin": "^1.3.0", - "exports-loader": "^1.1.1", - "find-package-json": "^1.2.0", - "google-closure-compiler": "20240317.0.0", - "html-webpack-plugin": "^4.5.2", - "mini-css-extract-plugin": "^1.6.2", - "webpack": "^4.47.0", - "webpack-cli": "^4.10.0" - } - }, - "node_modules/@ampproject/remapping": { - "version": "2.2.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "@jridgewell/gen-mapping": "^0.1.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/highlight": "^7.24.7", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helpers": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/debug": { - "version": "4.3.4", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/core/node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/@babel/core/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", - "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/generator/node_modules/jsesc": { - "version": "2.5.2", - "dev": true, - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "browserslist": "^4.22.2", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "regexpu-core": "^5.3.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/debug": { - "version": "4.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/helper-define-polyfill-provider/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/helper-environment-visitor": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-function-name": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-hoist-variables": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-wrap-function": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-member-expression-to-functions": "^7.24.7", - "@babel/helper-optimise-call-expression": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-split-export-declaration": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-function-name": "^7.24.7", - "@babel/template": "^7.24.7", - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/template": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.24.7", - "chalk": "^2.4.2", - "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/highlight/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/@babel/highlight/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/highlight/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/highlight/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@babel/parser": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-async-generators": { - "version": "7.8.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-properties": { - "version": "7.12.13", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-class-static-block": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-export-namespace-from": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.3" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-meta": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-json-strings": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-logical-assignment-operators": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-numeric-separator": { - "version": "7.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-object-rest-spread": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-catch-binding": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-optional-chaining": { - "version": "7.8.3", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-private-property-in-object": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-top-level-await": { - "version": "7.14.5", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7", - "@babel/plugin-syntax-async-generators": "^7.8.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-remap-async-to-generator": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-class-static-block": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/template": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-json-strings": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-replace-supers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.24.7", - "@babel/helper-create-class-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "regenerator-transform": "^0.15.2" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-skip-transparent-expression-wrappers": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.24.7", - "@babel/helper-compilation-targets": "^7.24.7", - "@babel/helper-plugin-utils": "^7.24.7", - "@babel/helper-validator-option": "^7.24.7", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.7", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.7", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.7", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.7", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-import-assertions": "^7.24.7", - "@babel/plugin-syntax-import-attributes": "^7.24.7", - "@babel/plugin-syntax-import-meta": "^7.10.4", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.24.7", - "@babel/plugin-transform-async-generator-functions": "^7.24.7", - "@babel/plugin-transform-async-to-generator": "^7.24.7", - "@babel/plugin-transform-block-scoped-functions": "^7.24.7", - "@babel/plugin-transform-block-scoping": "^7.24.7", - "@babel/plugin-transform-class-properties": "^7.24.7", - "@babel/plugin-transform-class-static-block": "^7.24.7", - "@babel/plugin-transform-classes": "^7.24.7", - "@babel/plugin-transform-computed-properties": "^7.24.7", - "@babel/plugin-transform-destructuring": "^7.24.7", - "@babel/plugin-transform-dotall-regex": "^7.24.7", - "@babel/plugin-transform-duplicate-keys": "^7.24.7", - "@babel/plugin-transform-dynamic-import": "^7.24.7", - "@babel/plugin-transform-exponentiation-operator": "^7.24.7", - "@babel/plugin-transform-export-namespace-from": "^7.24.7", - "@babel/plugin-transform-for-of": "^7.24.7", - "@babel/plugin-transform-function-name": "^7.24.7", - "@babel/plugin-transform-json-strings": "^7.24.7", - "@babel/plugin-transform-literals": "^7.24.7", - "@babel/plugin-transform-logical-assignment-operators": "^7.24.7", - "@babel/plugin-transform-member-expression-literals": "^7.24.7", - "@babel/plugin-transform-modules-amd": "^7.24.7", - "@babel/plugin-transform-modules-commonjs": "^7.24.7", - "@babel/plugin-transform-modules-systemjs": "^7.24.7", - "@babel/plugin-transform-modules-umd": "^7.24.7", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.24.7", - "@babel/plugin-transform-new-target": "^7.24.7", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.7", - "@babel/plugin-transform-numeric-separator": "^7.24.7", - "@babel/plugin-transform-object-rest-spread": "^7.24.7", - "@babel/plugin-transform-object-super": "^7.24.7", - "@babel/plugin-transform-optional-catch-binding": "^7.24.7", - "@babel/plugin-transform-optional-chaining": "^7.24.7", - "@babel/plugin-transform-parameters": "^7.24.7", - "@babel/plugin-transform-private-methods": "^7.24.7", - "@babel/plugin-transform-private-property-in-object": "^7.24.7", - "@babel/plugin-transform-property-literals": "^7.24.7", - "@babel/plugin-transform-regenerator": "^7.24.7", - "@babel/plugin-transform-reserved-words": "^7.24.7", - "@babel/plugin-transform-shorthand-properties": "^7.24.7", - "@babel/plugin-transform-spread": "^7.24.7", - "@babel/plugin-transform-sticky-regex": "^7.24.7", - "@babel/plugin-transform-template-literals": "^7.24.7", - "@babel/plugin-transform-typeof-symbol": "^7.24.7", - "@babel/plugin-transform-unicode-escapes": "^7.24.7", - "@babel/plugin-transform-unicode-property-regex": "^7.24.7", - "@babel/plugin-transform-unicode-regex": "^7.24.7", - "@babel/plugin-transform-unicode-sets-regex": "^7.24.7", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.4", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.31.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/regjsgen": { - "version": "0.8.0", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/runtime": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.24.7", - "@babel/helper-environment-visitor": "^7.24.7", - "@babel/helper-function-name": "^7.24.7", - "@babel/helper-hoist-variables": "^7.24.7", - "@babel/helper-split-export-declaration": "^7.24.7", - "@babel/parser": "^7.24.7", - "@babel/types": "^7.24.7", - "debug": "^4.3.1", - "globals": "^11.1.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.5", - "dev": true, - "license": "MIT", - "dependencies": { - "ms": "2.1.2" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/@babel/traverse/node_modules/ms": { - "version": "2.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@babel/types": { - "version": "7.24.7", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "to-fast-properties": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@gar/promisify": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/set-array": "^1.0.0", - "@jridgewell/sourcemap-codec": "^1.4.10" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.14", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "dev": true, - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@npmcli/fs": { - "version": "1.1.1", - "dev": true, - "license": "ISC", - "dependencies": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - } - }, - "node_modules/@npmcli/move-file": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/@npmcli/move-file/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/@simonwep/pickr": { - "version": "1.8.2", - "license": "MIT", - "dependencies": { - "core-js": "^3.15.1", - "nanopop": "^2.1.0" - } - }, - "node_modules/@types/glob": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/minimatch": "*", - "@types/node": "*" - } - }, - "node_modules/@types/html-minifier-terser": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/json-schema": { - "version": "7.0.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/minimatch": { - "version": "5.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "18.15.11", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/q": { - "version": "1.5.5", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/source-list-map": { - "version": "0.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/tapable": { - "version": "1.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/@types/uglify-js": { - "version": "3.17.1", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "^0.6.1" - } - }, - "node_modules/@types/webpack": { - "version": "4.41.33", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/@types/webpack-sources": { - "version": "3.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" - } - }, - "node_modules/@types/webpack-sources/node_modules/source-map": { - "version": "0.7.4", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "license": "ISC" - }, - "node_modules/@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.9.0", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" - } - }, - "node_modules/@webassemblyjs/wast-parser": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.9.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webpack-cli/configtest": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "webpack": "4.x.x || 5.x.x", - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/info": { - "version": "1.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "envinfo": "^7.7.3" - }, - "peerDependencies": { - "webpack-cli": "4.x.x" - } - }, - "node_modules/@webpack-cli/serve": { - "version": "1.7.0", - "dev": true, - "license": "MIT", - "peerDependencies": { - "webpack-cli": "4.x.x" - }, - "peerDependenciesMeta": { - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "license": "Apache-2.0" - }, - "node_modules/abbrev": { - "version": "1.1.1", - "license": "ISC" - }, - "node_modules/acorn": { - "version": "8.8.2", - "dev": true, - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "6.12.6", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-errors": { - "version": "1.0.1", - "license": "MIT", - "peerDependencies": { - "ajv": ">=5.0.0" - } - }, - "node_modules/ajv-keywords": { - "version": "3.5.2", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/alphanum-sort": { - "version": "1.0.2", - "dev": true, - "license": "MIT" - }, - "node_modules/angular": { - "version": "1.8.3", - "license": "MIT" - }, - "node_modules/angular-route": { - "version": "1.8.3", - "license": "MIT" - }, - "node_modules/angular-templatecache-webpack-plugin": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "fs": "0.0.1-security", - "glob": "^7.1.6", - "glob-parent": "^5.1.1", - "jsesc": "^3.0.2", - "lodash.template": "^4.5.0", - "path": "^0.12.7", - "schema-utils": "^1.0.0" - }, - "peerDependencies": { - "webpack": "^1.0.0 || ^2.0.0 || ^3.0.0 || ^4.0.0" - } - }, - "node_modules/angular-translate": { - "version": "2.19.1", - "license": "MIT", - "dependencies": { - "angular": "^1.8.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/angular-translate-interpolation-messageformat": { - "version": "2.19.1", - "license": "MIT", - "dependencies": { - "angular-translate": "~2.19.1", - "messageformat": "~1.0.2" - } - }, - "node_modules/angular-translate-loader-static-files": { - "version": "2.19.1", - "license": "MIT", - "dependencies": { - "angular-translate": "~2.19.1" - } - }, - "node_modules/ansi-colors": { - "version": "3.2.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/ansi-regex": { - "version": "2.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "devOptional": true, - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/aproba": { - "version": "1.2.0", - "license": "ISC" - }, - "node_modules/argparse": { - "version": "1.0.10", - "dev": true, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/arr-diff": { - "version": "4.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-flatten": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/arr-union": { - "version": "3.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-buffer-byte-length": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "is-array-buffer": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/array-union": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-uniq": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-uniq": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array-unique": { - "version": "0.3.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/array.prototype.reduce": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/asn1.js": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", - "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/asn1.js/node_modules/bn.js": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", - "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==", - "license": "MIT" - }, - "node_modules/assert": { - "version": "1.5.0", - "license": "MIT", - "dependencies": { - "object-assign": "^4.1.1", - "util": "0.10.3" - } - }, - "node_modules/assert/node_modules/inherits": { - "version": "2.0.1", - "license": "ISC" - }, - "node_modules/assert/node_modules/util": { - "version": "0.10.3", - "license": "MIT", - "dependencies": { - "inherits": "2.0.1" - } - }, - "node_modules/assign-symbols": { - "version": "1.0.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/async-each": { - "version": "1.0.6", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "optional": true - }, - "node_modules/atob": { - "version": "2.1.2", - "license": "(MIT OR Apache-2.0)", - "bin": { - "atob": "bin/atob.js" - }, - "engines": { - "node": ">= 4.5.0" - } - }, - "node_modules/available-typed-arrays": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/babel-loader": { - "version": "8.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^2.0.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "engines": { - "node": ">= 8.9" - }, - "peerDependencies": { - "@babel/core": "^7.0.0", - "webpack": ">=2" - } - }, - "node_modules/babel-loader/node_modules/find-cache-dir": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/babel-loader/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader/node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/babel-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/babel-loader/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader/node_modules/make-dir": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/babel-loader/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/babel-loader/node_modules/schema-utils": { - "version": "2.7.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/babel-loader/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.4", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.1", - "core-js-compat": "^3.36.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/base": { - "version": "0.11.2", - "license": "MIT", - "dependencies": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base/node_modules/define-property": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/big.js": { - "version": "5.2.2", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.2.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=8" - } - }, - "node_modules/bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "file-uri-to-path": "1.0.0" - } - }, - "node_modules/blob-polyfill": { - "version": "9.0.20240710", - "resolved": "https://registry.npmjs.org/blob-polyfill/-/blob-polyfill-9.0.20240710.tgz", - "integrity": "sha512-DPUO/EjNANCgSVg0geTy1vmUpu5hhp9tV2F7xUSTUd1jwe4XpwupGB+lt5PhVUqpqAk+zK1etqp6Pl/HVf71Ug==" - }, - "node_modules/bluebird": { - "version": "3.7.2", - "license": "MIT" - }, - "node_modules/bn.js": { - "version": "5.2.1", - "license": "MIT" - }, - "node_modules/boolbase": { - "version": "1.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/brace-expansion": { - "version": "1.1.11", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "2.3.2", - "license": "MIT", - "dependencies": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/braces/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/braces/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/brorand": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/browserify-aes": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/browserify-cipher": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" - } - }, - "node_modules/browserify-des": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/browserify-rsa": { - "version": "4.1.0", - "license": "MIT", - "dependencies": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" - } - }, - "node_modules/browserify-sign": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", - "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", - "license": "ISC", - "dependencies": { - "bn.js": "^5.2.1", - "browserify-rsa": "^4.1.0", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.5", - "hash-base": "~3.0", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.7", - "readable-stream": "^2.3.8", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.12" - } - }, - "node_modules/browserify-sign/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/browserify-sign/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/browserify-zlib": { - "version": "0.2.0", - "license": "MIT", - "dependencies": { - "pako": "~1.0.5" - } - }, - "node_modules/browserslist": { - "version": "4.23.1", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "caniuse-lite": "^1.0.30001629", - "electron-to-chromium": "^1.4.796", - "node-releases": "^2.0.14", - "update-browserslist-db": "^1.0.16" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer": { - "version": "4.9.2", - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/buffer-xor": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/builtin-status-codes": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/cacache": { - "version": "12.0.4", - "license": "ISC", - "dependencies": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "node_modules/cache-base": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/call-bind": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/caller-callsite": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "callsites": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/caller-path": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "caller-callsite": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/callsites": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001632", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/chalk": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/chokidar": { - "version": "3.5.3", - "funding": [ - { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - ], - "license": "MIT", - "optional": true, - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chokidar/node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "optional": true, - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar/node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "optional": true, - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/chokidar/node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/chokidar/node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "optional": true, - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/chownr": { - "version": "1.1.4", - "license": "ISC" - }, - "node_modules/chrome-trace-event": { - "version": "1.0.3", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/cipher-base": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/class-utils": { - "version": "0.3.6", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/define-property": { - "version": "0.2.5", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor": { - "version": "0.1.4", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/is-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/class-utils/node_modules/kind-of": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-css": { - "version": "4.2.4", - "dev": true, - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/clean-webpack-plugin": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "del": "^4.1.1" - }, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "webpack": ">=4.0.0 <6.0.0" - } - }, - "node_modules/clone": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/clone-buffer": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clone-stats": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/cloneable-readable": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "process-nextick-args": "^2.0.0", - "readable-stream": "^2.3.5" - } - }, - "node_modules/closure-webpack-plugin": { - "version": "2.6.1", - "dev": true, - "license": "MIT", - "dependencies": { - "acorn": "8.x", - "acorn-walk": "^8.2.0", - "schema-utils": "1.x", - "unquoted-property-validator": "^1.0.2", - "webpack-sources": "1.x" - }, - "engines": { - "node": ">= 6.9.0 || >= 8.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "google-closure-compiler": ">=20200830.0.0", - "webpack": "4.x" - } - }, - "node_modules/coa": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/coa/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/coa/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/coa/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/coa/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/collection-visit": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/color": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.3", - "color-string": "^1.6.0" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/color-string": { - "version": "1.9.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" - } - }, - "node_modules/color/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/color/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.19", - "dev": true, - "license": "MIT" - }, - "node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/commondir": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/component-emitter": { - "version": "1.3.0", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/concat-stream": { - "version": "1.6.2", - "engines": [ - "node >= 0.8" - ], - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" - } - }, - "node_modules/console-browserify": { - "version": "1.2.0" - }, - "node_modules/constants-browserify": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/copy-concurrently": { - "version": "1.0.5", - "license": "ISC", - "dependencies": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" - } - }, - "node_modules/copy-descriptor": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "5.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "webpack-log": "^2.0.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "3.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/core-js": { - "version": "3.30.0", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.37.1", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "5.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/create-ecdh": { - "version": "4.0.4", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" - } - }, - "node_modules/create-ecdh/node_modules/bn.js": { - "version": "4.12.0", - "license": "MIT" - }, - "node_modules/create-hash": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" - } - }, - "node_modules/create-hmac": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "dev": true, - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-browserify": { - "version": "3.12.0", - "license": "MIT", - "dependencies": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" - }, - "engines": { - "node": "*" - } - }, - "node_modules/css-color-names": { - "version": "0.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/css-declaration-sorter": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" - }, - "engines": { - "node": ">4" - } - }, - "node_modules/css-declaration-sorter/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/css-declaration-sorter/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/css-loader": { - "version": "5.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.27.0 || ^5.0.0" - } - }, - "node_modules/css-loader/node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/css-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/css-loader/node_modules/schema-utils": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "1.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cacache": "^15.0.5", - "cssnano": "^4.1.10", - "find-cache-dir": "^3.3.1", - "jest-worker": "^26.3.0", - "p-limit": "^3.0.2", - "schema-utils": "^3.0.0", - "serialize-javascript": "^5.0.1", - "source-map": "^0.6.1", - "webpack-sources": "^1.4.3" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/cacache": { - "version": "15.3.0", - "dev": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/chownr": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/find-cache-dir": { - "version": "3.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/make-dir": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "semver": "^6.0.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/p-limit": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "yocto-queue": "^0.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/p-locate/node_modules/p-limit": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/p-map": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/rimraf": { - "version": "3.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/semver": { - "version": "6.3.1", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/serialize-javascript": { - "version": "5.0.1", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/ssri": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.1.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/css-minimizer-webpack-plugin/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/css-select": { - "version": "4.3.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-select-base-adapter": { - "version": "0.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/css-tree": { - "version": "1.0.0-alpha.37", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/css-what": { - "version": "6.1.0", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssesc": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "4.1.11", - "dev": true, - "license": "MIT", - "dependencies": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-preset-default": { - "version": "4.0.8", - "dev": true, - "license": "MIT", - "dependencies": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-preset-default/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/cssnano-preset-default/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/cssnano-util-get-arguments": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-get-match": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano-util-raw-cache/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/cssnano-util-raw-cache/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/cssnano-util-same-parent": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/cssnano/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/cssnano/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/csso": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "css-tree": "^1.1.2" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "1.1.3", - "dev": true, - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.14", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/csv": { - "version": "6.3.9", - "license": "MIT", - "dependencies": { - "csv-generate": "^4.4.1", - "csv-parse": "^5.5.6", - "csv-stringify": "^6.5.0", - "stream-transform": "^3.3.2" - }, - "engines": { - "node": ">= 0.1.90" - } - }, - "node_modules/csv-generate": { - "version": "4.4.1", - "license": "MIT" - }, - "node_modules/csv-parse": { - "version": "5.5.6", - "license": "MIT" - }, - "node_modules/csv-stringify": { - "version": "6.5.0", - "license": "MIT" - }, - "node_modules/cyclist": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/d3-path": { - "version": "3.1.0", - "license": "ISC", - "engines": { - "node": ">=12" - } - }, - "node_modules/d3-shape": { - "version": "3.2.0", - "license": "ISC", - "dependencies": { - "d3-path": "^3.1.0" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/datalist-polyfill": { - "version": "1.25.1", - "license": "MIT" - }, - "node_modules/debug": { - "version": "2.6.9", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/decode-uri-component": { - "version": "0.2.2", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/define-properties": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-property": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/del/node_modules/globby": { - "version": "6.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/del/node_modules/globby/node_modules/pify": { - "version": "2.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/des.js": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/diffie-hellman": { - "version": "5.0.3", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - } - }, - "node_modules/diffie-hellman/node_modules/bn.js": { - "version": "4.12.0", - "license": "MIT" - }, - "node_modules/dir-glob": { - "version": "2.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "path-type": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "1.4.1", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domain-browser": { - "version": "1.2.0", - "license": "MIT", - "engines": { - "node": ">=0.4", - "npm": ">=1.2" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "4.3.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "2.8.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dot-prop": { - "version": "5.3.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/duplexify": { - "version": "3.7.1", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/electron-to-chromium": { - "version": "1.4.798", - "dev": true, - "license": "ISC" - }, - "node_modules/elliptic": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", - "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", - "license": "MIT", - "dependencies": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/elliptic/node_modules/bn.js": { - "version": "4.12.0", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/end-of-stream": { - "version": "1.4.4", - "license": "MIT", - "dependencies": { - "once": "^1.4.0" - } - }, - "node_modules/enhanced-resolve": { - "version": "4.5.0", - "dependencies": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/enhanced-resolve/node_modules/memory-fs": { - "version": "0.5.0", - "license": "MIT", - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - }, - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/entities": { - "version": "2.2.0", - "dev": true, - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/envinfo": { - "version": "7.8.1", - "dev": true, - "license": "MIT", - "bin": { - "envinfo": "dist/cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/errno": { - "version": "0.1.8", - "license": "MIT", - "dependencies": { - "prr": "~1.0.1" - }, - "bin": { - "errno": "cli.js" - } - }, - "node_modules/error-ex": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-abstract": { - "version": "1.21.2", - "dev": true, - "license": "MIT", - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "es-set-tostringtag": "^2.0.1", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.5", - "get-intrinsic": "^1.2.0", - "get-symbol-description": "^1.0.0", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", - "has": "^1.0.3", - "has-property-descriptors": "^1.0.0", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "internal-slot": "^1.0.5", - "is-array-buffer": "^3.0.2", - "is-callable": "^1.2.7", - "is-negative-zero": "^2.0.2", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "is-string": "^1.0.7", - "is-typed-array": "^1.1.10", - "is-weakref": "^1.0.2", - "object-inspect": "^1.12.3", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.4.3", - "safe-regex-test": "^1.0.0", - "string.prototype.trim": "^1.2.7", - "string.prototype.trimend": "^1.0.6", - "string.prototype.trimstart": "^1.0.6", - "typed-array-length": "^1.0.4", - "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.9" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/es-array-method-boxes-properly": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/es-set-tostringtag": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3", - "has": "^1.0.3", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-to-primitive": { - "version": "1.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/escalade": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-string-regexp": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/eslint-scope": { - "version": "4.0.3", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "dev": true, - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/events": { - "version": "3.3.0", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/evp_bytestokey": { - "version": "1.0.3", - "license": "MIT", - "dependencies": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" - } - }, - "node_modules/expand-brackets": { - "version": "2.1.4", - "license": "MIT", - "dependencies": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/define-property": { - "version": "0.2.5", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor": { - "version": "0.1.4", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/expand-brackets/node_modules/kind-of": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/exports-loader": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/exports-loader/node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/exports-loader/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/exports-loader/node_modules/schema-utils": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/extend-shallow": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/define-property": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/extglob/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "license": "MIT" - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "license": "MIT" - }, - "node_modules/fastest-levenshtein": { - "version": "1.0.16", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 4.9.1" - } - }, - "node_modules/figgy-pudding": { - "version": "3.5.2", - "license": "ISC" - }, - "node_modules/file-saver": { - "version": "2.0.5", - "license": "MIT" - }, - "node_modules/file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", - "license": "MIT", - "optional": true - }, - "node_modules/fill-range": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fill-range/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/find-cache-dir": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/find-package-json": { - "version": "1.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/find-up": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "locate-path": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/flush-write-stream": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" - } - }, - "node_modules/for-each": { - "version": "0.3.3", - "dev": true, - "license": "MIT", - "dependencies": { - "is-callable": "^1.1.3" - } - }, - "node_modules/for-in": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fragment-cache": { - "version": "0.2.1", - "license": "MIT", - "dependencies": { - "map-cache": "^0.2.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/from2": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" - } - }, - "node_modules/fs": { - "version": "0.0.1-security", - "license": "ISC" - }, - "node_modules/fs-minipass": { - "version": "2.1.0", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/fs-write-stream-atomic": { - "version": "1.0.10", - "license": "ISC", - "dependencies": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" - } - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/function.prototype.name": { - "version": "1.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.19.0", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/functions-have-names": { - "version": "1.2.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/fuzzysort": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.0.2.tgz", - "integrity": "sha512-ZyahVgxvckB1Qosn7YGWLDJJp2XlyaQ2WmZeI+d0AzW0AMqVYnz5N89G6KAKa6m/LOtv+kzJn4lhDF/yVg11Cg==" - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-symbol-description": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-value": { - "version": "2.0.6", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/glob": { - "version": "7.2.3", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/globals": { - "version": "11.12.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/globalthis": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/globby": { - "version": "7.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/globby/node_modules/pify": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/google-closure-compiler": { - "version": "20240317.0.0", - "dev": true, - "license": "Apache-2.0", - "dependencies": { - "chalk": "4.x", - "google-closure-compiler-java": "^20240317.0.0", - "minimist": "1.x", - "vinyl": "2.x", - "vinyl-sourcemaps-apply": "^0.2.0" - }, - "bin": { - "google-closure-compiler": "cli.js" - }, - "engines": { - "node": ">=10" - }, - "optionalDependencies": { - "google-closure-compiler-linux": "^20240317.0.0", - "google-closure-compiler-osx": "^20240317.0.0", - "google-closure-compiler-windows": "^20240317.0.0" - } - }, - "node_modules/google-closure-compiler-java": { - "version": "20240317.0.0", - "dev": true, - "license": "Apache-2.0" - }, - "node_modules/google-closure-compiler-linux": { - "version": "20240317.0.0", - "cpu": [ - "x32", - "x64" - ], - "dev": true, - "license": "Apache-2.0", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/gopd": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "license": "ISC" - }, - "node_modules/has": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.1" - }, - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/has-bigints": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-flag": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.1.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-proto": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.0.3", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-value": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/has-values/node_modules/kind-of": { - "version": "4.0.0", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/hash-base": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/hash-base/node_modules/readable-stream": { - "version": "3.6.2", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/hash-base/node_modules/safe-buffer": { - "version": "5.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/hash.js": { - "version": "1.1.7", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" - } - }, - "node_modules/he": { - "version": "1.2.0", - "dev": true, - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/hex-color-regex": { - "version": "1.1.0", - "dev": true, - "license": "MIT" - }, - "node_modules/hmac-drbg": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" - } - }, - "node_modules/hsl-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/hsla-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "5.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.1", - "clean-css": "^4.2.3", - "commander": "^4.1.1", - "he": "^1.2.0", - "param-case": "^3.0.3", - "relateurl": "^0.2.7", - "terser": "^4.6.3" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/html-webpack-plugin": { - "version": "4.5.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^5.0.0", - "@types/tapable": "^1.0.5", - "@types/webpack": "^4.41.8", - "html-minifier-terser": "^5.0.1", - "loader-utils": "^1.2.3", - "lodash": "^4.17.20", - "pretty-error": "^2.1.1", - "tapable": "^1.1.3", - "util.promisify": "1.0.0" - }, - "engines": { - "node": ">=6.9" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/htmlparser2": { - "version": "6.1.0", - "dev": true, - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/https-browserify": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ieee754": { - "version": "1.2.1", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/iferr": { - "version": "0.1.5", - "license": "MIT" - }, - "node_modules/ignore": { - "version": "3.3.10", - "dev": true, - "license": "MIT" - }, - "node_modules/import-fresh": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/import-local": { - "version": "3.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, - "bin": { - "import-local-fixture": "fixtures/cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-local/node_modules/find-up": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/locate-path": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/p-locate": { - "version": "4.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/path-exists": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/import-local/node_modules/pkg-dir": { - "version": "4.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "find-up": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/indexes-of": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/infer-owner": { - "version": "1.0.4", - "license": "ISC" - }, - "node_modules/inflight": { - "version": "1.0.6", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "license": "ISC" - }, - "node_modules/internal-slot": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "get-intrinsic": "^1.2.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/interpret": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/is-absolute-url": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-accessor-descriptor": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-array-buffer": { - "version": "3.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.0", - "is-typed-array": "^1.1.10" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "dev": true, - "license": "MIT" - }, - "node_modules/is-bigint": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-bigints": "^1.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-boolean-object": { - "version": "1.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-buffer": { - "version": "1.1.6", - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-color-stop": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" - } - }, - "node_modules/is-core-module": { - "version": "2.11.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-data-descriptor": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-date-object": { - "version": "1.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-descriptor": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-directory": { - "version": "0.3.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extendable": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-negative-zero": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-number-object": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-number/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-obj": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-path-cwd": { - "version": "2.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-in-cwd": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "is-path-inside": "^2.1.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-path-inside": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "path-is-inside": "^1.0.2" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regex": { - "version": "1.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-resolvable": { - "version": "1.1.0", - "dev": true, - "license": "ISC" - }, - "node_modules/is-shared-array-buffer": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-string": { - "version": "1.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-symbol": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-typed-array": { - "version": "1.1.10", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-weakref": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-windows": { - "version": "1.0.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-wsl": { - "version": "1.1.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-worker": { - "version": "26.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/jquery": { - "version": "3.7.1", - "license": "MIT" - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "3.14.1", - "dev": true, - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.0.2", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-parse-better-errors": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/json5": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.0" - }, - "bin": { - "json5": "lib/cli.js" - } - }, - "node_modules/jstz": { - "version": "2.1.1", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/loader-runner": { - "version": "2.4.0", - "license": "MIT", - "engines": { - "node": ">=4.3.0 <5.0.0 || >=5.10" - } - }, - "node_modules/loader-utils": { - "version": "1.4.2", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/locate-path": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "license": "MIT" - }, - "node_modules/lodash._reinterpolate": { - "version": "3.0.0", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.template": { - "version": "4.5.0", - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0", - "lodash.templatesettings": "^4.0.0" - } - }, - "node_modules/lodash.templatesettings": { - "version": "4.2.0", - "license": "MIT", - "dependencies": { - "lodash._reinterpolate": "^3.0.0" - } - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "dev": true, - "license": "MIT" - }, - "node_modules/lower-case": { - "version": "2.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/make-dir": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/make-dir/node_modules/semver": { - "version": "5.7.2", - "license": "ISC", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/make-plural": { - "version": "3.0.6", - "license": "ISC", - "bin": { - "make-plural": "bin/make-plural" - }, - "optionalDependencies": { - "minimist": "^1.2.0" - } - }, - "node_modules/map-cache": { - "version": "0.2.2", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/map-visit": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "object-visit": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/md5.js": { - "version": "1.3.5", - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/mdn-data": { - "version": "2.0.4", - "dev": true, - "license": "CC0-1.0" - }, - "node_modules/memory-fs": { - "version": "0.4.1", - "license": "MIT", - "dependencies": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/messageformat": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "glob": "~7.0.6", - "make-plural": "~3.0.6", - "messageformat-parser": "^1.0.0", - "nopt": "~3.0.6", - "reserved-words": "^0.1.1" - }, - "bin": { - "messageformat": "bin/messageformat.js" - } - }, - "node_modules/messageformat-parser": { - "version": "1.1.0", - "license": "MIT" - }, - "node_modules/messageformat/node_modules/glob": { - "version": "7.0.6", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.2", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - } - }, - "node_modules/micromatch": { - "version": "3.1.10", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/miller-rabin": { - "version": "4.0.1", - "license": "MIT", - "dependencies": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, - "bin": { - "miller-rabin": "bin/miller-rabin" - } - }, - "node_modules/miller-rabin/node_modules/bn.js": { - "version": "4.12.0", - "license": "MIT" - }, - "node_modules/mini-css-extract-plugin": { - "version": "1.6.2", - "dev": true, - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.4.0 || ^5.0.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/json5": { - "version": "2.2.3", - "dev": true, - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/loader-utils": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/minimalistic-crypto-utils": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/minipass": { - "version": "3.3.6", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass-collect": { - "version": "1.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-flush": { - "version": "1.0.5", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minipass-pipeline": { - "version": "1.2.4", - "dev": true, - "license": "ISC", - "dependencies": { - "minipass": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/minipass/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/minizlib": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/minizlib/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/mississippi": { - "version": "3.0.0", - "license": "BSD-2-Clause", - "dependencies": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/mixin-deep": { - "version": "1.3.2", - "license": "MIT", - "dependencies": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/mkdirp": { - "version": "0.5.6", - "license": "MIT", - "dependencies": { - "minimist": "^1.2.6" - }, - "bin": { - "mkdirp": "bin/cmd.js" - } - }, - "node_modules/move-concurrently": { - "version": "1.0.1", - "license": "ISC", - "dependencies": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" - } - }, - "node_modules/ms": { - "version": "2.0.0", - "license": "MIT" - }, - "node_modules/nan": { - "version": "2.22.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.22.0.tgz", - "integrity": "sha512-nbajikzWTMwsW+eSsNm3QwlOs7het9gGJU5dDZzRTQGk03vyBOauxgI4VakDzE0PtsGTmXPsXTbbjVhRwR5mpw==", - "license": "MIT", - "optional": true - }, - "node_modules/nanoid": { - "version": "3.3.8", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", - "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", - "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/nanomatch": { - "version": "1.2.13", - "license": "MIT", - "dependencies": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/nanopop": { - "version": "2.2.0", - "license": "MIT" - }, - "node_modules/neo-async": { - "version": "2.6.2", - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-libs-browser": { - "version": "2.2.1", - "license": "MIT", - "dependencies": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - } - }, - "node_modules/node-libs-browser/node_modules/inherits": { - "version": "2.0.3", - "license": "ISC" - }, - "node_modules/node-libs-browser/node_modules/punycode": { - "version": "1.4.1", - "license": "MIT" - }, - "node_modules/node-libs-browser/node_modules/util": { - "version": "0.11.1", - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/node-releases": { - "version": "2.0.14", - "dev": true, - "license": "MIT" - }, - "node_modules/nopt": { - "version": "3.0.6", - "license": "ISC", - "dependencies": { - "abbrev": "1" - }, - "bin": { - "nopt": "bin/nopt.js" - } - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "3.3.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/nth-check": { - "version": "2.1.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy": { - "version": "0.1.0", - "license": "MIT", - "dependencies": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/define-property": { - "version": "0.2.5", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-data-descriptor": { - "version": "0.1.4", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/is-descriptor/node_modules/kind-of": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-copy/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.12.3", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object-visit": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.assign": { - "version": "4.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "has-symbols": "^1.0.3", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.getownpropertydescriptors": { - "version": "2.1.5", - "dev": true, - "license": "MIT", - "dependencies": { - "array.prototype.reduce": "^1.0.5", - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.8" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object.pick": { - "version": "1.3.0", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object.values": { - "version": "1.1.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/once": { - "version": "1.4.0", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/os-browserify": { - "version": "0.3.0", - "license": "MIT" - }, - "node_modules/p-limit": { - "version": "2.3.0", - "license": "MIT", - "dependencies": { - "p-try": "^2.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "p-limit": "^2.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/p-map": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/p-try": { - "version": "2.2.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pako": { - "version": "1.0.11", - "license": "(MIT AND Zlib)" - }, - "node_modules/parallel-transform": { - "version": "1.2.0", - "license": "MIT", - "dependencies": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parse-asn1": { - "version": "5.1.7", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", - "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", - "license": "ISC", - "dependencies": { - "asn1.js": "^4.10.1", - "browserify-aes": "^1.2.0", - "evp_bytestokey": "^1.0.3", - "hash-base": "~3.0", - "pbkdf2": "^3.1.2", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-asn1/node_modules/hash-base": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", - "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.4", - "safe-buffer": "^5.2.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/parse-asn1/node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/pascalcase": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path": { - "version": "0.12.7", - "license": "MIT", - "dependencies": { - "process": "^0.11.1", - "util": "^0.10.3" - } - }, - "node_modules/path-browserify": { - "version": "0.0.1", - "license": "MIT" - }, - "node_modules/path-dirname": { - "version": "1.0.2", - "devOptional": true, - "license": "MIT" - }, - "node_modules/path-exists": { - "version": "3.0.0", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "dev": true, - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "dev": true, - "license": "MIT" - }, - "node_modules/path-type": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "pify": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/path-type/node_modules/pify": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/pbkdf2": { - "version": "3.1.2", - "license": "MIT", - "dependencies": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" - }, - "engines": { - "node": ">=0.12" - } - }, - "node_modules/picocolors": { - "version": "1.0.1", - "dev": true, - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "devOptional": true, - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pify": { - "version": "4.0.1", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pinkie": { - "version": "2.0.4", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pinkie-promise": { - "version": "2.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "pinkie": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/pkg-dir": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "find-up": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/posix-character-classes": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postcss": { - "version": "8.4.31", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.6", - "picocolors": "^1.0.0", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-calc": { - "version": "7.0.5", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - } - }, - "node_modules/postcss-calc/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-calc/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-colormin": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-colormin/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-colormin/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-colormin/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-convert-values": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-convert-values/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-convert-values/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-convert-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-discard-comments": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-comments/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-discard-comments/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-duplicates/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-discard-duplicates/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-empty": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-empty/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-discard-empty/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-discard-overridden/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-discard-overridden/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "4.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-longhand/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-merge-longhand/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-longhand/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-merge-rules": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-merge-rules/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-merge-rules/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-font-values/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-minify-font-values/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-font-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-minify-gradients": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-gradients/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-minify-gradients/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-gradients/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-minify-params": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-params/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-minify-params/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-params/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-minify-selectors": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-minify-selectors/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-minify-selectors/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^6.0.4" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-charset/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-charset/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-display-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-positions": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-positions/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-positions/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-positions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-repeat-style/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-string": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-string/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-string/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-string/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-timing-functions/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-unicode": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-unicode/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-url": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-url/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-url/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-url/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-normalize-whitespace": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-normalize-whitespace/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-ordered-values": { - "version": "4.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-ordered-values/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-ordered-values/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-ordered-values/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-reduce-initial": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-initial/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-reduce-initial/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "4.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-reduce-transforms/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-selector-parser": { - "version": "6.0.11", - "dev": true, - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-svgo": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-svgo/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-svgo/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-svgo/node_modules/postcss-value-parser": { - "version": "3.3.1", - "dev": true, - "license": "MIT" - }, - "node_modules/postcss-unique-selectors": { - "version": "4.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/postcss-unique-selectors/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/postcss-unique-selectors/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "dev": true, - "license": "MIT" - }, - "node_modules/pretty-error": { - "version": "2.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^2.0.4" - } - }, - "node_modules/process": { - "version": "0.11.10", - "license": "MIT", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/promise-inflight": { - "version": "1.0.1", - "license": "ISC" - }, - "node_modules/prr": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/public-encrypt": { - "version": "4.0.3", - "license": "MIT", - "dependencies": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "node_modules/public-encrypt/node_modules/bn.js": { - "version": "4.12.0", - "license": "MIT" - }, - "node_modules/pump": { - "version": "3.0.0", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/pumpify": { - "version": "1.5.1", - "license": "MIT", - "dependencies": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - } - }, - "node_modules/pumpify/node_modules/pump": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - }, - "node_modules/punycode": { - "version": "2.3.0", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/q": { - "version": "1.5.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.6.0", - "teleport": ">=0.2.0" - } - }, - "node_modules/querystring": { - "version": "0.2.0", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/querystring-es3": { - "version": "0.2.1", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/randomfill": { - "version": "1.0.4", - "license": "MIT", - "dependencies": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" - } - }, - "node_modules/readable-stream": { - "version": "2.3.8", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "license": "MIT", - "optional": true, - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/rechoir": { - "version": "0.7.1", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve": "^1.9.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.1.1", - "dev": true, - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regenerator-runtime": { - "version": "0.14.1", - "dev": true, - "license": "MIT" - }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, - "node_modules/regex-not": { - "version": "1.0.2", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/regexp.prototype.flags": { - "version": "1.4.3", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "functions-have-names": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/regexpu-core": { - "version": "5.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "@babel/regjsgen": "^0.8.0", - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.1.0", - "regjsparser": "^0.9.1", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regjsparser": { - "version": "0.9.1", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~0.5.0" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "0.5.0", - "dev": true, - "bin": { - "jsesc": "bin/jsesc" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remove-trailing-separator": { - "version": "1.1.0", - "devOptional": true, - "license": "ISC" - }, - "node_modules/renderkid": { - "version": "2.0.7", - "dev": true, - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^3.0.1" - } - }, - "node_modules/repeat-element": { - "version": "1.1.4", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/replace-ext": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/reserved-words": { - "version": "0.1.2", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.1", - "dev": true, - "license": "MIT", - "dependencies": { - "is-core-module": "^2.9.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-cwd": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "resolve-from": "^5.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-cwd/node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/resolve-from": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-url": { - "version": "0.2.1", - "license": "MIT" - }, - "node_modules/ret": { - "version": "0.1.15", - "license": "MIT", - "engines": { - "node": ">=0.12" - } - }, - "node_modules/rgb-regex": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/rgba-regex": { - "version": "1.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/rimraf": { - "version": "2.7.1", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - } - }, - "node_modules/ripemd160": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" - } - }, - "node_modules/run-queue": { - "version": "1.0.3", - "license": "ISC", - "dependencies": { - "aproba": "^1.1.1" - } - }, - "node_modules/safe-buffer": { - "version": "5.1.2", - "license": "MIT" - }, - "node_modules/safe-regex": { - "version": "1.1.0", - "license": "MIT", - "dependencies": { - "ret": "~0.1.10" - } - }, - "node_modules/safe-regex-test": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "is-regex": "^1.1.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sax": { - "version": "1.2.4", - "dev": true, - "license": "ISC" - }, - "node_modules/schema-utils": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - }, - "engines": { - "node": ">= 4" - } - }, - "node_modules/semver": { - "version": "7.5.4", - "dev": true, - "license": "ISC", - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/lru-cache": { - "version": "6.0.0", - "dev": true, - "license": "ISC", - "dependencies": { - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/serialize-javascript": { - "version": "4.0.0", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/set-value": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/set-value/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/setimmediate": { - "version": "1.0.5", - "license": "MIT" - }, - "node_modules/sha.js": { - "version": "2.4.11", - "license": "(MIT AND BSD-3-Clause)", - "dependencies": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" - }, - "bin": { - "sha.js": "bin.js" - } - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/side-channel": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/simple-swizzle": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.3.1" - } - }, - "node_modules/simple-swizzle/node_modules/is-arrayish": { - "version": "0.3.2", - "dev": true, - "license": "MIT" - }, - "node_modules/slash": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon": { - "version": "0.8.2", - "license": "MIT", - "dependencies": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-node/node_modules/define-property": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "is-descriptor": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util": { - "version": "3.0.1", - "license": "MIT", - "dependencies": { - "kind-of": "^3.2.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon-util/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/define-property": { - "version": "0.2.5", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/extend-shallow": { - "version": "2.0.1", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor": { - "version": "0.1.4", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/kind-of": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/snapdragon/node_modules/source-map": { - "version": "0.5.7", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-list-map": { - "version": "2.0.1", - "license": "MIT" - }, - "node_modules/source-map": { - "version": "0.6.1", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-js": { - "version": "1.0.2", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-resolve": { - "version": "0.5.3", - "license": "MIT", - "dependencies": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-url": { - "version": "0.4.1", - "license": "MIT" - }, - "node_modules/split-string": { - "version": "3.1.0", - "license": "MIT", - "dependencies": { - "extend-shallow": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "dev": true, - "license": "BSD-3-Clause" - }, - "node_modules/ssri": { - "version": "6.0.2", - "license": "ISC", - "dependencies": { - "figgy-pudding": "^3.5.1" - } - }, - "node_modules/stable": { - "version": "0.1.8", - "dev": true, - "license": "MIT" - }, - "node_modules/static-extend": { - "version": "0.1.2", - "license": "MIT", - "dependencies": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/define-property": { - "version": "0.2.5", - "license": "MIT", - "dependencies": { - "is-descriptor": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-accessor-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor": { - "version": "0.1.4", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-data-descriptor/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/is-descriptor": { - "version": "0.1.6", - "license": "MIT", - "dependencies": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/static-extend/node_modules/kind-of": { - "version": "5.1.0", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stream-browserify": { - "version": "2.0.2", - "license": "MIT", - "dependencies": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } - }, - "node_modules/stream-each": { - "version": "1.2.3", - "license": "MIT", - "dependencies": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" - } - }, - "node_modules/stream-http": { - "version": "2.8.3", - "license": "MIT", - "dependencies": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" - } - }, - "node_modules/stream-shift": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/stream-transform": { - "version": "3.3.2", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.1.1", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/string.prototype.trim": { - "version": "1.2.7", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimend": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/string.prototype.trimstart": { - "version": "1.0.6", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.4", - "es-abstract": "^1.20.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/strip-ansi": { - "version": "3.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-regex": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/stylehacks": { - "version": "4.0.3", - "dev": true, - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/stylehacks/node_modules/picocolors": { - "version": "0.2.1", - "dev": true, - "license": "ISC" - }, - "node_modules/stylehacks/node_modules/postcss": { - "version": "7.0.39", - "dev": true, - "license": "MIT", - "dependencies": { - "picocolors": "^0.2.1", - "source-map": "^0.6.1" - }, - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - } - }, - "node_modules/stylehacks/node_modules/postcss-selector-parser": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svgo": { - "version": "1.3.2", - "dev": true, - "license": "MIT", - "dependencies": { - "chalk": "^2.4.1", - "coa": "^2.0.2", - "css-select": "^2.0.0", - "css-select-base-adapter": "^0.1.1", - "css-tree": "1.0.0-alpha.37", - "csso": "^4.0.2", - "js-yaml": "^3.13.1", - "mkdirp": "~0.5.1", - "object.values": "^1.1.0", - "sax": "~1.2.4", - "stable": "^0.1.8", - "unquote": "~1.1.1", - "util.promisify": "~1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/svgo/node_modules/ansi-styles": { - "version": "3.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "color-convert": "^1.9.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/chalk": { - "version": "2.4.2", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/color-convert": { - "version": "1.9.3", - "dev": true, - "license": "MIT", - "dependencies": { - "color-name": "1.1.3" - } - }, - "node_modules/svgo/node_modules/color-name": { - "version": "1.1.3", - "dev": true, - "license": "MIT" - }, - "node_modules/svgo/node_modules/css-select": { - "version": "2.1.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" - } - }, - "node_modules/svgo/node_modules/css-what": { - "version": "3.4.2", - "dev": true, - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/svgo/node_modules/dom-serializer": { - "version": "0.2.2", - "dev": true, - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" - } - }, - "node_modules/svgo/node_modules/domutils": { - "version": "1.7.0", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "node_modules/svgo/node_modules/domutils/node_modules/domelementtype": { - "version": "1.3.1", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/svgo/node_modules/has-flag": { - "version": "3.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/svgo/node_modules/nth-check": { - "version": "1.0.2", - "dev": true, - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "~1.0.0" - } - }, - "node_modules/svgo/node_modules/supports-color": { - "version": "5.5.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^3.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/tapable": { - "version": "1.1.3", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/tar": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", - "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, - "license": "ISC", - "dependencies": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^5.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/chownr": { - "version": "2.0.0", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=8" - } - }, - "node_modules/tar/node_modules/mkdirp": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "bin": { - "mkdirp": "bin/cmd.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/tar/node_modules/yallist": { - "version": "4.0.0", - "dev": true, - "license": "ISC" - }, - "node_modules/terser": { - "version": "4.8.1", - "license": "BSD-2-Clause", - "dependencies": { - "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "1.4.6", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.6.tgz", - "integrity": "sha512-2lBVf/VMVIddjSn3GqbT90GvIJ/eYXJkt8cTzU7NbjKqK8fwv18Ftr4PlbF46b/e88743iZFL5Dtr/rC4hjIeA==", - "license": "MIT", - "dependencies": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - }, - "engines": { - "node": ">= 6.9.0" - }, - "peerDependencies": { - "webpack": "^4.0.0" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "license": "MIT" - }, - "node_modules/through2": { - "version": "2.0.5", - "license": "MIT", - "dependencies": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "node_modules/timers-browserify": { - "version": "2.0.12", - "license": "MIT", - "dependencies": { - "setimmediate": "^1.0.4" - }, - "engines": { - "node": ">=0.6.0" - } - }, - "node_modules/timsort": { - "version": "0.3.0", - "dev": true, - "license": "MIT" - }, - "node_modules/to-arraybuffer": { - "version": "1.0.1", - "license": "MIT" - }, - "node_modules/to-fast-properties": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/to-object-path": { - "version": "0.3.0", - "license": "MIT", - "dependencies": { - "kind-of": "^3.0.2" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-object-path/node_modules/kind-of": { - "version": "3.2.2", - "license": "MIT", - "dependencies": { - "is-buffer": "^1.1.5" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex": { - "version": "3.0.2", - "license": "MIT", - "dependencies": { - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "regex-not": "^1.0.2", - "safe-regex": "^1.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/to-regex-range": { - "version": "2.1.1", - "license": "MIT", - "dependencies": { - "is-number": "^3.0.0", - "repeat-string": "^1.6.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/tslib": { - "version": "2.5.0", - "dev": true, - "license": "0BSD" - }, - "node_modules/tty-browserify": { - "version": "0.0.0", - "license": "MIT" - }, - "node_modules/typed-array-length": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "is-typed-array": "^1.1.9" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/typedarray": { - "version": "0.0.6", - "license": "MIT" - }, - "node_modules/unbox-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.2", - "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/union-value": { - "version": "1.0.1", - "license": "MIT", - "dependencies": { - "arr-union": "^3.1.0", - "get-value": "^2.0.6", - "is-extendable": "^0.1.1", - "set-value": "^2.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/union-value/node_modules/is-extendable": { - "version": "0.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/uniq": { - "version": "1.0.1", - "dev": true, - "license": "MIT" - }, - "node_modules/uniqs": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/unique-filename": { - "version": "1.1.1", - "license": "ISC", - "dependencies": { - "unique-slug": "^2.0.0" - } - }, - "node_modules/unique-slug": { - "version": "2.0.2", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4" - } - }, - "node_modules/unquote": { - "version": "1.1.1", - "dev": true, - "license": "MIT" - }, - "node_modules/unquoted-property-validator": { - "version": "1.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/unset-value": { - "version": "1.0.0", - "license": "MIT", - "dependencies": { - "has-value": "^0.3.1", - "isobject": "^3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value": { - "version": "0.3.1", - "license": "MIT", - "dependencies": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { - "version": "2.1.0", - "license": "MIT", - "dependencies": { - "isarray": "1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/unset-value/node_modules/has-values": { - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/upath": { - "version": "1.2.0", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=4", - "yarn": "*" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.0.16", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/urix": { - "version": "0.1.0", - "license": "MIT" - }, - "node_modules/url": { - "version": "0.11.0", - "license": "MIT", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "license": "MIT" - }, - "node_modules/use": { - "version": "3.1.1", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/util": { - "version": "0.10.4", - "license": "MIT", - "dependencies": { - "inherits": "2.0.3" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "license": "MIT" - }, - "node_modules/util.promisify": { - "version": "1.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "define-properties": "^1.1.2", - "object.getownpropertydescriptors": "^2.0.3" - } - }, - "node_modules/util/node_modules/inherits": { - "version": "2.0.3", - "license": "ISC" - }, - "node_modules/utila": { - "version": "0.4.0", - "dev": true, - "license": "MIT" - }, - "node_modules/uuid": { - "version": "3.4.0", - "dev": true, - "license": "MIT", - "bin": { - "uuid": "bin/uuid" - } - }, - "node_modules/vendors": { - "version": "1.0.4", - "dev": true, - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/vinyl": { - "version": "2.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "clone": "^2.1.1", - "clone-buffer": "^1.0.0", - "clone-stats": "^1.0.0", - "cloneable-readable": "^1.0.0", - "remove-trailing-separator": "^1.0.1", - "replace-ext": "^1.0.0" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/vinyl-sourcemaps-apply": { - "version": "0.2.1", - "dev": true, - "license": "ISC", - "dependencies": { - "source-map": "^0.5.1" - } - }, - "node_modules/vinyl-sourcemaps-apply/node_modules/source-map": { - "version": "0.5.7", - "dev": true, - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/vm-browserify": { - "version": "1.1.2", - "license": "MIT" - }, - "node_modules/watchpack": { - "version": "1.7.5", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0" - }, - "optionalDependencies": { - "chokidar": "^3.4.1", - "watchpack-chokidar2": "^2.0.1" - } - }, - "node_modules/watchpack-chokidar2": { - "version": "2.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "chokidar": "^2.1.8" - } - }, - "node_modules/watchpack-chokidar2/node_modules/anymatch": { - "version": "2.0.0", - "license": "ISC", - "optional": true, - "dependencies": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "node_modules/watchpack-chokidar2/node_modules/anymatch/node_modules/normalize-path": { - "version": "2.1.1", - "license": "MIT", - "optional": true, - "dependencies": { - "remove-trailing-separator": "^1.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/binary-extensions": { - "version": "1.13.1", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/chokidar": { - "version": "2.1.8", - "license": "MIT", - "optional": true, - "dependencies": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - }, - "optionalDependencies": { - "fsevents": "^1.2.7" - } - }, - "node_modules/watchpack-chokidar2/node_modules/fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "deprecated": "Upgrade to fsevents v2 to mitigate potential security issues", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "dependencies": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - }, - "engines": { - "node": ">= 4.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/glob-parent": { - "version": "3.1.0", - "license": "ISC", - "optional": true, - "dependencies": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/glob-parent/node_modules/is-glob": { - "version": "3.1.0", - "license": "MIT", - "optional": true, - "dependencies": { - "is-extglob": "^2.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/is-binary-path": { - "version": "1.0.1", - "license": "MIT", - "optional": true, - "dependencies": { - "binary-extensions": "^1.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/watchpack-chokidar2/node_modules/readdirp": { - "version": "2.2.1", - "license": "MIT", - "optional": true, - "dependencies": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - }, - "engines": { - "node": ">=0.10" - } - }, - "node_modules/webpack": { - "version": "4.47.0", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=6.11.5" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - }, - "webpack-command": { - "optional": true - } - } - }, - "node_modules/webpack-cli": { - "version": "4.10.0", - "dev": true, - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "^0.5.0", - "@webpack-cli/configtest": "^1.2.0", - "@webpack-cli/info": "^1.5.0", - "@webpack-cli/serve": "^1.7.0", - "colorette": "^2.0.14", - "commander": "^7.0.0", - "cross-spawn": "^7.0.3", - "fastest-levenshtein": "^1.0.12", - "import-local": "^3.0.2", - "interpret": "^2.2.0", - "rechoir": "^0.7.0", - "webpack-merge": "^5.7.3" - }, - "bin": { - "webpack-cli": "bin/cli.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "4.x.x || 5.x.x" - }, - "peerDependenciesMeta": { - "@webpack-cli/generators": { - "optional": true - }, - "@webpack-cli/migrate": { - "optional": true - }, - "webpack-bundle-analyzer": { - "optional": true - }, - "webpack-dev-server": { - "optional": true - } - } - }, - "node_modules/webpack-cli/node_modules/commander": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-log": { - "version": "2.0.0", - "dev": true, - "license": "MIT", - "dependencies": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/webpack-merge": { - "version": "5.8.0", - "dev": true, - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "1.4.3", - "license": "MIT", - "dependencies": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - }, - "node_modules/webpack/node_modules/acorn": { - "version": "6.4.2", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "dev": true, - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "dev": true, - "license": "MIT", - "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/which-typed-array": { - "version": "1.1.9", - "dev": true, - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.5", - "call-bind": "^1.0.2", - "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-tostringtag": "^1.0.0", - "is-typed-array": "^1.1.10" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/wildcard": { - "version": "2.0.0", - "dev": true, - "license": "MIT" - }, - "node_modules/worker-farm": { - "version": "1.7.0", - "license": "MIT", - "dependencies": { - "errno": "~0.1.7" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "license": "ISC" - }, - "node_modules/xtend": { - "version": "4.0.2", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, - "node_modules/y18n": { - "version": "4.0.3", - "license": "ISC" - }, - "node_modules/yallist": { - "version": "3.1.1", - "license": "ISC" - }, - "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/yocto-queue": { - "version": "0.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - } - } -} diff --git a/guacamole/src/main/frontend/package.json b/guacamole/src/main/frontend/package.json deleted file mode 100644 index 5d63c88b90..0000000000 --- a/guacamole/src/main/frontend/package.json +++ /dev/null @@ -1,43 +0,0 @@ -{ - "private": true, - "scripts": { - "build": "webpack --progress" - }, - "dependencies": { - "@simonwep/pickr": "^1.8.2", - "angular": "^1.8.3", - "angular-route": "^1.8.3", - "angular-templatecache-webpack-plugin": "^1.0.1", - "angular-translate": "^2.19.1", - "angular-translate-interpolation-messageformat": "^2.19.1", - "angular-translate-loader-static-files": "^2.19.1", - "blob-polyfill": "^9.0.20240710", - "csv": "^6.3.9", - "d3-path": "^3.1.0", - "d3-shape": "^3.2.0", - "datalist-polyfill": "^1.25.1", - "file-saver": "^2.0.5", - "fuzzysort": "^3.0.2", - "jquery": "^3.7.1", - "jstz": "^2.1.1", - "lodash": "^4.17.21", - "yaml": "^2.5.0" - }, - "devDependencies": { - "@babel/core": "^7.24.7", - "@babel/preset-env": "^7.24.7", - "babel-loader": "^8.3.0", - "clean-webpack-plugin": "^4.0.0", - "closure-webpack-plugin": "^2.6.1", - "copy-webpack-plugin": "^5.1.2", - "css-loader": "^5.2.7", - "css-minimizer-webpack-plugin": "^1.3.0", - "exports-loader": "^1.1.1", - "find-package-json": "^1.2.0", - "google-closure-compiler": "20240317.0.0", - "html-webpack-plugin": "^4.5.2", - "mini-css-extract-plugin": "^1.6.2", - "webpack": "^4.47.0", - "webpack-cli": "^4.10.0" - } -} diff --git a/guacamole/src/main/frontend/plugins/dependency-list-plugin.js b/guacamole/src/main/frontend/plugins/dependency-list-plugin.js deleted file mode 100644 index 2dfaa00613..0000000000 --- a/guacamole/src/main/frontend/plugins/dependency-list-plugin.js +++ /dev/null @@ -1,157 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -const finder = require('find-package-json'); -const fs = require('fs'); -const path = require('path'); -const validateOptions = require('schema-utils'); - -/** - * The name of this plugin. - * - * @type {string} - */ -const PLUGIN_NAME = 'dependency-list-plugin'; - -/** - * The schema of the configuration options object accepted by the constructor - * of DependencyListPlugin. - * - * @see https://github.com/webpack/schema-utils/blob/v1.0.0/README.md#usage - */ -const PLUGIN_OPTIONS_SCHEMA = { - type: 'object', - properties: { - - /** - * The name of the file that should contain the dependency list. By - * default, this will be "npm-dependencies.txt". - */ - filename: { type: 'string' }, - - /** - * The path in which the dependency list file should be saved. By - * default, this will be the output path of the Webpack compiler. - */ - path: { type: 'string' } - - }, - additionalProperties: false -}; - -/** - * Webpack plugin that automatically lists each of the NPM dependencies - * included within any bundles produced by the compile process. - */ -class DependencyListPlugin { - - /** - * Creates a new DependencyListPlugin configured with the given options. - * The options given must conform to the options schema. - * - * @see PLUGIN_OPTIONS_SCHEMA - * - * @param {*} options - * The configuration options to apply to the plugin. - */ - constructor(options = {}) { - validateOptions(PLUGIN_OPTIONS_SCHEMA, options, 'DependencyListPlugin'); - this.options = options; - } - - /** - * Entrypoint for all Webpack plugins. This function will be invoked when - * the plugin is being associated with the compile process. - * - * @param {Compiler} compiler - * A reference to the Webpack compiler. - */ - apply(compiler) { - - /** - * Logger for this plugin. - * - * @type {Logger} - */ - const logger = compiler.getInfrastructureLogger(PLUGIN_NAME); - - /** - * The directory receiving the dependency list file. - * - * @type {string} - */ - const outputPath = this.options.path || compiler.options.output.path; - - /** - * The full path to the output file that should contain the list of - * discovered NPM module dependencies. - * - * @type {string} - */ - const outputFile = path.join( - outputPath, - this.options.filename || 'npm-dependencies.txt' - ); - - // Wait for compilation to fully complete - compiler.hooks.done.tap(PLUGIN_NAME, (stats) => { - - const moduleCoords = {}; - - // Map each file used within any bundle built by the compiler to - // its corresponding NPM package, ignoring files that have no such - // package - stats.compilation.fileDependencies.forEach(file => { - - // Locate NPM package corresponding to file dependency (there - // may not be one) - const moduleFinder = finder(file); - const npmPackage = moduleFinder.next().value; - - // Translate absolute path into more readable path relative to - // root of compilation process - const relativePath = path.relative(compiler.options.context, file); - - if (npmPackage.name) { - moduleCoords[npmPackage.name + ':' + npmPackage.version] = true; - logger.info('File dependency "%s" mapped to NPM package "%s" (v%s)', - relativePath, npmPackage.name, npmPackage.version); - } - else - logger.info('Skipping file dependency "%s" (no NPM package)', - relativePath); - - }); - - // Create output path if it doesn't yet exist - if (!fs.existsSync(outputPath)) - fs.mkdirSync(outputPath, { recursive: true, mode: 0o755 }); - - // Write all discovered NPM packages to configured output file - const sortedCoords = Object.keys(moduleCoords).sort(); - fs.writeFileSync(outputFile, sortedCoords.join('\n') + '\n'); - - }); - - } - -} - -module.exports = DependencyListPlugin; - diff --git a/guacamole/src/main/frontend/src/angular-module-shim.js b/guacamole/src/main/frontend/src/angular-module-shim.js deleted file mode 100644 index 3e6e6d315c..0000000000 --- a/guacamole/src/main/frontend/src/angular-module-shim.js +++ /dev/null @@ -1,56 +0,0 @@ -/* - * The MIT License (MIT) - * - * Copyright (c) 2014 Jed Richards - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -(function(angular) { - - 'use strict'; - - if ( !angular ) { - throw new Error('angular-module-shim: Missing Angular'); - } - - var origFn = angular.module; - var hash = {}; - - angular.module = function(name,requires,configFn) { - - var requires = requires || []; - var registered = hash[name]; - var module; - - if ( registered ) { - module = origFn(name); - module.requires.push.apply(module.requires,requires); - // Register the config function if it exists. - if (configFn) { - module.config(configFn); - } - } else { - hash[name] = true; - module = origFn(name,requires,configFn); - } - - return module; - }; -})(window.angular); diff --git a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js b/guacamole/src/main/frontend/src/app/client/controllers/clientController.js deleted file mode 100644 index 6a486fe9a2..0000000000 --- a/guacamole/src/main/frontend/src/app/client/controllers/clientController.js +++ /dev/null @@ -1,879 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for the page used to connect to a connection or balancing group. - */ -angular.module('client').controller('clientController', ['$scope', '$routeParams', '$injector', - function clientController($scope, $routeParams, $injector) { - - // Required types - const ConnectionGroup = $injector.get('ConnectionGroup'); - const ManagedClient = $injector.get('ManagedClient'); - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - const ManagedClientState = $injector.get('ManagedClientState'); - const ManagedFilesystem = $injector.get('ManagedFilesystem'); - const Protocol = $injector.get('Protocol'); - const ScrollState = $injector.get('ScrollState'); - - // Required services - const $location = $injector.get('$location'); - const authenticationService = $injector.get('authenticationService'); - const connectionGroupService = $injector.get('connectionGroupService'); - const clipboardService = $injector.get('clipboardService'); - const dataSourceService = $injector.get('dataSourceService'); - const guacClientManager = $injector.get('guacClientManager'); - const guacFullscreen = $injector.get('guacFullscreen'); - const iconService = $injector.get('iconService'); - const preferenceService = $injector.get('preferenceService'); - const requestService = $injector.get('requestService'); - const tunnelService = $injector.get('tunnelService'); - const userPageService = $injector.get('userPageService'); - - /** - * The minimum number of pixels a drag gesture must move to result in the - * menu being shown or hidden. - * - * @type Number - */ - var MENU_DRAG_DELTA = 64; - - /** - * The maximum X location of the start of a drag gesture for that gesture - * to potentially show the menu. - * - * @type Number - */ - var MENU_DRAG_MARGIN = 64; - - /** - * When showing or hiding the menu via a drag gesture, the maximum number - * of pixels the touch can move vertically and still affect the menu. - * - * @type Number - */ - var MENU_DRAG_VERTICAL_TOLERANCE = 10; - - /** - * In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are - * several possible keysysms for each key. - */ - var SHIFT_KEYS = {0xFFE1 : true, 0xFFE2 : true}, - ALT_KEYS = {0xFFE9 : true, 0xFFEA : true, 0xFE03 : true, - 0xFFE7 : true, 0xFFE8 : true}, - CTRL_KEYS = {0xFFE3 : true, 0xFFE4 : true}, - MENU_KEYS = angular.extend({}, SHIFT_KEYS, ALT_KEYS, CTRL_KEYS); - - /** - * Keysym for detecting any END key presses, for the purpose of passing through - * the Ctrl-Alt-Del sequence to a remote system. - */ - var END_KEYS = {0xFF57 : true, 0xFFB1 : true}; - - /** - * Keysym for sending the DELETE key when the Ctrl-Alt-End hotkey - * combo is pressed. - * - * @type Number - */ - var DEL_KEY = 0xFFFF; - - /** - * Menu-specific properties. - */ - $scope.menu = { - - /** - * Whether the menu is currently shown. - * - * @type Boolean - */ - shown : false, - - /** - * The currently selected input method. This may be any of the values - * defined within preferenceService.inputMethods. - * - * @type String - */ - inputMethod : preferenceService.preferences.inputMethod, - - /** - * Whether translation of touch to mouse events should emulate an - * absolute pointer device, or a relative pointer device. - * - * @type Boolean - */ - emulateAbsoluteMouse : preferenceService.preferences.emulateAbsoluteMouse, - - /** - * The current scroll state of the menu. - * - * @type ScrollState - */ - scrollState : new ScrollState(), - - /** - * The current desired values of all editable connection parameters as - * a set of name/value pairs, including any changes made by the user. - * - * @type {Object.} - */ - connectionParameters : {} - - }; - - // Convenience method for closing the menu - $scope.closeMenu = function closeMenu() { - $scope.menu.shown = false; - }; - - /** - * Applies any changes to connection parameters made by the user within the - * Guacamole menu to the given ManagedClient. If no client is supplied, - * this function has no effect. - * - * @param {ManagedClient} client - * The client to apply parameter changes to. - */ - $scope.applyParameterChanges = function applyParameterChanges(client) { - angular.forEach($scope.menu.connectionParameters, function sendArgv(value, name) { - if (client) - ManagedClient.setArgument(client, name, value); - }); - }; - - /** - * The currently-focused client within the current ManagedClientGroup. If - * there is no current group, no client is focused, or multiple clients are - * focused, this will be null. - * - * @type ManagedClient - */ - $scope.focusedClient = null; - - /** - * The set of clients that should be attached to the client UI. This will - * be immediately initialized by a call to updateAttachedClients() below. - * - * @type ManagedClientGroup - */ - $scope.clientGroup = null; - - /** - * @borrows ManagedClientGroup.getName - */ - $scope.getName = ManagedClientGroup.getName; - - /** - * @borrows ManagedClientGroup.getTitle - */ - $scope.getTitle = ManagedClientGroup.getTitle; - - /** - * Arbitrary context that should be exposed to the guacGroupList directive - * displaying the dropdown list of available connections within the - * Guacamole menu. - */ - $scope.connectionListContext = { - - /** - * The set of clients desired within the current view. For each client - * that should be present within the current view, that client's ID - * will map to "true" here. - * - * @type {Object.} - */ - attachedClients : {}, - - /** - * Notifies that the client with the given ID has been added or - * removed from the set of clients desired within the current view, - * and the current view should be updated accordingly. - * - * @param {string} id - * The ID of the client that was added or removed from the current - * view. - */ - updateAttachedClients : function updateAttachedClients(id) { - $scope.addRemoveClient(id, !$scope.connectionListContext.attachedClients[id]); - } - - }; - - /** - * Adds or removes the client with the given ID from the set of clients - * within the current view, updating the current URL accordingly. - * - * @param {string} id - * The ID of the client to add or remove from the current view. - * - * @param {boolean} [remove=false] - * Whether the specified client should be added (false) or removed - * (true). - */ - $scope.addRemoveClient = function addRemoveClient(id, remove) { - - // Deconstruct current path into corresponding client IDs - const ids = ManagedClientGroup.getClientIdentifiers($routeParams.id); - - // Add/remove ID as requested - if (remove) - _.pull(ids, id); - else - ids.push(id); - - // Reconstruct path, updating attached clients via change in route - $location.path('/client/' + ManagedClientGroup.getIdentifier(ids)); - - }; - - /** - * Reloads the contents of $scope.clientGroup to reflect the client IDs - * currently listed in the URL. - */ - const reparseRoute = function reparseRoute() { - - const previousClients = $scope.clientGroup ? $scope.clientGroup.clients.slice() : []; - - // Replace existing group with new group - setAttachedGroup(guacClientManager.getManagedClientGroup($routeParams.id)); - - // Store current set of attached clients for later use within the - // Guacamole menu - $scope.connectionListContext.attachedClients = {}; - $scope.clientGroup.clients.forEach((client) => { - $scope.connectionListContext.attachedClients[client.id] = true; - }); - - // Ensure menu is closed if updated view is not a modification of the - // current view (has no clients in common). The menu should remain open - // only while the current view is being modified, not when navigating - // to an entirely different view. - if (_.isEmpty(_.intersection(previousClients, $scope.clientGroup.clients))) - $scope.menu.shown = false; - - // Update newly-attached clients with current contents of clipboard - clipboardService.resyncClipboard(); - - }; - - /** - * Replaces the ManagedClientGroup currently attached to the client - * interface via $scope.clientGroup with the given ManagedClientGroup, - * safely cleaning up after the previous group. If no ManagedClientGroup is - * provided, the existing group is simply removed. - * - * @param {ManagedClientGroup} [managedClientGroup] - * The ManagedClientGroup to attach to the interface, if any. - */ - const setAttachedGroup = function setAttachedGroup(managedClientGroup) { - - // Do nothing if group is not actually changing - if ($scope.clientGroup === managedClientGroup) - return; - - if ($scope.clientGroup) { - - // Remove all disconnected clients from management (the user has - // seen their status) - _.filter($scope.clientGroup.clients, client => { - - const connectionState = client.clientState.connectionState; - return connectionState === ManagedClientState.ConnectionState.DISCONNECTED - || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR - || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR; - - }).forEach(client => { - guacClientManager.removeManagedClient(client.id); - }); - - // Flag group as detached - $scope.clientGroup.attached = false; - - } - - if (managedClientGroup) { - $scope.clientGroup = managedClientGroup; - $scope.clientGroup.attached = true; - $scope.clientGroup.lastUsed = new Date().getTime(); - } - - }; - - // Init sets of clients based on current URL ... - reparseRoute(); - - // ... and re-initialize those sets if the URL has changed without - // reloading the route - $scope.$on('$routeUpdate', reparseRoute); - - /** - * The root connection groups of the connection hierarchy that should be - * presented to the user for selecting a different connection, as a map of - * data source identifier to the root connection group of that data - * source. This will be null if the connection group hierarchy has not yet - * been loaded or if the hierarchy is inapplicable due to only one - * connection or balancing group being available. - * - * @type Object. - */ - $scope.rootConnectionGroups = null; - - /** - * Array of all connection properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionProperties = [ - 'name' - ]; - - /** - * Array of all connection group properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionGroupProperties = [ - 'name' - ]; - - // Retrieve root groups and all descendants - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - authenticationService.getAvailableDataSources(), - ConnectionGroup.ROOT_IDENTIFIER - ) - .then(function rootGroupsRetrieved(rootConnectionGroups) { - - // Store retrieved groups only if there are multiple connections or - // balancing groups available - var clientPages = userPageService.getClientPages(rootConnectionGroups); - if (clientPages.length > 1) - $scope.rootConnectionGroups = rootConnectionGroups; - - }, requestService.WARN); - - /** - * Map of all available sharing profiles for the current connection by - * their identifiers. If this information is not yet available, or no such - * sharing profiles exist, this will be an empty object. - * - * @type Object. - */ - $scope.sharingProfiles = {}; - - /** - * Map of all substituted key presses. If one key is pressed in place of another - * the value of the substituted key is stored in an object with the keysym of - * the original key. - * - * @type Object. - */ - var substituteKeysPressed = {}; - - /** - * Returns whether the shortcut for showing/hiding the Guacamole menu - * (Ctrl+Alt+Shift) has been pressed. - * - * @param {Guacamole.Keyboard} keyboard - * The Guacamole.Keyboard object tracking the local keyboard state. - * - * @returns {boolean} - * true if Ctrl+Alt+Shift has been pressed, false otherwise. - */ - const isMenuShortcutPressed = function isMenuShortcutPressed(keyboard) { - - // Ctrl+Alt+Shift has NOT been pressed if any key is currently held - // down that isn't Ctrl, Alt, or Shift - if (_.findKey(keyboard.pressed, (val, keysym) => !MENU_KEYS[keysym])) - return false; - - // Verify that one of each required key is held, regardless of - // left/right location on the keyboard - return !!( - _.findKey(SHIFT_KEYS, (val, keysym) => keyboard.pressed[keysym]) - && _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym]) - && _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym]) - ); - - }; - - // Show menu if the user swipes from the left, hide menu when the user - // swipes from the right, scroll menu while visible - $scope.menuDrag = function menuDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) { - - if ($scope.menu.shown) { - - // Hide menu if swipe-from-right gesture is detected - if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE - && startX - currentX >= MENU_DRAG_DELTA) - $scope.menu.shown = false; - - // Scroll menu by default - else { - $scope.menu.scrollState.left -= deltaX; - $scope.menu.scrollState.top -= deltaY; - } - - } - - // Show menu if swipe-from-left gesture is detected - else if (startX <= MENU_DRAG_MARGIN) { - if (Math.abs(currentY - startY) < MENU_DRAG_VERTICAL_TOLERANCE - && currentX - startX >= MENU_DRAG_DELTA) - $scope.menu.shown = true; - } - - return false; - - }; - - // Show/hide UI elements depending on input method - $scope.$watch('menu.inputMethod', function setInputMethod(inputMethod) { - - // Show input methods only if selected - $scope.showOSK = (inputMethod === 'osk'); - $scope.showTextInput = (inputMethod === 'text'); - - }); - - // Update client state/behavior as visibility of the Guacamole menu changes - $scope.$watch('menu.shown', function menuVisibilityChanged(menuShown, menuShownPreviousState) { - - // Re-update available connection parameters, if there is a focused - // client (parameter information may not have been available at the - // time focus changed) - if (menuShown) - $scope.menu.connectionParameters = $scope.focusedClient ? - ManagedClient.getArgumentModel($scope.focusedClient) : {}; - - // Send any argument value data once menu is hidden - else if (menuShownPreviousState) - $scope.applyParameterChanges($scope.focusedClient); - - /* Broadcast changes to the menu display state */ - $scope.$broadcast('guacMenuShown', menuShown); - - }); - - // Toggle the menu when the guacClientToggleMenu event is received - $scope.$on('guacToggleMenu', - () => $scope.menu.shown = !$scope.menu.shown); - - // Show the menu when the guacClientShowMenu event is received - $scope.$on('guacShowMenu', () => $scope.menu.shown = true); - - // Hide the menu when the guacClientHideMenu event is received - $scope.$on('guacHideMenu', () => $scope.menu.shown = false); - - // Automatically track and cache the currently-focused client - $scope.$on('guacClientFocused', function focusedClientChanged(event, newFocusedClient) { - - const oldFocusedClient = $scope.focusedClient; - $scope.focusedClient = newFocusedClient; - - // Apply any parameter changes when focus is changing - if (oldFocusedClient) - $scope.applyParameterChanges(oldFocusedClient); - - // Update available connection parameters, if there is a focused - // client - $scope.menu.connectionParameters = newFocusedClient ? - ManagedClient.getArgumentModel(newFocusedClient) : {}; - - }); - - // Automatically update connection parameters that have been modified - // for the current focused client - $scope.$on('guacClientArgumentsUpdated', function argumentsChanged(event, focusedClient) { - - // Ignore any updated arguments not for the current focused client - if ($scope.focusedClient && $scope.focusedClient === focusedClient) - $scope.menu.connectionParameters = ManagedClient.getArgumentModel(focusedClient); - - }); - - // Update page icon when thumbnail changes - $scope.$watch('focusedClient.thumbnail.canvas', function thumbnailChanged(canvas) { - iconService.setIcons(canvas); - }); - - // Pull sharing profiles once the tunnel UUID is known - $scope.$watch('focusedClient.tunnel.uuid', function retrieveSharingProfiles(uuid) { - - // Only pull sharing profiles if tunnel UUID is actually available - if (!uuid) { - $scope.sharingProfiles = {}; - return; - } - - // Pull sharing profiles for the current connection - tunnelService.getSharingProfiles(uuid) - .then(function sharingProfilesRetrieved(sharingProfiles) { - $scope.sharingProfiles = sharingProfiles; - }, requestService.WARN); - - }); - - /** - * Produces a sharing link for the current connection using the given - * sharing profile. The resulting sharing link, and any required login - * information, will be displayed to the user within the Guacamole menu. - * - * @param {SharingProfile} sharingProfile - * The sharing profile to use to generate the sharing link. - */ - $scope.share = function share(sharingProfile) { - if ($scope.focusedClient) - ManagedClient.createShareLink($scope.focusedClient, sharingProfile); - }; - - /** - * Returns whether the current connection has any associated share links. - * - * @returns {Boolean} - * true if the current connection has at least one associated share - * link, false otherwise. - */ - $scope.isShared = function isShared() { - return !!$scope.focusedClient && ManagedClient.isShared($scope.focusedClient); - }; - - /** - * Returns the total number of share links associated with the current - * connection. - * - * @returns {Number} - * The total number of share links associated with the current - * connection. - */ - $scope.getShareLinkCount = function getShareLinkCount() { - - if (!$scope.focusedClient) - return 0; - - // Count total number of links within the ManagedClient's share link map - var linkCount = 0; - for (const dummy in $scope.focusedClient.shareLinks) - linkCount++; - - return linkCount; - - }; - - // Opening the Guacamole menu after Ctrl+Alt+Shift, preventing those - // keypresses from reaching any Guacamole client - $scope.$on('guacBeforeKeydown', function incomingKeydown(event, keysym, keyboard) { - - // Toggle menu if menu shortcut (Ctrl+Alt+Shift) is pressed - if (isMenuShortcutPressed(keyboard)) { - - // Don't send this key event through to the client, and release - // all other keys involved in performing this shortcut - event.preventDefault(); - keyboard.reset(); - - // Toggle the menu - $scope.$apply(function() { - $scope.menu.shown = !$scope.menu.shown; - }); - - } - - // Prevent all keydown events while menu is open - else if ($scope.menu.shown) - event.preventDefault(); - - }); - - // Prevent all keyup events while menu is open - $scope.$on('guacBeforeKeyup', function incomingKeyup(event, keysym, keyboard) { - if ($scope.menu.shown) - event.preventDefault(); - }); - - // Send Ctrl-Alt-Delete when Ctrl-Alt-End is pressed. - $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - - // If one of the End keys is pressed, and we have a one keysym from each - // of Ctrl and Alt groups, send Ctrl-Alt-Delete. - if (END_KEYS[keysym] - && _.findKey(ALT_KEYS, (val, keysym) => keyboard.pressed[keysym]) - && _.findKey(CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym]) - ) { - - // Don't send this event through to the client. - event.preventDefault(); - - // Record the substituted key press so that it can be - // properly dealt with later. - substituteKeysPressed[keysym] = DEL_KEY; - - // Send through the delete key. - $scope.$broadcast('guacSyntheticKeydown', DEL_KEY); - } - - }); - - // Update pressed keys as they are released - $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - - // Deal with substitute key presses - if (substituteKeysPressed[keysym]) { - event.preventDefault(); - $scope.$broadcast('guacSyntheticKeyup', substituteKeysPressed[keysym]); - delete substituteKeysPressed[keysym]; - } - - }); - - // Update page title when client title changes - $scope.$watch('getTitle(clientGroup)', function clientTitleChanged(title) { - $scope.page.title = title; - }); - - /** - * Returns whether the current connection has been flagged as unstable due - * to an apparent network disruption. - * - * @returns {Boolean} - * true if the current connection has been flagged as unstable, false - * otherwise. - */ - $scope.isConnectionUnstable = function isConnectionUnstable() { - return _.findIndex($scope.clientGroup.clients, client => client.clientState.tunnelUnstable) !== -1; - }; - - /** - * Immediately disconnects all currently-focused clients, if any. - */ - $scope.disconnect = function disconnect() { - - // Disconnect if client is available - if ($scope.clientGroup) { - $scope.clientGroup.clients.forEach(client => { - if (client.clientProperties.focused) - client.client.disconnect(); - }); - } - - // Hide menu - $scope.menu.shown = false; - - }; - - /** - * Disconnects the given ManagedClient, removing it from the current - * view. - * - * @param {ManagedClient} client - * The client to disconnect. - */ - $scope.closeClientTile = function closeClientTile(client) { - - $scope.addRemoveClient(client.id, true); - guacClientManager.removeManagedClient(client.id); - - // Ensure at least one client has focus (the only client with - // focus may just have been removed) - ManagedClientGroup.verifyFocus($scope.clientGroup); - - }; - - /** - * Action which immediately disconnects the currently-connected client, if - * any. - */ - var DISCONNECT_MENU_ACTION = { - name : 'CLIENT.ACTION_DISCONNECT', - className : 'danger disconnect', - callback : $scope.disconnect - }; - - /** - * Action that toggles fullscreen mode within the - * currently-connected client and then closes the menu. - */ - var FULLSCREEN_MENU_ACTION = { - name : 'CLIENT.ACTION_FULLSCREEN', - classname : 'fullscreen action', - callback : function fullscreen() { - - guacFullscreen.toggleFullscreenMode(); - $scope.menu.shown = false; - } - }; - - // Set client-specific menu actions - $scope.clientMenuActions = [ DISCONNECT_MENU_ACTION,FULLSCREEN_MENU_ACTION ]; - - /** - * @borrows Protocol.getNamespace - */ - $scope.getProtocolNamespace = Protocol.getNamespace; - - /** - * The currently-visible filesystem within the filesystem menu, if the - * filesystem menu is open. If no filesystem is currently visible, this - * will be null. - * - * @type ManagedFilesystem - */ - $scope.filesystemMenuContents = null; - - /** - * Hides the filesystem menu. - */ - $scope.hideFilesystemMenu = function hideFilesystemMenu() { - $scope.filesystemMenuContents = null; - }; - - /** - * Shows the filesystem menu, displaying the contents of the given - * filesystem within it. - * - * @param {ManagedFilesystem} filesystem - * The filesystem to show within the filesystem menu. - */ - $scope.showFilesystemMenu = function showFilesystemMenu(filesystem) { - $scope.filesystemMenuContents = filesystem; - }; - - /** - * Returns whether the filesystem menu should be visible. - * - * @returns {Boolean} - * true if the filesystem menu is shown, false otherwise. - */ - $scope.isFilesystemMenuShown = function isFilesystemMenuShown() { - return !!$scope.filesystemMenuContents && $scope.menu.shown; - }; - - // Automatically refresh display when filesystem menu is shown - $scope.$watch('isFilesystemMenuShown()', function refreshFilesystem() { - - // Refresh filesystem, if defined - var filesystem = $scope.filesystemMenuContents; - if (filesystem) - ManagedFilesystem.refresh(filesystem, filesystem.currentDirectory); - - }); - - /** - * Returns the full path to the given file as an ordered array of parent - * directories. - * - * @param {ManagedFilesystem.File} file - * The file whose full path should be retrieved. - * - * @returns {ManagedFilesystem.File[]} - * An array of directories which make up the hierarchy containing the - * given file, in order of increasing depth. - */ - $scope.getPath = function getPath(file) { - - var path = []; - - // Add all files to path in ascending order of depth - while (file && file.parent) { - path.unshift(file); - file = file.parent; - } - - return path; - - }; - - /** - * Changes the current directory of the given filesystem to the given - * directory. - * - * @param {ManagedFilesystem} filesystem - * The filesystem whose current directory should be changed. - * - * @param {ManagedFilesystem.File} file - * The directory to change to. - */ - $scope.changeDirectory = function changeDirectory(filesystem, file) { - ManagedFilesystem.changeDirectory(filesystem, file); - }; - - /** - * Begins a file upload through the attached Guacamole client for - * each file in the given FileList. - * - * @param {FileList} files - * The files to upload. - */ - $scope.uploadFiles = function uploadFiles(files) { - - // Upload each file - for (var i = 0; i < files.length; i++) - ManagedClient.uploadFile($scope.filesystemMenuContents.client, files[i], $scope.filesystemMenuContents); - - }; - - /** - * Determines whether the attached client group has any associated file - * transfers, regardless of those file transfers' state. - * - * @returns {Boolean} - * true if there are any file transfers associated with the - * attached client group, false otherise. - */ - $scope.hasTransfers = function hasTransfers() { - - // There are no file transfers if there is no client group - if (!$scope.clientGroup) - return false; - - return _.findIndex($scope.clientGroup.clients, ManagedClient.hasTransfers) !== -1; - - }; - - /** - * Returns whether the current user can share the current connection with - * other users. A connection can be shared if and only if there is at least - * one associated sharing profile. - * - * @returns {Boolean} - * true if the current user can share the current connection with other - * users, false otherwise. - */ - $scope.canShareConnection = function canShareConnection() { - - // If there is at least one sharing profile, the connection can be shared - for (var dummy in $scope.sharingProfiles) - return true; - - // Otherwise, sharing is not possible - return false; - - }; - - // Clean up when view destroyed - $scope.$on('$destroy', function clientViewDestroyed() { - setAttachedGroup(null); - - // always unset fullscreen mode to not confuse user - guacFullscreen.setFullscreenMode(false); - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js b/guacamole/src/main/frontend/src/app/client/directives/guacClient.js deleted file mode 100644 index 18a1b080c5..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClient.js +++ /dev/null @@ -1,675 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for the guacamole client. - */ -angular.module('client').directive('guacClient', [function guacClient() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/client/templates/guacClient.html' - }; - - directive.scope = { - - /** - * The client to display within this guacClient directive. - * - * @type ManagedClient - */ - client : '=', - - /** - * Whether translation of touch to mouse events should emulate an - * absolute pointer device, or a relative pointer device. - * - * @type boolean - */ - emulateAbsoluteMouse : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacClientController($scope, $injector, $element) { - - // Required types - const ManagedClient = $injector.get('ManagedClient'); - - // Required services - const $rootScope = $injector.get('$rootScope'); - const $window = $injector.get('$window'); - - /** - * Whether the local, hardware mouse cursor is in use. - * - * @type Boolean - */ - let localCursor = false; - - /** - * The current Guacamole client instance. - * - * @type Guacamole.Client - */ - let client = null; - - /** - * The display of the current Guacamole client instance. - * - * @type Guacamole.Display - */ - let display = null; - - /** - * The element associated with the display of the current - * Guacamole client instance. - * - * @type Element - */ - let displayElement = null; - - /** - * The element which must contain the Guacamole display element. - * - * @type Element - */ - const displayContainer = $element.find('.display')[0]; - - /** - * The main containing element for the entire directive. - * - * @type Element - */ - const main = $element[0]; - - /** - * Guacamole mouse event object, wrapped around the main client - * display. - * - * @type Guacamole.Mouse - */ - const mouse = new Guacamole.Mouse(displayContainer); - - /** - * Guacamole absolute mouse emulation object, wrapped around the - * main client display. - * - * @type Guacamole.Mouse.Touchscreen - */ - const touchScreen = new Guacamole.Mouse.Touchscreen(displayContainer); - - /** - * Guacamole relative mouse emulation object, wrapped around the - * main client display. - * - * @type Guacamole.Mouse.Touchpad - */ - const touchPad = new Guacamole.Mouse.Touchpad(displayContainer); - - /** - * Guacamole touch event handling object, wrapped around the main - * client dislay. - * - * @type Guacamole.Touch - */ - const touch = new Guacamole.Touch(displayContainer); - - /** - * Updates the scale of the attached Guacamole.Client based on current window - * size and "auto-fit" setting. - */ - const updateDisplayScale = function updateDisplayScale() { - - if (!display) return; - - // Calculate scale to fit screen - $scope.client.clientProperties.minScale = Math.min( - main.offsetWidth / Math.max(display.getWidth(), 1), - main.offsetHeight / Math.max(display.getHeight(), 1) - ); - - // Calculate appropriate maximum zoom level - $scope.client.clientProperties.maxScale = Math.max($scope.client.clientProperties.minScale, 3); - - // Clamp zoom level, maintain auto-fit - if (display.getScale() < $scope.client.clientProperties.minScale || $scope.client.clientProperties.autoFit) - $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; - - else if (display.getScale() > $scope.client.clientProperties.maxScale) - $scope.client.clientProperties.scale = $scope.client.clientProperties.maxScale; - - }; - - /** - * Scrolls the client view such that the mouse cursor is visible. - * - * @param {Guacamole.Mouse.State} mouseState The current mouse - * state. - */ - const scrollToMouse = function scrollToMouse(mouseState) { - - // Determine mouse position within view - const mouse_view_x = mouseState.x + displayContainer.offsetLeft - main.scrollLeft; - const mouse_view_y = mouseState.y + displayContainer.offsetTop - main.scrollTop; - - // Determine viewport dimensions - const view_width = main.offsetWidth; - const view_height = main.offsetHeight; - - // Determine scroll amounts based on mouse position relative to document - - let scroll_amount_x; - if (mouse_view_x > view_width) - scroll_amount_x = mouse_view_x - view_width; - else if (mouse_view_x < 0) - scroll_amount_x = mouse_view_x; - else - scroll_amount_x = 0; - - let scroll_amount_y; - if (mouse_view_y > view_height) - scroll_amount_y = mouse_view_y - view_height; - else if (mouse_view_y < 0) - scroll_amount_y = mouse_view_y; - else - scroll_amount_y = 0; - - // Scroll (if necessary) to keep mouse on screen. - main.scrollLeft += scroll_amount_x; - main.scrollTop += scroll_amount_y; - - }; - - /** - * Return the name of the angular event associated with the provided - * mouse event. - * - * @param {Guacamole.Mouse.MouseEvent} event - * The mouse event to determine an angular event name for. - * - * @returns - * The name of the angular event associated with the provided - * mouse event. - */ - const getMouseEventName = event => { - switch (event.type) { - case 'mousedown': - return 'guacClientMouseDown'; - case 'mouseup': - return 'guacClientMouseUp'; - default: - return 'guacClientMouseMove'; - } - }; - - /** - * Handles a mouse event originating from the user's actual mouse. - * This differs from handleEmulatedMouseEvent() in that the - * software mouse cursor must be shown only if the user's browser - * does not support explicitly setting the hardware mouse cursor. - * - * @param {Guacamole.Mouse.MouseEvent} event - * The mouse event to handle. - */ - const handleMouseEvent = function handleMouseEvent(event) { - - // Do not attempt to handle mouse state changes if the client - // or display are not yet available - if (!client || !display) - return; - - event.stopPropagation(); - event.preventDefault(); - - // Send mouse state, show cursor if necessary - display.showCursor(!localCursor); - client.sendMouseState(event.state, true); - - // Broadcast the mouse event - $rootScope.$broadcast(getMouseEventName(event), event, client); - - }; - - /** - * Handles a mouse event originating from one of Guacamole's mouse - * emulation objects. This differs from handleMouseState() in that - * the software mouse cursor must always be shown (as the emulated - * mouse device will not have its own cursor). - * - * @param {Guacamole.Mouse.MouseEvent} event - * The mouse event to handle. - */ - const handleEmulatedMouseEvent = function handleEmulatedMouseEvent(event) { - - // Do not attempt to handle mouse state changes if the client - // or display are not yet available - if (!client || !display) - return; - - event.stopPropagation(); - event.preventDefault(); - - // Ensure software cursor is shown - display.showCursor(true); - - // Send mouse state, ensure cursor is visible - scrollToMouse(event.state); - client.sendMouseState(event.state, true); - - // Broadcast the mouse event - $rootScope.$broadcast(getMouseEventName(event), event, client); - - }; - - /** - * Return the name of the angular event associated with the provided - * touch event. - * - * @param {Guacamole.Touch.TouchEvent} event - * The touch event to determine an angular event name for. - * - * @returns - * The name of the angular event associated with the provided - * touch event. - */ - const getTouchEventName = event => { - switch (event.type) { - case 'touchstart': - return 'guacClientTouchStart'; - case 'touchend': - return 'guacClientTouchEnd'; - default: - return 'guacClientTouchMove'; - } - }; - - /** - * Handles a touch event originating from the user's device. - * - * @param {Guacamole.Touch.Event} touchEvent - * The touch event. - */ - const handleTouchEvent = function handleTouchEvent(event) { - - // Do not attempt to handle touch state changes if the client - // or display are not yet available - if (!client || !display) - return; - - event.preventDefault(); - - // Send touch state, hiding local cursor - display.showCursor(false); - client.sendTouchState(event.state, true); - - // Broadcast the touch event - $rootScope.$broadcast(getTouchEventName(event), event, client); - - }; - - // Attach any given managed client - $scope.$watch('client', function attachManagedClient(managedClient) { - - // Remove any existing display - displayContainer.innerHTML = ""; - - // Only proceed if a client is given - if (!managedClient) - return; - - // Get Guacamole client instance - client = managedClient.client; - - // Attach possibly new display - display = client.getDisplay(); - display.scale($scope.client.clientProperties.scale); - - // Add display element - displayElement = display.getElement(); - displayContainer.appendChild(displayElement); - - // Do nothing when the display element is clicked on - display.getElement().onclick = function(e) { - e.preventDefault(); - return false; - }; - - // Connect and update interface to match required size, deferring - // connecting until a future element resize if the main element - // size (desired display size) is not known and thus can't be sent - // during the handshake - $scope.mainElementResized(); - - }); - - // Update actual view scrollLeft when scroll properties change - $scope.$watch('client.clientProperties.scrollLeft', function scrollLeftChanged(scrollLeft) { - main.scrollLeft = scrollLeft; - $scope.client.clientProperties.scrollLeft = main.scrollLeft; - }); - - // Update actual view scrollTop when scroll properties change - $scope.$watch('client.clientProperties.scrollTop', function scrollTopChanged(scrollTop) { - main.scrollTop = scrollTop; - $scope.client.clientProperties.scrollTop = main.scrollTop; - }); - - // Update scale when display is resized - $scope.$watch('client.managedDisplay.size', function setDisplaySize() { - $scope.$evalAsync(updateDisplayScale); - }); - - // Keep local cursor up-to-date - $scope.$watch('client.managedDisplay.cursor', function setCursor(cursor) { - if (cursor) - localCursor = mouse.setCursor(cursor.canvas, cursor.x, cursor.y); - }); - - // Update touch event handling depending on remote multi-touch - // support and mouse emulation mode - $scope.$watchGroup([ - 'client.multiTouchSupport', - 'emulateAbsoluteMouse' - ], function touchBehaviorChanged() { - - // Clear existing event handling - touch.offEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent); - touchScreen.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent); - touchPad.offEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent); - - // Directly forward local touch events - if ($scope.client.multiTouchSupport) - touch.onEach(['touchstart', 'touchmove', 'touchend'], handleTouchEvent); - - // Switch to touchscreen if mouse emulation is required and - // absolute mouse emulation is preferred - else if ($scope.emulateAbsoluteMouse) - touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent); - - // Use touchpad for mouse emulation if absolute mouse emulation - // is not preferred - else - touchPad.onEach(['mousedown', 'mousemove', 'mouseup'], handleEmulatedMouseEvent); - - }); - - // Adjust scale if modified externally - $scope.$watch('client.clientProperties.scale', function changeScale(scale) { - - // Fix scale within limits - scale = Math.max(scale, $scope.client.clientProperties.minScale); - scale = Math.min(scale, $scope.client.clientProperties.maxScale); - - // If at minimum zoom level, hide scroll bars - if (scale === $scope.client.clientProperties.minScale) - main.style.overflow = "hidden"; - - // If not at minimum zoom level, show scroll bars - else - main.style.overflow = "auto"; - - // Apply scale if client attached - if (display) - display.scale(scale); - - if (scale !== $scope.client.clientProperties.scale) - $scope.client.clientProperties.scale = scale; - - }); - - // If autofit is set, the scale should be set to the minimum scale, filling the screen - $scope.$watch('client.clientProperties.autoFit', function changeAutoFit(autoFit) { - if(autoFit) - $scope.client.clientProperties.scale = $scope.client.clientProperties.minScale; - }); - - /** - * Sends the current size of the main element (the display container) - * to the Guacamole server, requesting that the remote display be - * resized. If the Guacamole client is not yet connected, it will be - * connected and the current size will sent through the initial - * handshake. If the size of the main element is not yet known, this - * function may need to be invoked multiple times until the size is - * known and the client may be connected. - */ - $scope.mainElementResized = function mainElementResized() { - - // Send new display size, if changed - if (client && display && main.offsetWidth && main.offsetHeight) { - - // Connect, if not already connected - ManagedClient.connect($scope.client, main.offsetWidth, main.offsetHeight); - - const pixelDensity = $window.devicePixelRatio || 1; - const width = main.offsetWidth * pixelDensity; - const height = main.offsetHeight * pixelDensity; - - if (display.getWidth() !== width || display.getHeight() !== height) - client.sendSize(width, height); - - } - - $scope.$evalAsync(updateDisplayScale); - - }; - - // Scroll client display if absolute mouse is in use (the same drag - // gesture is needed for moving the mouse pointer with relative mouse) - $scope.clientDrag = function clientDrag(inProgress, startX, startY, currentX, currentY, deltaX, deltaY) { - - if ($scope.emulateAbsoluteMouse) { - $scope.client.clientProperties.scrollLeft -= deltaX; - $scope.client.clientProperties.scrollTop -= deltaY; - } - - return false; - - }; - - /** - * If a pinch gesture is in progress, the scale of the client display when - * the pinch gesture began. - * - * @type Number - */ - let initialScale = null; - - /** - * If a pinch gesture is in progress, the X coordinate of the point on the - * client display that was centered within the pinch at the time the - * gesture began. - * - * @type Number - */ - let initialCenterX = 0; - - /** - * If a pinch gesture is in progress, the Y coordinate of the point on the - * client display that was centered within the pinch at the time the - * gesture began. - * - * @type Number - */ - let initialCenterY = 0; - - // Zoom and pan client via pinch gestures - $scope.clientPinch = function clientPinch(inProgress, startLength, currentLength, centerX, centerY) { - - // Do not handle pinch gestures if they would conflict with remote - // handling of similar gestures - if ($scope.client.multiTouchSupport > 1) - return false; - - // Do not handle pinch gestures while relative mouse is in use (2+ - // contact point gestures are used by relative mouse emulation to - // support right click, middle click, and scrolling) - if (!$scope.emulateAbsoluteMouse) - return false; - - // Stop gesture if not in progress - if (!inProgress) { - initialScale = null; - return false; - } - - // Set initial scale if gesture has just started - if (!initialScale) { - initialScale = $scope.client.clientProperties.scale; - initialCenterX = (centerX + $scope.client.clientProperties.scrollLeft) / initialScale; - initialCenterY = (centerY + $scope.client.clientProperties.scrollTop) / initialScale; - } - - // Determine new scale absolutely - let currentScale = initialScale * currentLength / startLength; - - // Fix scale within limits - scroll will be miscalculated otherwise - currentScale = Math.max(currentScale, $scope.client.clientProperties.minScale); - currentScale = Math.min(currentScale, $scope.client.clientProperties.maxScale); - - // Update scale based on pinch distance - $scope.client.clientProperties.autoFit = false; - $scope.client.clientProperties.scale = currentScale; - - // Scroll display to keep original pinch location centered within current pinch - $scope.client.clientProperties.scrollLeft = initialCenterX * currentScale - centerX; - $scope.client.clientProperties.scrollTop = initialCenterY * currentScale - centerY; - - return false; - - }; - - // Ensure focus is regained via mousedown before forwarding event - mouse.on('mousedown', document.body.focus.bind(document.body)); - - // Forward all mouse events - mouse.onEach(['mousedown', 'mousemove', 'mouseup'], handleMouseEvent); - - // Hide software cursor when mouse leaves display - mouse.on('mouseout', function() { - if (!display) return; - display.showCursor(false); - }); - - // Update remote clipboard if local clipboard changes - $scope.$on('guacClipboard', function onClipboard(event, data) { - ManagedClient.setClipboard($scope.client, data); - }); - - // Translate local keydown events to remote keydown events if keyboard is enabled - $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.focused) { - client.sendKeyEvent(1, keysym); - event.preventDefault(); - } - }); - - // Translate local keyup events to remote keyup events if keyboard is enabled - $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - if ($scope.client.clientProperties.focused) { - client.sendKeyEvent(0, keysym); - event.preventDefault(); - } - }); - - // Universally handle all synthetic keydown events - $scope.$on('guacSyntheticKeydown', function syntheticKeydownListener(event, keysym) { - if ($scope.client.clientProperties.focused) - client.sendKeyEvent(1, keysym); - }); - - // Universally handle all synthetic keyup events - $scope.$on('guacSyntheticKeyup', function syntheticKeyupListener(event, keysym) { - if ($scope.client.clientProperties.focused) - client.sendKeyEvent(0, keysym); - }); - - /** - * Whether a drag/drop operation is currently in progress (the user has - * dragged a file over the Guacamole connection but has not yet - * dropped it). - * - * @type boolean - */ - $scope.dropPending = false; - - /** - * Displays a visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the in-progress drag/drop operation. - */ - const notifyDragStart = function notifyDragStart(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = true; - }); - - }; - - /** - * Removes the visual indication that dropping the file currently - * being dragged is possible. Further propagation and default behavior - * of the given event is automatically prevented. - * - * @param {Event} e - * The event related to the end of the former drag/drop operation. - */ - const notifyDragEnd = function notifyDragEnd(e) { - - e.preventDefault(); - e.stopPropagation(); - - $scope.$apply(() => { - $scope.dropPending = false; - }); - - }; - - main.addEventListener('dragenter', notifyDragStart, false); - main.addEventListener('dragover', notifyDragStart, false); - main.addEventListener('dragleave', notifyDragEnd, false); - - // File drop event handler - main.addEventListener('drop', function(e) { - - notifyDragEnd(e); - - // Ignore file drops if no attached client - if (!$scope.client) - return; - - // Upload each file - const files = e.dataTransfer.files; - for (let i = 0; i < files.length; i++) - ManagedClient.uploadFile($scope.client, files[i]); - - }, false); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js deleted file mode 100644 index 03344b627f..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientNotification.js +++ /dev/null @@ -1,409 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for displaying a non-global notification describing the status - * of a specific Guacamole client, including prompts for any information - * necessary to continue the connection. - */ -angular.module('client').directive('guacClientNotification', [function guacClientNotification() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/client/templates/guacClientNotification.html' - }; - - directive.scope = { - - /** - * The client whose status should be displayed. - * - * @type ManagedClient - */ - client : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacClientNotificationController($scope, $injector, $element) { - - // Required types - const ManagedClient = $injector.get('ManagedClient'); - const ManagedClientState = $injector.get('ManagedClientState'); - const Protocol = $injector.get('Protocol'); - - // Required services - const $location = $injector.get('$location'); - const authenticationService = $injector.get('authenticationService'); - const guacClientManager = $injector.get('guacClientManager'); - const guacTranslate = $injector.get('guacTranslate'); - const requestService = $injector.get('requestService'); - const userPageService = $injector.get('userPageService'); - - /** - * A Notification object describing the client status to display as a - * dialog or prompt, as would be accepted by guacNotification.showStatus(), - * or false if no status should be shown. - * - * @type {Notification|Object|Boolean} - */ - $scope.status = false; - - /** - * All error codes for which automatic reconnection is appropriate when a - * client error occurs. - */ - const CLIENT_AUTO_RECONNECT = { - 0x0200: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0301: true, - 0x0308: true - }; - - /** - * All error codes for which automatic reconnection is appropriate when a - * tunnel error occurs. - */ - const TUNNEL_AUTO_RECONNECT = { - 0x0200: true, - 0x0202: true, - 0x0203: true, - 0x0207: true, - 0x0208: true, - 0x0308: true - }; - - /** - * Action which logs out from Guacamole entirely. - */ - const LOGOUT_ACTION = { - name : "CLIENT.ACTION_LOGOUT", - className : "logout button", - callback : function logoutCallback() { - authenticationService.logout() - ['catch'](requestService.IGNORE); - } - }; - - /** - * Action which returns the user to the home screen. If the home page has - * not yet been determined, this will be null. - */ - let NAVIGATE_HOME_ACTION = null; - - // Assign home page action once user's home page has been determined - userPageService.getHomePage() - .then(function homePageRetrieved(homePage) { - - // Define home action only if different from current location - if ($location.path() !== homePage.url) { - NAVIGATE_HOME_ACTION = { - name : "CLIENT.ACTION_NAVIGATE_HOME", - className : "home button", - callback : function navigateHomeCallback() { - $location.url(homePage.url); - } - }; - } - - }, requestService.WARN); - - /** - * Action which replaces the current client with a newly-connected client. - */ - const RECONNECT_ACTION = { - name : "CLIENT.ACTION_RECONNECT", - className : "reconnect button", - callback : function reconnectCallback() { - $scope.client = guacClientManager.replaceManagedClient($scope.client.id); - $scope.status = false; - } - }; - - /** - * The reconnect countdown to display if an error or status warrants an - * automatic, timed reconnect. - */ - const RECONNECT_COUNTDOWN = { - text: "CLIENT.TEXT_RECONNECT_COUNTDOWN", - callback: RECONNECT_ACTION.callback, - remaining: 15 - }; - - /** - * Displays a notification at the end of a Guacamole connection, whether - * that connection is ending normally or due to an error. As the end of - * a Guacamole connection may be due to changes in authentication status, - * this will also implicitly peform a re-authentication attempt to check - * for such changes, possibly resulting in auth-related events like - * guacInvalidCredentials. - * - * @param {Notification|Boolean|Object} status - * The status notification to show, as would be accepted by - * guacNotification.showStatus(). - */ - const notifyConnectionClosed = function notifyConnectionClosed(status) { - - // Re-authenticate to verify auth status at end of connection - authenticationService.updateCurrentToken($location.search()) - ['catch'](requestService.IGNORE) - - // Show the requested status once the authentication check has finished - ['finally'](function authenticationCheckComplete() { - $scope.status = status; - }); - - }; - - /** - * Notifies the user that the connection state has changed. - * - * @param {String} connectionState - * The current connection state, as defined by - * ManagedClientState.ConnectionState. - */ - const notifyConnectionState = function notifyConnectionState(connectionState) { - - // Hide any existing status - $scope.status = false; - - // Do not display status if status not known - if (!connectionState) - return; - - // Build array of available actions - let actions; - if (NAVIGATE_HOME_ACTION) - actions = [ NAVIGATE_HOME_ACTION, RECONNECT_ACTION, LOGOUT_ACTION ]; - else - actions = [ RECONNECT_ACTION, LOGOUT_ACTION ]; - - // Get any associated status code - const status = $scope.client.clientState.statusCode; - - // Connecting - if (connectionState === ManagedClientState.ConnectionState.CONNECTING - || connectionState === ManagedClientState.ConnectionState.WAITING) { - $scope.status = { - className : "connecting", - title: "CLIENT.DIALOG_HEADER_CONNECTING", - text: { - key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() - } - }; - } - - // Client error - else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { - - // Translation IDs for this error code - const errorPrefix = "CLIENT.ERROR_CLIENT_"; - const errorId = errorPrefix + status.toString(16).toUpperCase(); - const defaultErrorId = errorPrefix + "DEFAULT"; - - // Determine whether the reconnect countdown applies - const countdown = (status in CLIENT_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - - // Use the guacTranslate service to determine if there is a translation for - // this error code; if not, use the default - guacTranslate(errorId, defaultErrorId).then( - - // Show error status - translationResult => notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : translationResult.id - }, - countdown : countdown, - actions : actions - }) - ); - - } - - // Tunnel error - else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { - - // Translation IDs for this error code - const errorPrefix = "CLIENT.ERROR_TUNNEL_"; - const errorId = errorPrefix + status.toString(16).toUpperCase(); - const defaultErrorId = errorPrefix + "DEFAULT"; - - // Determine whether the reconnect countdown applies - const countdown = (status in TUNNEL_AUTO_RECONNECT) ? RECONNECT_COUNTDOWN : null; - - // Use the guacTranslate service to determine if there is a translation for - // this error code; if not, use the default - guacTranslate(errorId, defaultErrorId).then( - - // Show error status - translationResult => notifyConnectionClosed({ - className : "error", - title : "CLIENT.DIALOG_HEADER_CONNECTION_ERROR", - text : { - key : translationResult.id - }, - countdown : countdown, - actions : actions - }) - ); - - } - - // Disconnected - else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { - notifyConnectionClosed({ - title : "CLIENT.DIALOG_HEADER_DISCONNECTED", - text : { - key : "CLIENT.TEXT_CLIENT_STATUS_" + connectionState.toUpperCase() - }, - actions : actions - }); - } - - // Hide status for all other states - else - $scope.status = false; - - }; - - /** - * Prompts the user to enter additional connection parameters. If the - * protocol and associated parameters of the underlying connection are not - * yet known, this function has no effect and should be re-invoked once - * the parameters are known. - * - * @param {Object.} requiredParameters - * The set of all parameters requested by the server via "required" - * instructions, where each object key is the name of a requested - * parameter and each value is the current value entered by the user. - */ - const notifyParametersRequired = function notifyParametersRequired(requiredParameters) { - - /** - * Action which submits the current set of parameter values, requesting - * that the connection continue. - */ - const SUBMIT_PARAMETERS = { - name : "CLIENT.ACTION_CONTINUE", - className : "button", - callback : function submitParameters() { - if ($scope.client) { - const params = $scope.client.requiredParameters; - $scope.client.requiredParameters = null; - ManagedClient.sendArguments($scope.client, params); - } - } - }; - - /** - * Action which cancels submission of additional parameters and - * disconnects from the current connection. - */ - const CANCEL_PARAMETER_SUBMISSION = { - name : "CLIENT.ACTION_CANCEL", - className : "button", - callback : function cancelSubmission() { - $scope.client.requiredParameters = null; - $scope.client.client.disconnect(); - } - }; - - // Attempt to prompt for parameters only if the parameters that apply - // to the underlying connection are known - if (!$scope.client.protocol || !$scope.client.forms) - return; - - // Prompt for parameters - $scope.status = { - className : "parameters-required", - formNamespace : Protocol.getNamespace($scope.client.protocol), - forms : $scope.client.forms, - formModel : requiredParameters, - formSubmitCallback : SUBMIT_PARAMETERS.callback, - actions : [ SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION ] - }; - - }; - - /** - * Returns whether the given connection state allows for submission of - * connection parameters via "argv" instructions. - * - * @param {String} connectionState - * The connection state to test, as defined by - * ManagedClientState.ConnectionState. - * - * @returns {boolean} - * true if the given connection state allows submission of connection - * parameters via "argv" instructions, false otherwise. - */ - const canSubmitParameters = function canSubmitParameters(connectionState) { - return (connectionState === ManagedClientState.ConnectionState.WAITING || - connectionState === ManagedClientState.ConnectionState.CONNECTED); - }; - - // Show status dialog when connection status changes - $scope.$watchGroup([ - 'client.clientState.connectionState', - 'client.requiredParameters', - 'client.protocol', - 'client.forms' - ], function clientStateChanged(newValues) { - - const connectionState = newValues[0]; - const requiredParameters = newValues[1]; - - // Prompt for parameters only if parameters can actually be submitted - if (requiredParameters && canSubmitParameters(connectionState)) - notifyParametersRequired(requiredParameters); - - // Otherwise, just show general connection state - else - notifyConnectionState(connectionState); - - }); - - /** - * Prevents the default behavior of the given AngularJS event if a - * notification is currently shown and the client is focused. - * - * @param {event} e - * The AngularJS event to selectively prevent. - */ - const preventDefaultDuringNotification = function preventDefaultDuringNotification(e) { - if ($scope.status && $scope.client.clientProperties.focused) - e.preventDefault(); - }; - - // Block internal handling of key events (by the client) if a - // notification is visible - $scope.$on('guacBeforeKeydown', preventDefaultDuringNotification); - $scope.$on('guacBeforeKeyup', preventDefaultDuringNotification); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js deleted file mode 100644 index efd23932d5..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientPanel.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A toolbar/panel which displays a list of active Guacamole connections. The - * panel is fixed to the bottom-right corner of its container and can be - * manually hidden/exposed by the user. - */ -angular.module('client').directive('guacClientPanel', ['$injector', function guacClientPanel($injector) { - - // Required services - const guacClientManager = $injector.get('guacClientManager'); - const sessionStorageFactory = $injector.get('sessionStorageFactory'); - - // Required types - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - const ManagedClientState = $injector.get('ManagedClientState'); - - /** - * Getter/setter for the boolean flag controlling whether the client panel - * is currently hidden. This flag is maintained in session-local storage to - * allow the state of the panel to persist despite navigation within the - * same tab. When hidden, the panel will be collapsed against the right - * side of the container. By default, the panel is visible. - * - * @type Function - */ - var panelHidden = sessionStorageFactory.create(false); - - return { - // Element only - restrict: 'E', - replace: true, - scope: { - - /** - * The ManagedClientGroup instances associated with the active - * connections to be displayed within this panel. - * - * @type ManagedClientGroup[] - */ - clientGroups : '=' - - }, - templateUrl: 'app/client/templates/guacClientPanel.html', - controller: ['$scope', '$element', function guacClientPanelController($scope, $element) { - - /** - * The DOM element containing the scrollable portion of the client - * panel. - * - * @type Element - */ - var scrollableArea = $element.find('.client-panel-connection-list')[0]; - - /** - * On-scope reference to session-local storage of the flag - * controlling whether then panel is hidden. - */ - $scope.panelHidden = panelHidden; - - /** - * Returns whether this panel currently has any client groups - * associated with it. - * - * @return {Boolean} - * true if at least one client group is associated with this - * panel, false otherwise. - */ - $scope.hasClientGroups = function hasClientGroups() { - return $scope.clientGroups && $scope.clientGroups.length; - }; - - /** - * @borrows ManagedClientGroup.getIdentifier - */ - $scope.getIdentifier = ManagedClientGroup.getIdentifier; - - /** - * @borrows ManagedClientGroup.getTitle - */ - $scope.getTitle = ManagedClientGroup.getTitle; - - /** - * Returns whether the status of any client within the given client - * group has changed in a way that requires the user's attention. - * This may be due to an error, or due to a server-initiated - * disconnect. - * - * @param {ManagedClientGroup} clientGroup - * The client group to test. - * - * @returns {Boolean} - * true if the given client requires the user's attention, - * false otherwise. - */ - $scope.hasStatusUpdate = function hasStatusUpdate(clientGroup) { - return _.findIndex(clientGroup.clients, (client) => { - - // Test whether the client has encountered an error - switch (client.clientState.connectionState) { - case ManagedClientState.ConnectionState.CONNECTION_ERROR: - case ManagedClientState.ConnectionState.TUNNEL_ERROR: - case ManagedClientState.ConnectionState.DISCONNECTED: - return true; - } - - return false; - - }) !== -1; - }; - - /** - * Initiates an orderly disconnect of all clients within the given - * group. The clients are removed from management such that - * attempting to connect to any of the same connections will result - * in new connections being established, rather than displaying a - * notification that the connection has ended. - * - * @param {ManagedClientGroup} clientGroup - * The group of clients to disconnect. - */ - $scope.disconnect = function disconnect(clientGroup) { - guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup)); - }; - - /** - * Toggles whether the client panel is currently hidden. - */ - $scope.togglePanel = function togglePanel() { - panelHidden(!panelHidden()); - }; - - // Override vertical scrolling, scrolling horizontally instead - scrollableArea.addEventListener('wheel', function reorientVerticalScroll(e) { - - var deltaMultiplier = { - /* DOM_DELTA_PIXEL */ 0x00: 1, - /* DOM_DELTA_LINE */ 0x01: 15, - /* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth - }; - - if (e.deltaY) { - this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01)); - e.preventDefault(); - } - - }); - - }] - }; -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientUserCount.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientUserCount.js deleted file mode 100644 index 3c42fc3ea6..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientUserCount.js +++ /dev/null @@ -1,237 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive that displays a status indicator showing the number of users - * joined to a connection. The specific usernames of those users are visible in - * a tooltip on mouseover, and small notifications are displayed as users - * join/leave the connection. - */ -angular.module('client').directive('guacClientUserCount', [function guacClientUserCount() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/client/templates/guacClientUserCount.html' - }; - - directive.scope = { - - /** - * The client whose current users should be displayed. - * - * @type ManagedClient - */ - client : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacClientUserCountController($scope, $injector, $element) { - - // Required types - var AuthenticationResult = $injector.get('AuthenticationResult'); - - // Required services - var $translate = $injector.get('$translate'); - - /** - * The maximum number of messages displayed by this directive at any - * given time. Old messages will be discarded as necessary to ensure - * the number of messages displayed never exceeds this value. - * - * @constant - * @type number - */ - var MAX_MESSAGES = 3; - - /** - * The list that should contain any notifications regarding users - * joining or leaving the connection. - * - * @type HTMLUListElement - */ - var messages = $element.find('.client-user-count-messages')[0]; - - /** - * Map of the usernames of all users of the current connection to the - * number of concurrent connections those users have to the current - * connection. - * - * @type Object. - */ - $scope.userCounts = {}; - - /** - * Displays a message noting that a change related to a particular user - * of this connection has occurred. - * - * @param {!string} str - * The key of the translation string containing the message to - * display. This translation key must accept "USERNAME" as the - * name of the translation parameter containing the username of - * the user in question. - * - * @param {!string} username - * The username of the user in question. - */ - var notify = function notify(str, username) { - $translate(str, { 'USERNAME' : username }).then(function translationReady(text) { - - if (messages.childNodes.length === 3) - messages.removeChild(messages.lastChild); - - var message = document.createElement('li'); - message.className = 'client-user-count-message'; - message.textContent = text; - messages.insertBefore(message, messages.firstChild); - - // Automatically remove the notification after its "fadeout" - // animation ends. NOTE: This will not fire if the element is - // not visible at all. - message.addEventListener('animationend', function animationEnded() { - messages.removeChild(message); - }); - - }); - }; - - /** - * Displays a message noting that a particular user has joined the - * current connection. - * - * @param {!string} username - * The username of the user that joined. - */ - var notifyUserJoined = function notifyUserJoined(username) { - if ($scope.isAnonymous(username)) - notify('CLIENT.TEXT_ANONYMOUS_USER_JOINED', username); - else - notify('CLIENT.TEXT_USER_JOINED', username); - }; - - /** - * Displays a message noting that a particular user has left the - * current connection. - * - * @param {!string} username - * The username of the user that left. - */ - var notifyUserLeft = function notifyUserLeft(username) { - if ($scope.isAnonymous(username)) - notify('CLIENT.TEXT_ANONYMOUS_USER_LEFT', username); - else - notify('CLIENT.TEXT_USER_LEFT', username); - }; - - /** - * The ManagedClient attached to this directive at the time the - * notification update scope watch was last invoked. This is necessary - * as $scope.$watchGroup() does not allow for the callback to know - * whether the scope was previously uninitialized (it's "oldValues" - * parameter receives a copy of the new values if there are no old - * values). - * - * @type ManagedClient - */ - var oldClient = null; - - /** - * Returns whether the given username represents an anonymous user. - * - * @param {!string} username - * The username of the user to check. - * - * @returns {!boolean} - * true if the given username represents an anonymous user, false - * otherwise. - */ - $scope.isAnonymous = function isAnonymous(username) { - return username === AuthenticationResult.ANONYMOUS_USERNAME; - }; - - /** - * Returns the translation key of the translation string that should be - * used to render the number of connections a user with the given - * username has to the current connection. The appropriate string will - * vary by whether the user is anonymous. - * - * @param {!string} username - * The username of the user to check. - * - * @returns {!string} - * The translation key of the translation string that should be - * used to render the number of connections the user with the given - * username has to the current connection. - */ - $scope.getUserCountTranslationKey = function getUserCountTranslationKey(username) { - return $scope.isAnonymous(username) ? 'CLIENT.INFO_ANONYMOUS_USER_COUNT' : 'CLIENT.INFO_USER_COUNT'; - }; - - // Update visible notifications as users join/leave - $scope.$watchGroup([ 'client', 'client.userCount' ], function usersChanged() { - - // Resynchronize directive with state of any attached client when - // the client changes, to ensure notifications are only shown for - // future changes in users present - if (oldClient !== $scope.client) { - - $scope.userCounts = {}; - oldClient = $scope.client; - - angular.forEach($scope.client.users, function initUsers(connections, username) { - var count = Object.keys(connections).length; - $scope.userCounts[username] = count; - }); - - return; - - } - - // Display join/leave notifications for users who are currently - // connected but whose connection counts have changed - angular.forEach($scope.client.users, function addNewUsers(connections, username) { - - var count = Object.keys(connections).length; - var known = $scope.userCounts[username] || 0; - - if (count > known) - notifyUserJoined(username); - else if (count < known) - notifyUserLeft(username); - - $scope.userCounts[username] = count; - - }); - - // Display leave notifications for users who are no longer connected - angular.forEach($scope.userCounts, function removeOldUsers(count, username) { - if (!$scope.client.users[username]) { - notifyUserLeft(username); - delete $scope.userCounts[username]; - } - }); - - }); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacClientZoom.js b/guacamole/src/main/frontend/src/app/client/directives/guacClientZoom.js deleted file mode 100644 index 471ca19738..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacClientZoom.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for controlling the zoom level and scale-to-fit behavior of a - * a single Guacamole client. - */ -angular.module('client').directive('guacClientZoom', [function guacClientZoom() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/client/templates/guacClientZoom.html' - }; - - directive.scope = { - - /** - * The client to control the zoom/autofit of. - * - * @type ManagedClient - */ - client : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacClientZoomController($scope, $injector, $element) { - - /** - * Zooms in by 10%, automatically disabling autofit. - */ - $scope.zoomIn = function zoomIn() { - $scope.client.clientProperties.autoFit = false; - $scope.client.clientProperties.scale += 0.1; - }; - - /** - * Zooms out by 10%, automatically disabling autofit. - */ - $scope.zoomOut = function zoomOut() { - $scope.client.clientProperties.autoFit = false; - $scope.client.clientProperties.scale -= 0.1; - }; - - /** - * Resets the client autofit setting to false. - */ - $scope.clearAutoFit = function clearAutoFit() { - $scope.client.clientProperties.autoFit = false; - }; - - /** - * Notifies that the autofit setting has been manually changed by the - * user. - */ - $scope.autoFitChanged = function autoFitChanged() { - - // Reset to 100% scale when autofit is first disabled - if (!$scope.client.clientProperties.autoFit) - $scope.client.clientProperties.scale = 1; - - }; - - }]; - - return directive; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileBrowser.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileBrowser.js deleted file mode 100644 index f1daf3eb69..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacFileBrowser.js +++ /dev/null @@ -1,281 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays the contents of a filesystem received through the - * Guacamole client. - */ -angular.module('client').directive('guacFileBrowser', [function guacFileBrowser() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * @type ManagedFilesystem - */ - filesystem : '=' - - }, - - templateUrl: 'app/client/templates/guacFileBrowser.html', - controller: ['$scope', '$element', '$injector', function guacFileBrowserController($scope, $element, $injector) { - - // Required types - var ManagedFilesystem = $injector.get('ManagedFilesystem'); - - // Required services - var $interpolate = $injector.get('$interpolate'); - var $templateRequest = $injector.get('$templateRequest'); - - /** - * The jQuery-wrapped element representing the contents of the - * current directory within the file browser. - * - * @type Element[] - */ - var currentDirectoryContents = $element.find('.current-directory-contents'); - - /** - * Statically-cached template HTML used to render each file within - * a directory. Once available, this will be used through - * createFileElement() to generate the DOM elements which make up - * a directory listing. - * - * @type String - */ - var fileTemplate = null; - - /** - * Returns whether the given file is a normal file. - * - * @param {ManagedFilesystem.File} file - * The file to test. - * - * @returns {Boolean} - * true if the given file is a normal file, false otherwise. - */ - $scope.isNormalFile = function isNormalFile(file) { - return file.type === ManagedFilesystem.File.Type.NORMAL; - }; - - /** - * Returns whether the given file is a directory. - * - * @param {ManagedFilesystem.File} file - * The file to test. - * - * @returns {Boolean} - * true if the given file is a directory, false otherwise. - */ - $scope.isDirectory = function isDirectory(file) { - return file.type === ManagedFilesystem.File.Type.DIRECTORY; - }; - - /** - * Changes the currently-displayed directory to the given - * directory. - * - * @param {ManagedFilesystem.File} file - * The directory to change to. - */ - $scope.changeDirectory = function changeDirectory(file) { - ManagedFilesystem.changeDirectory($scope.filesystem, file); - }; - - /** - * Initiates a download of the given file. The progress of the - * download can be observed through guacFileTransferManager. - * - * @param {ManagedFilesystem.File} file - * The file to download. - */ - $scope.downloadFile = function downloadFile(file) { - ManagedFilesystem.downloadFile($scope.filesystem, file.streamName); - }; - - /** - * Recursively interpolates all text nodes within the DOM tree of - * the given element. All other node types, attributes, etc. will - * be left uninterpolated. - * - * @param {Element} element - * The element at the root of the DOM tree to be interpolated. - * - * @param {Object} context - * The evaluation context to use when evaluating expressions - * embedded in text nodes within the provided element. - */ - var interpolateElement = function interpolateElement(element, context) { - - // Interpolate the contents of text nodes directly - if (element.nodeType === Node.TEXT_NODE) - element.nodeValue = $interpolate(element.nodeValue)(context); - - // Recursively interpolate the contents of all descendant text - // nodes - if (element.hasChildNodes()) { - var children = element.childNodes; - for (var i = 0; i < children.length; i++) - interpolateElement(children[i], context); - } - - }; - - /** - * Creates a new element representing the given file and properly - * handling user events, bypassing the overhead incurred through - * use of ngRepeat and related techniques. - * - * Note that this function depends on the availability of the - * statically-cached fileTemplate. - * - * @param {ManagedFilesystem.File} file - * The file to generate an element for. - * - * @returns {Element[]} - * A jQuery-wrapped array containing a single DOM element - * representing the given file. - */ - var createFileElement = function createFileElement(file) { - - // Create from internal template - var element = angular.element(fileTemplate); - interpolateElement(element[0], file); - - // Double-clicking on unknown file types will do nothing - var fileAction = function doNothing() {}; - - // Change current directory when directories are clicked - if ($scope.isDirectory(file)) { - element.addClass('directory'); - fileAction = function changeDirectory() { - $scope.changeDirectory(file); - }; - } - - // Initiate downloads when normal files are clicked - else if ($scope.isNormalFile(file)) { - element.addClass('normal-file'); - fileAction = function downloadFile() { - $scope.downloadFile(file); - }; - } - - // Mark file as focused upon click - element.on('click', function handleFileClick() { - - // Fire file-specific action if already focused - if (element.hasClass('focused')) { - fileAction(); - element.removeClass('focused'); - } - - // Otherwise mark as focused - else { - element.parent().children().removeClass('focused'); - element.addClass('focused'); - } - - }); - - // Prevent text selection during navigation - element.on('selectstart', function avoidSelect(e) { - e.preventDefault(); - e.stopPropagation(); - }); - - return element; - - }; - - /** - * Sorts the given map of files, returning an array of those files - * grouped by file type (directories first, followed by non- - * directories) and sorted lexicographically. - * - * @param {Object.} files - * The map of files to sort. - * - * @returns {ManagedFilesystem.File[]} - * An array of all files in the given map, sorted - * lexicographically with directories first, followed by non- - * directories. - */ - var sortFiles = function sortFiles(files) { - - // Get all given files as an array - var unsortedFiles = []; - for (var name in files) - unsortedFiles.push(files[name]); - - // Sort files - directories first, followed by all other files - // sorted by name - return unsortedFiles.sort(function fileComparator(a, b) { - - // Directories come before non-directories - if ($scope.isDirectory(a) && !$scope.isDirectory(b)) - return -1; - - // Non-directories come after directories - if (!$scope.isDirectory(a) && $scope.isDirectory(b)) - return 1; - - // All other combinations are sorted by name - return a.name.localeCompare(b.name); - - }); - - }; - - // Watch directory contents once file template is available - $templateRequest('app/client/templates/file.html').then(function fileTemplateRetrieved(html) { - - // Store file template statically - fileTemplate = html; - - // Update the contents of the file browser whenever the current directory (or its contents) changes - $scope.$watch('filesystem.currentDirectory.files', function currentDirectoryChanged(files) { - - // Clear current content - currentDirectoryContents.html(''); - - // Display all files within current directory, sorted - angular.forEach(sortFiles(files), function displayFile(file) { - currentDirectoryContents.append(createFileElement(file)); - }); - - }); - - }, angular.noop); // end retrieve file template - - // Refresh file browser when any upload completes - $scope.$on('guacUploadComplete', function uploadComplete(event, filename) { - - // Refresh filesystem, if it exists - if ($scope.filesystem) - ManagedFilesystem.refresh($scope.filesystem, $scope.filesystem.currentDirectory); - - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js deleted file mode 100644 index d016a721c0..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransfer.js +++ /dev/null @@ -1,216 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Directive which displays an active file transfer, providing links for - * downloads, if applicable. - */ -angular.module('client').directive('guacFileTransfer', [function guacFileTransfer() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The file transfer to display. - * - * @type ManagedFileUpload|ManagedFileDownload - */ - transfer : '=' - - }, - - templateUrl: 'app/client/templates/guacFileTransfer.html', - controller: ['$scope', '$injector', function guacFileTransferController($scope, $injector) { - - // Required services - const guacTranslate = $injector.get('guacTranslate'); - - // Required types - var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); - - /** - * Returns the unit string that is most appropriate for the - * number of bytes transferred thus far - either 'gb', 'mb', 'kb', - * or 'b'. - * - * @returns {String} - * The unit string that is most appropriate for the number of - * bytes transferred thus far. - */ - $scope.getProgressUnit = function getProgressUnit() { - - var bytes = $scope.transfer.progress; - - // Gigabytes - if (bytes > 1000000000) - return 'gb'; - - // Megabytes - if (bytes > 1000000) - return 'mb'; - - // Kilobytes - if (bytes > 1000) - return 'kb'; - - // Bytes - return 'b'; - - }; - - /** - * Returns the amount of data transferred thus far, in the units - * returned by getProgressUnit(). - * - * @returns {Number} - * The amount of data transferred thus far, in the units - * returned by getProgressUnit(). - */ - $scope.getProgressValue = function getProgressValue() { - - var bytes = $scope.transfer.progress; - if (!bytes) - return bytes; - - // Convert bytes to necessary units - switch ($scope.getProgressUnit()) { - - // Gigabytes - case 'gb': - return (bytes / 1000000000).toFixed(1); - - // Megabytes - case 'mb': - return (bytes / 1000000).toFixed(1); - - // Kilobytes - case 'kb': - return (bytes / 1000).toFixed(1); - - // Bytes - case 'b': - default: - return bytes; - - } - - }; - - /** - * Returns the percentage of bytes transferred thus far, if the - * overall length of the file is known. - * - * @returns {Number} - * The percentage of bytes transferred thus far, if the - * overall length of the file is known. - */ - $scope.getPercentDone = function getPercentDone() { - return $scope.transfer.progress / $scope.transfer.length * 100; - }; - - /** - * Determines whether the associated file transfer is in progress. - * - * @returns {Boolean} - * true if the file transfer is in progress, false othherwise. - */ - $scope.isInProgress = function isInProgress() { - - // Not in progress if there is no transfer - if (!$scope.transfer) - return false; - - // Determine in-progress status based on stream state - switch ($scope.transfer.transferState.streamState) { - - // IDLE or OPEN file transfers are active - case ManagedFileTransferState.StreamState.IDLE: - case ManagedFileTransferState.StreamState.OPEN: - return true; - - // All others are not active - default: - return false; - - } - - }; - - /** - * Returns whether the file associated with this file transfer can - * be saved locally via a call to save(). - * - * @returns {Boolean} - * true if a call to save() will result in the file being - * saved, false otherwise. - */ - $scope.isSavable = function isSavable() { - return !!$scope.transfer.blob; - }; - - /** - * Saves the downloaded file, if any. If this transfer is an upload - * or the download is not yet complete, this function has no - * effect. - */ - $scope.save = function save() { - - // Ignore if no blob exists - if (!$scope.transfer.blob) - return; - - // Save file - saveAs($scope.transfer.blob, $scope.transfer.filename); - - }; - - /** - * Returns whether an error has occurred. If an error has occurred, - * the transfer is no longer active, and the text of the error can - * be read from getErrorText(). - * - * @returns {Boolean} - * true if an error has occurred during transfer, false - * otherwise. - */ - $scope.hasError = function hasError() { - return $scope.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; - }; - - // The translated error message for the current status code - $scope.translatedErrorMessage = ''; - - $scope.$watch('transfer.transferState.statusCode', function statusCodeChanged(statusCode) { - - // Determine translation name of error - const errorName = 'CLIENT.ERROR_UPLOAD_' + statusCode.toString(16).toUpperCase(); - - // Use translation string, or the default if no translation is found for this error code - guacTranslate(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').then( - translationResult => $scope.translatedErrorMessage = translationResult.message - ); - - }); - - }] // end file transfer controller - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransferManager.js b/guacamole/src/main/frontend/src/app/client/directives/guacFileTransferManager.js deleted file mode 100644 index 3a7c3a3efd..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacFileTransferManager.js +++ /dev/null @@ -1,105 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Directive which displays all active file transfers. - */ -angular.module('client').directive('guacFileTransferManager', [function guacFileTransferManager() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The client group whose file transfers should be managed by this - * directive. - * - * @type ManagedClientGroup - */ - clientGroup : '=' - - }, - - templateUrl: 'app/client/templates/guacFileTransferManager.html', - controller: ['$scope', '$injector', function guacFileTransferManagerController($scope, $injector) { - - // Required types - const ManagedClient = $injector.get('ManagedClient'); - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - const ManagedFileTransferState = $injector.get('ManagedFileTransferState'); - - /** - * Determines whether the given file transfer state indicates an - * in-progress transfer. - * - * @param {ManagedFileTransferState} transferState - * The file transfer state to check. - * - * @returns {Boolean} - * true if the given file transfer state indicates an in- - * progress transfer, false otherwise. - */ - var isInProgress = function isInProgress(transferState) { - switch (transferState.streamState) { - - // IDLE or OPEN file transfers are active - case ManagedFileTransferState.StreamState.IDLE: - case ManagedFileTransferState.StreamState.OPEN: - return true; - - // All others are not active - default: - return false; - - } - }; - - /** - * Removes all file transfers which are not currently in-progress. - */ - $scope.clearCompletedTransfers = function clearCompletedTransfers() { - - // Nothing to clear if no client group attached - if (!$scope.clientGroup) - return; - - // Remove completed uploads - $scope.clientGroup.clients.forEach(client => { - client.uploads = client.uploads.filter(function isUploadInProgress(upload) { - return isInProgress(upload.transferState); - }); - }); - - }; - - /** - * @borrows ManagedClientGroup.hasMultipleClients - */ - $scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients; - - /** - * @borrows ManagedClient.hasTransfers - */ - $scope.hasTransfers = ManagedClient.hasTransfers; - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacThumbnail.js b/guacamole/src/main/frontend/src/app/client/directives/guacThumbnail.js deleted file mode 100644 index 3b518e7df5..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacThumbnail.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for displaying a Guacamole client as a non-interactive - * thumbnail. - */ -angular.module('client').directive('guacThumbnail', [function guacThumbnail() { - - return { - // Element only - restrict: 'E', - replace: true, - scope: { - - /** - * The client to display within this guacThumbnail directive. - * - * @type ManagedClient - */ - client : '=' - - }, - templateUrl: 'app/client/templates/guacThumbnail.html', - controller: ['$scope', '$injector', '$element', function guacThumbnailController($scope, $injector, $element) { - - // Required services - var $window = $injector.get('$window'); - - /** - * The display of the current Guacamole client instance. - * - * @type Guacamole.Display - */ - var display = null; - - /** - * The element associated with the display of the current - * Guacamole client instance. - * - * @type Element - */ - var displayElement = null; - - /** - * The element which must contain the Guacamole display element. - * - * @type Element - */ - var displayContainer = $element.find('.display')[0]; - - /** - * The main containing element for the entire directive. - * - * @type Element - */ - var main = $element[0]; - - /** - * Updates the scale of the attached Guacamole.Client based on current window - * size and "auto-fit" setting. - */ - $scope.updateDisplayScale = function updateDisplayScale() { - - if (!display) return; - - // Fit within available area - display.scale(Math.min( - main.offsetWidth / Math.max(display.getWidth(), 1), - main.offsetHeight / Math.max(display.getHeight(), 1) - )); - - }; - - // Attach any given managed client - $scope.$watch('client', function attachManagedClient(managedClient) { - - // Remove any existing display - displayContainer.innerHTML = ""; - - // Only proceed if a client is given - if (!managedClient) - return; - - // Get Guacamole client instance - var client = managedClient.client; - - // Attach possibly new display - display = client.getDisplay(); - - // Add display element - displayElement = display.getElement(); - displayContainer.appendChild(displayElement); - - }); - - // Update scale when display is resized - $scope.$watch('client.managedDisplay.size', function setDisplaySize(size) { - $scope.$evalAsync($scope.updateDisplayScale); - }); - - }] - }; -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js deleted file mode 100644 index 52289bcd0a..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacTiledClients.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays one or more Guacamole clients in an evenly-tiled - * view. The number of rows and columns used for the arrangement of tiles is - * automatically determined by the number of clients present. - */ -angular.module('client').directive('guacTiledClients', [function guacTiledClients() { - - const directive = { - restrict: 'E', - templateUrl: 'app/client/templates/guacTiledClients.html', - }; - - directive.scope = { - - /** - * The function to invoke when the "close" button in the header of a - * client tile is clicked. The ManagedClient that is closed will be - * made available to the Angular expression defining the callback as - * "$client". - * - * @type function - */ - onClose : '&', - - /** - * The group of Guacamole clients that should be displayed in an - * evenly-tiled grid arrangement. - * - * @type ManagedClientGroup - */ - clientGroup : '=', - - /** - * Whether translation of touch to mouse events should emulate an - * absolute pointer device, or a relative pointer device. - * - * @type boolean - */ - emulateAbsoluteMouse : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacTiledClientsController($scope, $injector, $element) { - - // Required services - const $rootScope = $injector.get('$rootScope'); - - // Required types - const ManagedClient = $injector.get('ManagedClient'); - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - - /** - * Returns the currently-focused ManagedClient. If there is no such - * client, or multiple clients are focused, null is returned. - * - * @returns {ManagedClient} - * The currently-focused client, or null if there are no focused - * clients or if multiple clients are focused. - */ - $scope.getFocusedClient = function getFocusedClient() { - - const managedClientGroup = $scope.clientGroup; - if (managedClientGroup) { - const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused); - if (focusedClients.length === 1) - return focusedClients[0]; - } - - return null; - - }; - - // Notify whenever identify of currently-focused client changes - $scope.$watch('getFocusedClient()', function focusedClientChanged(focusedClient) { - $rootScope.$broadcast('guacClientFocused', focusedClient); - }); - - // Notify whenever arguments of currently-focused client changes - $scope.$watch('getFocusedClient().arguments', function focusedClientParametersChanged() { - $rootScope.$broadcast('guacClientArgumentsUpdated', $scope.getFocusedClient()); - }, true); - - // Notify whenever protocol of currently-focused client changes - $scope.$watch('getFocusedClient().protocol', function focusedClientParametersChanged() { - $rootScope.$broadcast('guacClientProtocolUpdated', $scope.getFocusedClient()); - }, true); - - /** - * Returns a callback for guacClick that assigns or updates keyboard - * focus to the given client, allowing that client to receive and - * handle keyboard events. Multiple clients may have keyboard focus - * simultaneously. - * - * @param {ManagedClient} client - * The client that should receive keyboard focus. - * - * @return {guacClick~callback} - * The callback that guacClient should invoke when the given client - * has been clicked. - */ - $scope.getFocusAssignmentCallback = function getFocusAssignmentCallback(client) { - return (shift, ctrl) => { - - // Clear focus of all other clients if not selecting multiple - if (!shift && !ctrl) { - $scope.clientGroup.clients.forEach(client => { - client.clientProperties.focused = false; - }); - } - - client.clientProperties.focused = true; - - // Fill in any gaps if performing rectangular multi-selection - // via shift-click - if (shift) { - - let minRow = $scope.clientGroup.rows - 1; - let minColumn = $scope.clientGroup.columns - 1; - let maxRow = 0; - let maxColumn = 0; - - // Determine extents of selected area - ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => { - if (client.clientProperties.focused) { - minRow = Math.min(minRow, row); - minColumn = Math.min(minColumn, column); - maxRow = Math.max(maxRow, row); - maxColumn = Math.max(maxColumn, column); - } - }); - - ManagedClientGroup.forEach($scope.clientGroup, (client, row, column) => { - client.clientProperties.focused = - row >= minRow - && row <= maxRow - && column >= minColumn - && column <= maxColumn; - }); - - } - - }; - }; - - /** - * @borrows ManagedClientGroup.hasMultipleClients - */ - $scope.hasMultipleClients = ManagedClientGroup.hasMultipleClients; - - /** - * @borrows ManagedClientGroup.getClientGrid - */ - $scope.getClientGrid = ManagedClientGroup.getClientGrid; - - /** - * @borrows ManagedClient.isShared - */ - $scope.isShared = ManagedClient.isShared; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js b/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js deleted file mode 100644 index e6417c93b8..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacTiledThumbnails.js +++ /dev/null @@ -1,77 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for displaying a group of Guacamole clients as a non-interactive - * thumbnail of tiled client displays. - */ -angular.module('client').directive('guacTiledThumbnails', [function guacTiledThumbnails() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/client/templates/guacTiledThumbnails.html' - }; - - directive.scope = { - - /** - * The group of clients to display as a thumbnail of tiled client - * displays. - * - * @type ManagedClientGroup - */ - clientGroup : '=' - - }; - - directive.controller = ['$scope', '$injector', '$element', - function guacTiledThumbnailsController($scope, $injector, $element) { - - // Required types - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - - /** - * The overall height of the thumbnail view of the tiled grid of - * clients within the client group, in pixels. This value is - * intentionally based off a snapshot of the current browser size at - * the time the directive comes into existence to ensure the contents - * of the thumbnail are familiar in appearance and aspect ratio. - */ - $scope.height = Math.min(window.innerHeight, 128); - - /** - * The overall width of the thumbnail view of the tiled grid of - * clients within the client group, in pixels. This value is - * intentionally based off a snapshot of the current browser size at - * the time the directive comes into existence to ensure the contents - * of the thumbnail are familiar in appearance and aspect ratio. - */ - $scope.width = window.innerWidth / window.innerHeight * $scope.height; - - /** - * @borrows ManagedClientGroup.getClientGrid - */ - $scope.getClientGrid = ManagedClientGroup.getClientGrid; - - }]; - - return directive; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacViewport.js b/guacamole/src/main/frontend/src/app/client/directives/guacViewport.js deleted file mode 100644 index d14a70a5e7..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacViewport.js +++ /dev/null @@ -1,112 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides a fullscreen environment for its content. - */ -angular.module('client').directive('guacViewport', [function guacViewport() { - - return { - // Element only - restrict: 'E', - scope: {}, - transclude: true, - templateUrl: 'app/client/templates/guacViewport.html', - controller: ['$scope', '$injector', '$element', - function guacViewportController($scope, $injector, $element) { - - // Required services - var $window = $injector.get('$window'); - - /** - * The fullscreen container element. - * - * @type Element - */ - var element = $element.find('.viewport')[0]; - - /** - * The width of the browser viewport when fitVisibleArea() was last - * invoked, in pixels, or null if fitVisibleArea() has not yet been - * called. - * - * @type Number - */ - var lastViewportWidth = null; - - /** - * The height of the browser viewport when fitVisibleArea() was - * last invoked, in pixels, or null if fitVisibleArea() has not yet - * been called. - * - * @type Number - */ - var lastViewportHeight = null; - - /** - * Resizes the container element inside the guacViewport such that - * it exactly fits within the visible area, even if the browser has - * been scrolled. - */ - var fitVisibleArea = function fitVisibleArea() { - - // Calculate viewport dimensions (this is NOT necessarily the - // same as 100vw and 100vh, 100%, etc., particularly when the - // on-screen keyboard of a mobile device pops open) - var viewportWidth = $window.innerWidth; - var viewportHeight = $window.innerHeight; - - // Adjust element width to fit exactly within visible area - if (viewportWidth !== lastViewportWidth) { - element.style.width = viewportWidth + 'px'; - lastViewportWidth = viewportWidth; - } - - // Adjust element height to fit exactly within visible area - if (viewportHeight !== lastViewportHeight) { - element.style.height = viewportHeight + 'px'; - lastViewportHeight = viewportHeight; - } - - // Scroll element such that its upper-left corner is exactly - // within the viewport upper-left corner, if not already there - if (element.scrollLeft || element.scrollTop) { - $window.scrollTo( - $window.pageXOffset + element.scrollLeft, - $window.pageYOffset + element.scrollTop - ); - } - - }; - - // Fit container within visible region when window scrolls - $window.addEventListener('scroll', fitVisibleArea); - - // Poll every 10ms, in case scroll event does not fire - var pollArea = $window.setInterval(fitVisibleArea, 10); - - // Clean up on destruction - $scope.$on('$destroy', function destroyViewport() { - $window.removeEventListener('scroll', fitVisibleArea); - $window.clearInterval(pollArea); - }); - - }] - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/client/directives/guacZoomCtrl.js b/guacamole/src/main/frontend/src/app/client/directives/guacZoomCtrl.js deleted file mode 100644 index 3e5b468da8..0000000000 --- a/guacamole/src/main/frontend/src/app/client/directives/guacZoomCtrl.js +++ /dev/null @@ -1,48 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which converts between human-readable zoom - * percentage and display scale. - */ -angular.module('client').directive('guacZoomCtrl', function guacZoomCtrl() { - return { - restrict: 'A', - require: 'ngModel', - priority: 101, - link: function(scope, element, attrs, ngModel) { - - // Evaluate the ngChange attribute when the model - // changes. - ngModel.$viewChangeListeners.push(function() { - scope.$eval(attrs.ngChange); - }); - - // When pushing to the menu, mutiply by 100. - ngModel.$formatters.push(function(value) { - return Math.round(value * 100); - }); - - // When parsing value from menu, divide by 100. - ngModel.$parsers.push(function(value) { - return Math.round(value) / 100; - }); - } - } -}); diff --git a/guacamole/src/main/frontend/src/app/client/services/guacImage.js b/guacamole/src/main/frontend/src/app/client/services/guacImage.js deleted file mode 100644 index 2cf15fbdeb..0000000000 --- a/guacamole/src/main/frontend/src/app/client/services/guacImage.js +++ /dev/null @@ -1,135 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for checking browser image support. - */ -angular.module('client').factory('guacImage', ['$injector', function guacImage($injector) { - - // Required services - var $q = $injector.get('$q'); - - var service = {}; - - /** - * Map of possibly-supported image mimetypes to corresponding test images - * encoded with base64. If the image is correctly decoded, it will be a - * single pixel (1x1) image. - * - * @type Object. - */ - var testImages = { - - /** - * Test JPEG image, encoded as base64. - */ - 'image/jpeg' : - '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH' - + 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME' - + 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' - + 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA' - + 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA' - + 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==', - - /** - * Test PNG image, encoded as base64. - */ - 'image/png' : - 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI' - + 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==', - - /** - * Test WebP image, encoded as base64. - */ - 'image/webp' : 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==' - - }; - - /** - * Deferred which tracks the progress and ultimate result of all pending - * image format tests. - * - * @type Deferred - */ - var deferredSupportedMimetypes = $q.defer(); - - /** - * Array of all promises associated with pending image tests. Each image - * test promise MUST be guaranteed to resolve and MUST NOT be rejected. - * - * @type Promise[] - */ - var pendingTests = []; - - /** - * The array of supported image formats. This will be gradually populated - * by the various image tests that occur in the background, and will not be - * fully populated until all promises within pendingTests are resolved. - * - * @type String[] - */ - var supported = []; - - /** - * Return a promise which resolves with to an array of image mimetypes - * supported by the browser, once those mimetypes are known. The returned - * promise is guaranteed to resolve successfully. - * - * @returns {Promise.} - * A promise which resolves with an array of image mimetypes supported - * by the browser. - */ - service.getSupportedMimetypes = function getSupportedMimetypes() { - return deferredSupportedMimetypes.promise; - }; - - // Test each possibly-supported image - angular.forEach(testImages, function testImageSupport(data, mimetype) { - - // Add promise for current image test - var imageTest = $q.defer(); - pendingTests.push(imageTest.promise); - - // Attempt to load image - var image = new Image(); - image.src = 'data:' + mimetype + ';base64,' + data; - - // Store as supported depending on whether load was successful - image.onload = image.onerror = function imageTestComplete() { - - // Image format is supported if successfully decoded - if (image.width === 1 && image.height === 1) - supported.push(mimetype); - - // Test is complete - imageTest.resolve(); - - }; - - }); - - // When all image tests are complete, resolve promise with list of - // supported formats - $q.all(pendingTests).then(function imageTestsCompleted() { - deferredSupportedMimetypes.resolve(supported); - }); - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js b/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js deleted file mode 100644 index c7fe8e9ffc..0000000000 --- a/guacamole/src/main/frontend/src/app/client/services/guacTranslate.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A wrapper around the angular-translate $translate service that offers a - * convenient way to fall back to a default translation if the requested - * translation is not available. - */ - angular.module('client').factory('guacTranslate', ['$injector', function guacTranslate($injector) { - - // Required services - const $q = $injector.get('$q'); - const $translate = $injector.get('$translate'); - - // Required types - const TranslationResult = $injector.get('TranslationResult'); - - /** - * Returns a promise that will be resolved with a TranslationResult containg either the - * requested ID and message (if translated), or the default ID and message if translated, - * or the literal value of `defaultTranslationId` for both the ID and message if neither - * is translated. - * - * @param {String} translationId - * The requested translation ID, which may or may not be translated. - * - * @param {Sting} defaultTranslationId - * The translation ID that will be used if no translation is found for `translationId`. - * - * @returns {Promise.} - * A promise which resolves with a TranslationResult containing the results from - * the translation attempt. - */ - var translateWithFallback = function translateWithFallback(translationId, defaultTranslationId) { - const deferredTranslation = $q.defer(); - - // Attempt to translate the requested translation ID - $translate(translationId).then( - - // If the requested translation is available, use that - translation => deferredTranslation.resolve(new TranslationResult({ - id: translationId, message: translation - })), - - // Otherwise, try the default translation ID - () => $translate(defaultTranslationId).then( - - // Default translation worked, so use that - defaultTranslation => - deferredTranslation.resolve(new TranslationResult({ - id: defaultTranslationId, message: defaultTranslation - })), - - // Neither translation is available; as a fallback, return default ID for both - () => deferredTranslation.resolve(new TranslationResult({ - id: defaultTranslationId, message: defaultTranslationId - })), - ) - ); - - return deferredTranslation.promise; - }; - - return translateWithFallback; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/templates/client.html b/guacamole/src/main/frontend/src/app/client/templates/client.html deleted file mode 100644 index 66dca3a770..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/client.html +++ /dev/null @@ -1,228 +0,0 @@ - - - - -
-
- - -
- - - - - -
- - -
- - -
- -
- - -
- -
- -
- -
-
- - -
- -
- - -
- {{'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | translate}} -
- - - - - - - -
diff --git a/guacamole/src/main/frontend/src/app/client/templates/connection.html b/guacamole/src/main/frontend/src/app/client/templates/connection.html deleted file mode 100644 index ae5ef91f14..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/connection.html +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html deleted file mode 100644 index 2e7d794cdc..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/connectionGroup.html +++ /dev/null @@ -1,10 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/client/templates/file.html b/guacamole/src/main/frontend/src/app/client/templates/file.html deleted file mode 100644 index 843047041b..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/file.html +++ /dev/null @@ -1,9 +0,0 @@ -
- - -
-
- {{::name}} -
- -
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClient.html b/guacamole/src/main/frontend/src/app/client/templates/guacClient.html deleted file mode 100644 index 46a78d6617..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClient.html +++ /dev/null @@ -1,17 +0,0 @@ -
- - -
- -
-
-
-
- -
- -
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html deleted file mode 100644 index 9948a2b1c2..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClientNotification.html +++ /dev/null @@ -1,5 +0,0 @@ -
- - - -
diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientPanel.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientPanel.html deleted file mode 100644 index 0705969aca..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClientPanel.html +++ /dev/null @@ -1,30 +0,0 @@ -
- - -
- - - - -
\ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientUserCount.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientUserCount.html deleted file mode 100644 index fa7eff911e..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClientUserCount.html +++ /dev/null @@ -1,11 +0,0 @@ -
- {{ client.userCount }} -
    -
      -
    • -
    -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacClientZoom.html b/guacamole/src/main/frontend/src/app/client/templates/guacClientZoom.html deleted file mode 100644 index d2e74101af..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacClientZoom.html +++ /dev/null @@ -1,18 +0,0 @@ -
    -
    -
    -
    -
    - % -
    -
    +
    -
    -
    - -
    -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileBrowser.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileBrowser.html deleted file mode 100644 index 70ea53dad7..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacFileBrowser.html +++ /dev/null @@ -1,6 +0,0 @@ -
    - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html deleted file mode 100644 index 32ead84e20..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransfer.html +++ /dev/null @@ -1,22 +0,0 @@ -
    - - -
    - - -
    -
    - {{transfer.filename}} -
    - - -

    {{translatedErrorMessage}}

    - -
    - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransferManager.html b/guacamole/src/main/frontend/src/app/client/templates/guacFileTransferManager.html deleted file mode 100644 index 6b546a26fc..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacFileTransferManager.html +++ /dev/null @@ -1,22 +0,0 @@ -
    - - -
    -

    {{'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | translate}}

    - -
    - - -
    -
    -

    {{ client.name }}

    -
    - - -
    -
    -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacThumbnail.html b/guacamole/src/main/frontend/src/app/client/templates/guacThumbnail.html deleted file mode 100644 index 931016eeea..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacThumbnail.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - - -
    -
    -
    -
    -
    -
    - -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html deleted file mode 100644 index ad24ca2927..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacTiledClients.html +++ /dev/null @@ -1,33 +0,0 @@ -
    -
    -
    - -
    -

    - - {{ client.title }} - - -

    - - - - - - - -
    - -
    -
    -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html b/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html deleted file mode 100644 index 010899c906..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacTiledThumbnails.html +++ /dev/null @@ -1,14 +0,0 @@ -
    -
    -
    - -
    - -
    - -
    -
    -
    diff --git a/guacamole/src/main/frontend/src/app/client/templates/guacViewport.html b/guacamole/src/main/frontend/src/app/client/templates/guacViewport.html deleted file mode 100644 index 28081ec7fb..0000000000 --- a/guacamole/src/main/frontend/src/app/client/templates/guacViewport.html +++ /dev/null @@ -1,2 +0,0 @@ -
    -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js b/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js deleted file mode 100644 index 0087c8e1c1..0000000000 --- a/guacamole/src/main/frontend/src/app/client/types/ClientProperties.js +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for generating new guacClient properties objects. - */ -angular.module('client').factory('ClientProperties', ['$injector', function defineClientProperties($injector) { - - /** - * Object used for interacting with a guacClient directive. - * - * @constructor - * @param {ClientProperties|Object} [template={}] - * The object whose properties should be copied within the new - * ClientProperties. - */ - var ClientProperties = function ClientProperties(template) { - - // Use empty object by default - template = template || {}; - - /** - * Whether the display should be scaled automatically to fit within the - * available space. - * - * @type Boolean - */ - this.autoFit = template.autoFit || true; - - /** - * The current scale. If autoFit is true, the effect of setting this - * value is undefined. - * - * @type Number - */ - this.scale = template.scale || 1; - - /** - * The minimum scale value. - * - * @type Number - */ - this.minScale = template.minScale || 1; - - /** - * The maximum scale value. - * - * @type Number - */ - this.maxScale = template.maxScale || 3; - - /** - * Whether this client should receive keyboard events. - * - * @type Boolean - */ - this.focused = template.focused || false; - - /** - * The relative Y coordinate of the scroll offset of the display within - * the client element. - * - * @type Number - */ - this.scrollTop = template.scrollTop || 0; - - /** - * The relative X coordinate of the scroll offset of the display within - * the client element. - * - * @type Number - */ - this.scrollLeft = template.scrollLeft || 0; - - }; - - return ClientProperties; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js b/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js deleted file mode 100644 index b7e83a3a05..0000000000 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClient.js +++ /dev/null @@ -1,1147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global Guacamole, _ */ - -/** - * Provides the ManagedClient class used by the guacClientManager service. - */ -angular.module('client').factory('ManagedClient', ['$rootScope', '$injector', - function defineManagedClient($rootScope, $injector) { - - // Required types - const ClientProperties = $injector.get('ClientProperties'); - const ClientIdentifier = $injector.get('ClientIdentifier'); - const ClipboardData = $injector.get('ClipboardData'); - const ManagedArgument = $injector.get('ManagedArgument'); - const ManagedClientState = $injector.get('ManagedClientState'); - const ManagedClientThumbnail = $injector.get('ManagedClientThumbnail'); - const ManagedDisplay = $injector.get('ManagedDisplay'); - const ManagedFilesystem = $injector.get('ManagedFilesystem'); - const ManagedFileUpload = $injector.get('ManagedFileUpload'); - const ManagedShareLink = $injector.get('ManagedShareLink'); - - // Required services - const $document = $injector.get('$document'); - const $q = $injector.get('$q'); - const $window = $injector.get('$window'); - const activeConnectionService = $injector.get('activeConnectionService'); - const authenticationService = $injector.get('authenticationService'); - const clipboardService = $injector.get('clipboardService'); - const connectionGroupService = $injector.get('connectionGroupService'); - const connectionService = $injector.get('connectionService'); - const preferenceService = $injector.get('preferenceService'); - const requestService = $injector.get('requestService'); - const tunnelService = $injector.get('tunnelService'); - const guacAudio = $injector.get('guacAudio'); - const guacHistory = $injector.get('guacHistory'); - const guacImage = $injector.get('guacImage'); - const guacVideo = $injector.get('guacVideo'); - - /** - * The minimum amount of time to wait between updates to the client - * thumbnail, in milliseconds. - * - * @type Number - */ - var THUMBNAIL_UPDATE_FREQUENCY = 5000; - - /** - * A deferred pipe stream, that has yet to be consumed, as well as all - * axuilary information needed to pull data from the stream. - * - * @constructor - * @param {DeferredPipeStream|Object} [template={}] - * The object whose properties should be copied within the new - * DeferredPipeStream. - */ - var DeferredPipeStream = function DeferredPipeStream(template) { - - // Use empty object by default - template = template || {}; - - /** - * The stream that will receive data from the server. - * - * @type Guacamole.InputStream - */ - this.stream = template.stream; - - /** - * The mimetype of the data which will be received. - * - * @type String - */ - this.mimetype = template.mimetype; - - /** - * The name of the pipe. - * - * @type String - */ - this.name = template.name; - - }; - - /** - * Object which serves as a surrogate interface, encapsulating a Guacamole - * client while it is active, allowing it to be maintained in the - * background. One or more ManagedClients are grouped within - * ManagedClientGroups before being attached to the client view. - * - * @constructor - * @param {ManagedClient|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedClient. - */ - var ManagedClient = function ManagedClient(template) { - - // Use empty object by default - template = template || {}; - - /** - * The ID of the connection associated with this client. - * - * @type String - */ - this.id = template.id; - - /** - * The actual underlying Guacamole client. - * - * @type Guacamole.Client - */ - this.client = template.client; - - /** - * The tunnel being used by the underlying Guacamole client. - * - * @type Guacamole.Tunnel - */ - this.tunnel = template.tunnel; - - /** - * The display associated with the underlying Guacamole client. - * - * @type ManagedDisplay - */ - this.managedDisplay = template.managedDisplay; - - /** - * The name returned associated with the connection or connection - * group in use. - * - * @type String - */ - this.name = template.name; - - /** - * The title which should be displayed as the page title for this - * client. - * - * @type String - */ - this.title = template.title; - - /** - * The name which uniquely identifies the protocol of the connection in - * use. If the protocol cannot be determined, such as when a connection - * group is in use, this will be null. - * - * @type {String} - */ - this.protocol = template.protocol || null; - - /** - * An array of forms describing all known parameters for the connection - * in use, including those which may not be editable. - * - * @type {Form[]} - */ - this.forms = template.forms || []; - - /** - * The most recently-generated thumbnail for this connection, as - * stored within the local connection history. If no thumbnail is - * stored, this will be null. - * - * @type ManagedClientThumbnail - */ - this.thumbnail = template.thumbnail; - - /** - * The current state of all parameters requested by the server via - * "required" instructions, where each object key is the name of a - * requested parameter and each value is the current value entered by - * the user or null if no parameters are currently being requested. - * - * @type Object. - */ - this.requiredParameters = null; - - /** - * All uploaded files. As files are uploaded, their progress can be - * observed through the elements of this array. It is intended that - * this array be manipulated externally as needed. - * - * @type ManagedFileUpload[] - */ - this.uploads = template.uploads || []; - - /** - * All currently-exposed filesystems. When the Guacamole server exposes - * a filesystem object, that object will be made available as a - * ManagedFilesystem within this array. - * - * @type ManagedFilesystem[] - */ - this.filesystems = template.filesystems || []; - - /** - * The current number of users sharing this connection, excluding the - * user that originally started the connection. Duplicate connections - * from the same user are included in this total. - */ - this.userCount = template.userCount || 0; - - /** - * All users currently sharing this connection, excluding the user that - * originally started the connection. If the connection is not shared, - * this object will be empty. This map consists of key/value pairs - * where each key is the user's username and each value is an object - * tracking the unique connections currently used by that user (a map - * of Guacamole protocol user IDs to boolean values). - * - * @type Object.> - */ - this.users = template.users || {}; - - /** - * All available share links generated for the this ManagedClient via - * ManagedClient.createShareLink(). Each resulting share link is stored - * under the identifier of its corresponding SharingProfile. - * - * @type Object. - */ - this.shareLinks = template.shareLinks || {}; - - /** - * The number of simultaneous touch contacts supported by the remote - * desktop. Unless explicitly declared otherwise by the remote desktop - * after connecting, this will be 0 (multi-touch unsupported). - * - * @type Number - */ - this.multiTouchSupport = template.multiTouchSupport || 0; - - /** - * The current state of the Guacamole client (idle, connecting, - * connected, terminated with error, etc.). - * - * @type ManagedClientState - */ - this.clientState = template.clientState || new ManagedClientState(); - - /** - * Properties associated with the display and behavior of the Guacamole - * client. - * - * @type ClientProperties - */ - this.clientProperties = template.clientProperties || new ClientProperties(); - - /** - * All editable arguments (connection parameters), stored by their - * names. Arguments will only be present within this set if their - * current values have been exposed by the server via an inbound "argv" - * stream and the server has confirmed that the value may be changed - * through a successful "ack" to an outbound "argv" stream. - * - * @type {Object.} - */ - this.arguments = template.arguments || {}; - - /** - * Any received pipe streams that have not been consumed by an onpipe - * handler or registered pipe handler, indexed by pipe stream name. - * - * @type {Object.} - */ - this.deferredPipeStreams = template.deferredPipeStreams || {}; - - /** - * Handlers for deferred pipe streams, indexed by the name of the pipe - * stream that the handler should handle. - * - * @type {Object.} - */ - this.deferredPipeStreamHandlers = template.deferredPipeStreamHandlers || {}; - - }; - - /** - * The mimetype of audio data to be sent along the Guacamole connection if - * audio input is supported. - * - * @constant - * @type String - */ - ManagedClient.AUDIO_INPUT_MIMETYPE = 'audio/L16;rate=44100,channels=2'; - - /** - * Returns a promise which resolves with the string of connection - * parameters to be passed to the Guacamole client during connection. This - * string generally contains the desired connection ID, display resolution, - * and supported audio/video/image formats. The returned promise is - * guaranteed to resolve successfully. - * - * @param {ClientIdentifier} identifier - * The identifier representing the connection or group to connect to. - * - * @param {number} [width] - * The optimal display width, in local CSS pixels. If omitted, the - * browser window width will be used. - * - * @param {number} [height] - * The optimal display height, in local CSS pixels. If omitted, the - * browser window height will be used. - * - * @returns {Promise.} - * A promise which resolves with the string of connection parameters to - * be passed to the Guacamole client, once the string is ready. - */ - const getConnectString = function getConnectString(identifier, width, height) { - - const deferred = $q.defer(); - - // Calculate optimal width/height for display - const pixel_density = $window.devicePixelRatio || 1; - const optimal_dpi = pixel_density * 96; - const optimal_width = width * pixel_density; - const optimal_height = height * pixel_density; - - // Build base connect string - let connectString = - "token=" + encodeURIComponent(authenticationService.getCurrentToken()) - + "&GUAC_DATA_SOURCE=" + encodeURIComponent(identifier.dataSource) - + "&GUAC_ID=" + encodeURIComponent(identifier.id) - + "&GUAC_TYPE=" + encodeURIComponent(identifier.type) - + "&GUAC_WIDTH=" + Math.floor(optimal_width) - + "&GUAC_HEIGHT=" + Math.floor(optimal_height) - + "&GUAC_DPI=" + Math.floor(optimal_dpi) - + "&GUAC_TIMEZONE=" + encodeURIComponent(preferenceService.preferences.timezone); - - // Add audio mimetypes to connect string - guacAudio.supported.forEach(function(mimetype) { - connectString += "&GUAC_AUDIO=" + encodeURIComponent(mimetype); - }); - - // Add video mimetypes to connect string - guacVideo.supported.forEach(function(mimetype) { - connectString += "&GUAC_VIDEO=" + encodeURIComponent(mimetype); - }); - - // Add image mimetypes to connect string - guacImage.getSupportedMimetypes().then(function supportedMimetypesKnown(mimetypes) { - - // Add each image mimetype - angular.forEach(mimetypes, function addImageMimetype(mimetype) { - connectString += "&GUAC_IMAGE=" + encodeURIComponent(mimetype); - }); - - // Connect string is now ready - nothing else is deferred - deferred.resolve(connectString); - - }); - - return deferred.promise; - - }; - - /** - * Requests the creation of a new audio stream, recorded from the user's - * local audio input device. If audio input is supported by the connection, - * an audio stream will be created which will remain open until the remote - * desktop requests that it be closed. If the audio stream is successfully - * created but is later closed, a new audio stream will automatically be - * established to take its place. The mimetype used for all audio streams - * produced by this function is defined by - * ManagedClient.AUDIO_INPUT_MIMETYPE. - * - * @param {Guacamole.Client} client - * The Guacamole.Client for which the audio stream is being requested. - */ - var requestAudioStream = function requestAudioStream(client) { - - // Create new audio stream, associating it with an AudioRecorder - var stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE); - var recorder = Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE); - - // If creation of the AudioRecorder failed, simply end the stream - if (!recorder) - stream.sendEnd(); - - // Otherwise, ensure that another audio stream is created after this - // audio stream is closed - else - recorder.onclose = requestAudioStream.bind(this, client); - - }; - - /** - * Creates a new ManagedClient representing the specified connection or - * connection group. The ManagedClient will not initially be connected, - * and must be explicitly connected by invoking ManagedClient.connect(). - * - * @param {String} id - * The ID of the connection or group to connect to. This String must be - * a valid ClientIdentifier string, as would be generated by - * ClientIdentifier.toString(). - * - * @returns {ManagedClient} - * A new ManagedClient instance which represents the connection or - * connection group having the given ID. - */ - ManagedClient.getInstance = function getInstance(id) { - - var tunnel; - - // If WebSocket available, try to use it. - if ($window.WebSocket) - tunnel = new Guacamole.ChainedTunnel( - new Guacamole.WebSocketTunnel('websocket-tunnel'), - new Guacamole.HTTPTunnel('tunnel') - ); - - // If no WebSocket, then use HTTP. - else - tunnel = new Guacamole.HTTPTunnel('tunnel'); - - // Get new client instance - var client = new Guacamole.Client(tunnel); - - // Associate new managed client with new client and tunnel - var managedClient = new ManagedClient({ - id : id, - client : client, - tunnel : tunnel - }); - - // Fire events for tunnel errors - tunnel.onerror = function tunnelError(status) { - $rootScope.$apply(function handleTunnelError() { - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.TUNNEL_ERROR, - status.code); - }); - }; - - // Pull protocol-specific information from tunnel once tunnel UUID is - // known - tunnel.onuuid = function tunnelAssignedUUID(uuid) { - tunnelService.getProtocol(uuid).then(function protocolRetrieved(protocol) { - managedClient.protocol = protocol.name; - managedClient.forms = protocol.connectionForms; - }, requestService.WARN); - }; - - // Update connection state as tunnel state changes - tunnel.onstatechange = function tunnelStateChanged(state) { - $rootScope.$evalAsync(function updateTunnelState() { - - switch (state) { - - // Connection is being established - case Guacamole.Tunnel.State.CONNECTING: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.CONNECTING); - break; - - // Connection is established / no longer unstable - case Guacamole.Tunnel.State.OPEN: - ManagedClientState.setTunnelUnstable(managedClient.clientState, false); - break; - - // Connection is established but misbehaving - case Guacamole.Tunnel.State.UNSTABLE: - ManagedClientState.setTunnelUnstable(managedClient.clientState, true); - break; - - // Connection has closed - case Guacamole.Tunnel.State.CLOSED: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.DISCONNECTED); - break; - - } - - }); - }; - - // Update connection state as client state changes - client.onstatechange = function clientStateChanged(clientState) { - $rootScope.$evalAsync(function updateClientState() { - - switch (clientState) { - - // Idle - case Guacamole.Client.State.IDLE: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.IDLE); - break; - - // Connecting - case Guacamole.Client.State.CONNECTING: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.CONNECTING); - break; - - // Connected + waiting - case Guacamole.Client.State.WAITING: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.WAITING); - break; - - // Connected - case Guacamole.Client.State.CONNECTED: - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.CONNECTED); - - // Sync current clipboard data - clipboardService.getClipboard().then((data) => { - ManagedClient.setClipboard(managedClient, data); - }, angular.noop); - - // Begin streaming audio input if possible - requestAudioStream(client); - - // Update thumbnail with initial display contents - ManagedClient.updateThumbnail(managedClient); - break; - - // Update history during disconnect phases - case Guacamole.Client.State.DISCONNECTING: - case Guacamole.Client.State.DISCONNECTED: - ManagedClient.updateThumbnail(managedClient); - break; - - } - - }); - }; - - // Disconnect and update status when the client receives an error - client.onerror = function clientError(status) { - $rootScope.$apply(function handleClientError() { - - // Disconnect, if connected - client.disconnect(); - - // Update state - ManagedClientState.setConnectionState(managedClient.clientState, - ManagedClientState.ConnectionState.CLIENT_ERROR, - status.code); - - }); - }; - - // Update user count when a new user joins - client.onjoin = function userJoined(id, username) { - $rootScope.$apply(function usersChanged() { - - var connections = managedClient.users[username] || {}; - managedClient.users[username] = connections; - - managedClient.userCount++; - connections[id] = true; - - }); - }; - - // Update user count when a user leaves - client.onleave = function userLeft(id, username) { - $rootScope.$apply(function usersChanged() { - - var connections = managedClient.users[username] || {}; - managedClient.users[username] = connections; - - managedClient.userCount--; - delete connections[id]; - - // Delete user entry after no connections remain - if (_.isEmpty(connections)) - delete managedClient.users[username]; - - }); - }; - - // Automatically update the client thumbnail - client.onsync = function syncReceived() { - - var thumbnail = managedClient.thumbnail; - var timestamp = new Date().getTime(); - - // Update thumbnail if it doesn't exist or is old - if (!thumbnail || timestamp - thumbnail.timestamp >= THUMBNAIL_UPDATE_FREQUENCY) { - $rootScope.$apply(function updateClientThumbnail() { - ManagedClient.updateThumbnail(managedClient); - }); - } - - }; - - // A default onpipe implementation that will automatically defer any - // received pipe streams, automatically invoking any registered handlers - // that may already be set for the received name - client.onpipe = (stream, mimetype, name) => { - - // Defer the pipe stream - managedClient.deferredPipeStreams[name] = new DeferredPipeStream( - { stream, mimetype, name }); - - // Invoke the handler now, if set - const handler = managedClient.deferredPipeStreamHandlers[name]; - if (handler) { - - // Handle the stream, and clear from the deferred streams - handler(stream, mimetype, name); - delete managedClient.deferredPipeStreams[name]; - } - }; - - // Test for argument mutability whenever an argument value is - // received - client.onargv = function clientArgumentValueReceived(stream, mimetype, name) { - - // Ignore arguments which do not use a mimetype currently supported - // by the web application - if (mimetype !== 'text/plain') - return; - - var reader = new Guacamole.StringReader(stream); - - // Assemble received data into a single string - var value = ''; - reader.ontext = function textReceived(text) { - value += text; - }; - - // Test mutability once stream is finished, storing the current - // value for the argument only if it is mutable - reader.onend = function textComplete() { - ManagedArgument.getInstance(managedClient, name, value).then(function argumentIsMutable(argument) { - managedClient.arguments[name] = argument; - }, function ignoreImmutableArguments() {}); - }; - - }; - - // Handle any received clipboard data - client.onclipboard = function clientClipboardReceived(stream, mimetype) { - - var reader; - - // If the received data is text, read it as a simple string - if (/^text\//.exec(mimetype)) { - - reader = new Guacamole.StringReader(stream); - - // Assemble received data into a single string - var data = ''; - reader.ontext = function textReceived(text) { - data += text; - }; - - // Set clipboard contents once stream is finished - reader.onend = function textComplete() { - clipboardService.setClipboard(new ClipboardData({ - source : managedClient.id, - type : mimetype, - data : data - }))['catch'](angular.noop); - }; - - } - - // Otherwise read the clipboard data as a Blob - else { - reader = new Guacamole.BlobReader(stream, mimetype); - reader.onend = function blobComplete() { - clipboardService.setClipboard(new ClipboardData({ - source : managedClient.id, - type : mimetype, - data : reader.getBlob() - }))['catch'](angular.noop); - }; - } - - }; - - // Update level of multi-touch support when known - client.onmultitouch = function multiTouchSupportDeclared(layer, touches) { - managedClient.multiTouchSupport = touches; - }; - - // Update title when a "name" instruction is received - client.onname = function clientNameReceived(name) { - $rootScope.$apply(function updateClientTitle() { - managedClient.title = name; - }); - }; - - // Handle any received files - client.onfile = function clientFileReceived(stream, mimetype, filename) { - tunnelService.downloadStream(tunnel.uuid, stream, mimetype, filename); - }; - - // Handle any received filesystem objects - client.onfilesystem = function fileSystemReceived(object, name) { - $rootScope.$apply(function exposeFilesystem() { - managedClient.filesystems.push(ManagedFilesystem.getInstance(managedClient, object, name)); - }); - }; - - // Handle any received prompts - client.onrequired = function onrequired(parameters) { - $rootScope.$apply(function promptUser() { - managedClient.requiredParameters = {}; - angular.forEach(parameters, function populateParameter(name) { - managedClient.requiredParameters[name] = ''; - }); - }); - }; - - // Manage the client display - managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); - - // Parse connection details from ID - var clientIdentifier = ClientIdentifier.fromString(id); - - // Defer actually connecting the Guacamole client until - // ManagedClient.connect() is explicitly invoked - - // If using a connection, pull connection name and protocol information - if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { - connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id) - .then(function connectionRetrieved(connection) { - managedClient.name = managedClient.title = connection.name; - }, requestService.WARN); - } - - // If using a connection group, pull connection name - else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) { - connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id) - .then(function connectionGroupRetrieved(group) { - managedClient.name = managedClient.title = group.name; - }, requestService.WARN); - } - - // If using an active connection, pull corresponding connection, then - // pull connection name and protocol information from that - else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) { - activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id) - .then(function activeConnectionRetrieved(activeConnection) { - - // Attempt to retrieve connection details only if the - // underlying connection is known - if (activeConnection.connectionIdentifier) { - connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier) - .then(function connectionRetrieved(connection) { - managedClient.name = managedClient.title = connection.name; - }, requestService.WARN); - } - - }, requestService.WARN); - } - - return managedClient; - - }; - - /** - * Connects the given ManagedClient instance to its associated connection - * or connection group. If the ManagedClient has already been connected, - * including if connected but subsequently disconnected, this function has - * no effect. - * - * @param {ManagedClient} managedClient - * The ManagedClient to connect. - * - * @param {number} [width] - * The optimal display width, in local CSS pixels. If omitted, the - * browser window width will be used. - * - * @param {number} [height] - * The optimal display height, in local CSS pixels. If omitted, the - * browser window height will be used. - */ - ManagedClient.connect = function connect(managedClient, width, height) { - - // Ignore if already connected - if (managedClient.clientState.connectionState !== ManagedClientState.ConnectionState.IDLE) - return; - - // Parse connection details from ID - const clientIdentifier = ClientIdentifier.fromString(managedClient.id); - - // Connect the Guacamole client - getConnectString(clientIdentifier, width, height) - .then(function connectClient(connectString) { - managedClient.client.connect(connectString); - }); - - }; - - /** - * Uploads the given file to the server through the given Guacamole client. - * The file transfer can be monitored through the corresponding entry in - * the uploads array of the given managedClient. - * - * @param {ManagedClient} managedClient - * The ManagedClient through which the file is to be uploaded. - * - * @param {File} file - * The file to upload. - * - * @param {ManagedFilesystem} [filesystem] - * The filesystem to upload the file to, if any. If not specified, the - * file will be sent as a generic Guacamole file stream. - * - * @param {ManagedFilesystem.File} [directory=filesystem.currentDirectory] - * The directory within the given filesystem to upload the file to. If - * not specified, but a filesystem is given, the current directory of - * that filesystem will be used. - */ - ManagedClient.uploadFile = function uploadFile(managedClient, file, filesystem, directory) { - - // Use generic Guacamole file streams by default - var object = null; - var streamName = null; - - // If a filesystem is given, determine the destination object and stream - if (filesystem) { - object = filesystem.object; - streamName = (directory || filesystem.currentDirectory).streamName + '/' + file.name; - } - - // Start and manage file upload - managedClient.uploads.push(ManagedFileUpload.getInstance(managedClient, file, object, streamName)); - - }; - - /** - * Sends the given clipboard data over the given Guacamole client, setting - * the contents of the remote clipboard to the data provided. If the given - * clipboard data was originally received from that client, the data is - * ignored and this function has no effect. - * - * @param {ManagedClient} managedClient - * The ManagedClient over which the given clipboard data is to be sent. - * - * @param {ClipboardData} data - * The clipboard data to send. - */ - ManagedClient.setClipboard = function setClipboard(managedClient, data) { - - // Ignore clipboard data that was received from this connection - if (data.source === managedClient.id) - return; - - var writer; - - // Create stream with proper mimetype - var stream = managedClient.client.createClipboardStream(data.type); - - // Send data as a string if it is stored as a string - if (typeof data.data === 'string') { - writer = new Guacamole.StringWriter(stream); - writer.sendText(data.data); - writer.sendEnd(); - } - - // Otherwise, assume the data is a File/Blob - else { - - // Write File/Blob asynchronously - writer = new Guacamole.BlobWriter(stream); - writer.oncomplete = function clipboardSent() { - writer.sendEnd(); - }; - - // Begin sending data - writer.sendBlob(data.data); - - } - - }; - - /** - * Assigns the given value to the connection parameter having the given - * name, updating the behavior of the connection in real-time. If the - * connection parameter is not editable, this function has no effect. - * - * @param {ManagedClient} managedClient - * The ManagedClient instance associated with the active connection - * being modified. - * - * @param {String} name - * The name of the connection parameter to modify. - * - * @param {String} value - * The value to attempt to assign to the given connection parameter. - */ - ManagedClient.setArgument = function setArgument(managedClient, name, value) { - var managedArgument = managedClient.arguments[name]; - managedArgument && ManagedArgument.setValue(managedArgument, value); - }; - - /** - * Sends the given connection parameter values using "argv" streams, - * updating the behavior of the connection in real-time if the server is - * expecting or requiring these parameters. - * - * @param {ManagedClient} managedClient - * The ManagedClient instance associated with the active connection - * being modified. - * - * @param {Object.} values - * The set of values to attempt to assign to corresponding connection - * parameters, where each object key is the connection parameter being - * set. - */ - ManagedClient.sendArguments = function sendArguments(managedClient, values) { - angular.forEach(values, function sendArgument(value, name) { - var stream = managedClient.client.createArgumentValueStream("text/plain", name); - var writer = new Guacamole.StringWriter(stream); - writer.sendText(value); - writer.sendEnd(); - }); - }; - - /** - * Retrieves the current values of all editable connection parameters as a - * set of name/value pairs suitable for use as the model of a form which - * edits those parameters. - * - * @param {ManagedClient} client - * The ManagedClient instance associated with the active connection - * whose parameter values are being retrieved. - * - * @returns {Object.} - * A new set of name/value pairs containing the current values of all - * editable parameters. - */ - ManagedClient.getArgumentModel = function getArgumentModel(client) { - - var model = {}; - - angular.forEach(client.arguments, function addModelEntry(managedArgument) { - model[managedArgument.name] = managedArgument.value; - }); - - return model; - - }; - - /** - * Produces a sharing link for the given ManagedClient using the given - * sharing profile. The resulting sharing link, and any required login - * information, can be retrieved from the shareLinks property - * of the given ManagedClient once the various underlying service calls - * succeed. - * - * @param {ManagedClient} client - * The ManagedClient which will be shared via the generated sharing - * link. - * - * @param {SharingProfile} sharingProfile - * The sharing profile to use to generate the sharing link. - * - * @returns {Promise} - * A Promise which is resolved once the sharing link has been - * successfully generated, and rejected if generating the link fails. - */ - ManagedClient.createShareLink = function createShareLink(client, sharingProfile) { - - // Retrieve sharing credentials for the sake of generating a share link - var credentialRequest = tunnelService.getSharingCredentials( - client.tunnel.uuid, sharingProfile.identifier); - - // Add a new share link once the credentials are ready - credentialRequest.then(function sharingCredentialsReceived(sharingCredentials) { - client.shareLinks[sharingProfile.identifier] = - ManagedShareLink.getInstance(sharingProfile, sharingCredentials); - }, requestService.WARN); - - return credentialRequest; - - }; - - /** - * Returns whether the given ManagedClient is being shared. A ManagedClient - * is shared if it has any associated share links. - * - * @param {ManagedClient} client - * The ManagedClient to check. - * - * @returns {Boolean} - * true if the ManagedClient has at least one associated share link, - * false otherwise. - */ - ManagedClient.isShared = function isShared(client) { - - // The connection is shared if at least one share link exists - for (var dummy in client.shareLinks) - return true; - - // No share links currently exist - return false; - - }; - - /** - * Returns whether the given client has any associated file transfers, - * regardless of those file transfers' state. - * - * @param {GuacamoleClient} client - * The client for which file transfers should be checked. - * - * @returns {boolean} - * true if there are any file transfers associated with the - * given client, false otherwise. - */ - ManagedClient.hasTransfers = function hasTransfers(client) { - return !!(client && client.uploads && client.uploads.length); - }; - - /** - * Store the thumbnail of the given managed client within the connection - * history under its associated ID. If the client is not connected, this - * function has no effect. - * - * @param {ManagedClient} managedClient - * The client whose history entry should be updated. - */ - ManagedClient.updateThumbnail = function updateThumbnail(managedClient) { - - var display = managedClient.client.getDisplay(); - - // Update stored thumbnail of previous connection - if (display && display.getWidth() > 0 && display.getHeight() > 0) { - - // Get screenshot - var canvas = display.flatten(); - - // Calculate scale of thumbnail (max 320x240, max zoom 100%) - var scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); - - // Create thumbnail canvas - var thumbnail = $document[0].createElement("canvas"); - thumbnail.width = canvas.width*scale; - thumbnail.height = canvas.height*scale; - - // Scale screenshot to thumbnail - var context = thumbnail.getContext("2d"); - context.drawImage(canvas, - 0, 0, canvas.width, canvas.height, - 0, 0, thumbnail.width, thumbnail.height - ); - - // Store updated thumbnail within client - managedClient.thumbnail = new ManagedClientThumbnail({ - timestamp : new Date().getTime(), - canvas : thumbnail - }); - - // Update historical thumbnail - guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL("image/png")); - - } - - }; - - - /** - * Register a handler that will be automatically invoked for any deferred - * pipe stream with the provided name, either when a pipe stream with a - * name matching a registered handler is received, or immediately when this - * function is called, if such a pipe stream has already been received. - * - * NOTE: Pipe streams are automatically deferred by the default onpipe - * implementation. To preserve this behavior when using a custom onpipe - * callback, make sure to defer to the default implementation as needed. - * - * @param {ManagedClient} managedClient - * The client for which the deferred pipe stream handler should be set. - * - * @param {String} name - * The name of the pipe stream that should be handeled by the provided - * handler. If another handler is already registered for this name, it - * will be replaced by the handler provided to this function. - * - * @param {Function} handler - * The handler that should handle any deferred pipe stream with the - * provided name. This function must take the same arguments as the - * standard onpipe handler - namely, the stream itself, the mimetype, - * and the name. - */ - ManagedClient.registerDeferredPipeHandler = function registerDeferredPipeHandler( - managedClient, name, handler) { - managedClient.deferredPipeStreamHandlers[name] = handler; - - // Invoke the handler now, if the pipestream has already been received - if (managedClient.deferredPipeStreams[name]) { - - // Invoke the handler with the deferred pipe stream - var deferredStream = managedClient.deferredPipeStreams[name]; - handler(deferredStream.stream, - deferredStream.mimetype, - deferredStream.name); - - // Clean up the now-consumed pipe stream - delete managedClient.deferredPipeStreams[name]; - } - }; - - /** - * Detach the provided deferred pipe stream handler, if it is currently - * registered for the provided pipe stream name. - * - * @param {String} name - * The name of the associated pipe stream for the handler that should - * be detached. - * - * @param {Function} handler - * The handler that should be detached. - * - * @param {ManagedClient} managedClient - * The client for which the deferred pipe stream handler should be - * detached. - */ - ManagedClient.detachDeferredPipeHandler = function detachDeferredPipeHandler( - managedClient, name, handler) { - - // Remove the handler if found - if (managedClient.deferredPipeStreamHandlers[name] === handler) - delete managedClient.deferredPipeStreamHandlers[name]; - }; - - return ManagedClient; - -}]); diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedDisplay.js b/guacamole/src/main/frontend/src/app/client/types/ManagedDisplay.js deleted file mode 100644 index fedefad5dc..0000000000 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedDisplay.js +++ /dev/null @@ -1,174 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the ManagedDisplay class used by the guacClientManager service. - */ -angular.module('client').factory('ManagedDisplay', ['$rootScope', - function defineManagedDisplay($rootScope) { - - /** - * Object which serves as a surrogate interface, encapsulating a Guacamole - * display while it is active, allowing it to be detached and reattached - * from different client views. - * - * @constructor - * @param {ManagedDisplay|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedDisplay. - */ - var ManagedDisplay = function ManagedDisplay(template) { - - // Use empty object by default - template = template || {}; - - /** - * The underlying Guacamole display. - * - * @type Guacamole.Display - */ - this.display = template.display; - - /** - * The current size of the Guacamole display. - * - * @type ManagedDisplay.Dimensions - */ - this.size = new ManagedDisplay.Dimensions(template.size); - - /** - * The current mouse cursor, if any. - * - * @type ManagedDisplay.Cursor - */ - this.cursor = template.cursor; - - }; - - /** - * Object which represents the size of the Guacamole display. - * - * @constructor - * @param {ManagedDisplay.Dimensions|Object} template - * The object whose properties should be copied within the new - * ManagedDisplay.Dimensions. - */ - ManagedDisplay.Dimensions = function Dimensions(template) { - - // Use empty object by default - template = template || {}; - - /** - * The current width of the Guacamole display, in pixels. - * - * @type Number - */ - this.width = template.width || 0; - - /** - * The current width of the Guacamole display, in pixels. - * - * @type Number - */ - this.height = template.height || 0; - - }; - - /** - * Object which represents a mouse cursor used by the Guacamole display. - * - * @constructor - * @param {ManagedDisplay.Cursor|Object} template - * The object whose properties should be copied within the new - * ManagedDisplay.Cursor. - */ - ManagedDisplay.Cursor = function Cursor(template) { - - // Use empty object by default - template = template || {}; - - /** - * The actual mouse cursor image. - * - * @type HTMLCanvasElement - */ - this.canvas = template.canvas; - - /** - * The X coordinate of the cursor hotspot. - * - * @type Number - */ - this.x = template.x; - - /** - * The Y coordinate of the cursor hotspot. - * - * @type Number - */ - this.y = template.y; - - }; - - /** - * Creates a new ManagedDisplay which represents the current state of the - * given Guacamole display. - * - * @param {Guacamole.Display} display - * The Guacamole display to represent. Changes to this display will - * affect this ManagedDisplay. - * - * @returns {ManagedDisplay} - * A new ManagedDisplay which represents the current state of the - * given Guacamole display. - */ - ManagedDisplay.getInstance = function getInstance(display) { - - var managedDisplay = new ManagedDisplay({ - display : display - }); - - // Store changes to display size - display.onresize = function setClientSize() { - $rootScope.$apply(function updateClientSize() { - managedDisplay.size = new ManagedDisplay.Dimensions({ - width : display.getWidth(), - height : display.getHeight() - }); - }); - }; - - // Store changes to display cursor - display.oncursor = function setClientCursor(canvas, x, y) { - $rootScope.$apply(function updateClientCursor() { - managedDisplay.cursor = new ManagedDisplay.Cursor({ - canvas : canvas, - x : x, - y : y - }); - }); - }; - - return managedDisplay; - - }; - - return ManagedDisplay; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js b/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js deleted file mode 100644 index 95eef0c61f..0000000000 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedFileUpload.js +++ /dev/null @@ -1,208 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the ManagedFileUpload class used by the guacClientManager service. - */ -angular.module('client').factory('ManagedFileUpload', ['$rootScope', '$injector', - function defineManagedFileUpload($rootScope, $injector) { - - // Required types - var Error = $injector.get('Error'); - var ManagedFileTransferState = $injector.get('ManagedFileTransferState'); - - // Required services - var requestService = $injector.get('requestService'); - var tunnelService = $injector.get('tunnelService'); - - /** - * Object which serves as a surrogate interface, encapsulating a Guacamole - * file upload while it is active, allowing it to be detached and - * reattached from different client views. - * - * @constructor - * @param {ManagedFileUpload|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedFileUpload. - */ - var ManagedFileUpload = function ManagedFileUpload(template) { - - // Use empty object by default - template = template || {}; - - /** - * The current state of the file transfer stream. - * - * @type ManagedFileTransferState - */ - this.transferState = template.transferState || new ManagedFileTransferState(); - - /** - * The mimetype of the file being transferred. - * - * @type String - */ - this.mimetype = template.mimetype; - - /** - * The filename of the file being transferred. - * - * @type String - */ - this.filename = template.filename; - - /** - * The number of bytes transferred so far. - * - * @type Number - */ - this.progress = template.progress; - - /** - * The total number of bytes in the file. - * - * @type Number - */ - this.length = template.length; - - }; - - /** - * Creates a new ManagedFileUpload which uploads the given file to the - * server through the given Guacamole client. - * - * @param {ManagedClient} managedClient - * The ManagedClient through which the file is to be uploaded. - * - * @param {File} file - * The file to upload. - * - * @param {Object} [object] - * The object to upload the file to, if any, such as a filesystem - * object. - * - * @param {String} [streamName] - * The name of the stream to upload the file to. If an object is given, - * this must be specified. - * - * @return {ManagedFileUpload} - * A new ManagedFileUpload object which can be used to track the - * progress of the upload. - */ - ManagedFileUpload.getInstance = function getInstance(managedClient, file, object, streamName) { - - var managedFileUpload = new ManagedFileUpload(); - - // Pull Guacamole.Tunnel and Guacamole.Client from given ManagedClient - var client = managedClient.client; - var tunnel = managedClient.tunnel; - - // Open file for writing - var stream; - if (!object) - stream = client.createFileStream(file.type, file.name); - - // If object/streamName specified, upload to that instead of a file - // stream - else - stream = object.createOutputStream(file.type, streamName); - - // Notify that the file transfer is pending - $rootScope.$evalAsync(function uploadStreamOpen() { - - // Init managed upload - managedFileUpload.filename = file.name; - managedFileUpload.mimetype = file.type; - managedFileUpload.progress = 0; - managedFileUpload.length = file.size; - - // Notify that stream is open - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.OPEN); - - }); - - // Upload file once stream is acknowledged - stream.onack = function beginUpload(status) { - - // Notify of any errors from the Guacamole server - if (status.isError()) { - $rootScope.$apply(function uploadStreamError() { - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.ERROR, - status.code); - }); - return; - } - - // Begin upload - tunnelService.uploadToStream(tunnel.uuid, stream, file, function uploadContinuing(length) { - $rootScope.$apply(function uploadStreamProgress() { - managedFileUpload.progress = length; - }); - }) - - // Notify if upload succeeds - .then(function uploadSuccessful() { - - // Upload complete - managedFileUpload.progress = file.size; - - // Close the stream - stream.sendEnd(); - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.CLOSED); - - // Notify of upload completion - $rootScope.$broadcast('guacUploadComplete', file.name); - }, - - // Notify if upload fails - requestService.createErrorCallback(function uploadFailed(error) { - - // Use provide status code if the error is coming from the stream - if (error.type === Error.Type.STREAM_ERROR) - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.ERROR, - error.statusCode); - - // Fail with internal error for all other causes - else - ManagedFileTransferState.setStreamState(managedFileUpload.transferState, - ManagedFileTransferState.StreamState.ERROR, - Guacamole.Status.Code.INTERNAL_ERROR); - - // Close the stream - stream.sendEnd(); - - })); - - // Ignore all further acks - stream.onack = null; - - - }; - - return managedFileUpload; - - }; - - return ManagedFileUpload; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedFilesystem.js b/guacamole/src/main/frontend/src/app/client/types/ManagedFilesystem.js deleted file mode 100644 index a81fcb1685..0000000000 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedFilesystem.js +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the ManagedFilesystem class used by ManagedClient to represent - * available remote filesystems. - */ -angular.module('client').factory('ManagedFilesystem', ['$rootScope', '$injector', - function defineManagedFilesystem($rootScope, $injector) { - - // Required types - var tunnelService = $injector.get('tunnelService'); - - /** - * Object which serves as a surrogate interface, encapsulating a Guacamole - * filesystem object while it is active, allowing it to be detached and - * reattached from different client views. - * - * @constructor - * @param {ManagedFilesystem|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedFilesystem. - */ - var ManagedFilesystem = function ManagedFilesystem(template) { - - // Use empty object by default - template = template || {}; - - /** - * The client that originally received the "filesystem" instruction - * that resulted in the creation of this ManagedFilesystem. - * - * @type ManagedClient - */ - this.client = template.client; - - /** - * The Guacamole filesystem object, as received via a "filesystem" - * instruction. - * - * @type Guacamole.Object - */ - this.object = template.object; - - /** - * The declared, human-readable name of the filesystem - * - * @type String - */ - this.name = template.name; - - /** - * The root directory of the filesystem. - * - * @type ManagedFilesystem.File - */ - this.root = template.root; - - /** - * The current directory being viewed or manipulated within the - * filesystem. - * - * @type ManagedFilesystem.File - */ - this.currentDirectory = template.currentDirectory || template.root; - - }; - - /** - * Refreshes the contents of the given file, if that file is a directory. - * Only the immediate children of the file are refreshed. Files further - * down the directory tree are not refreshed. - * - * @param {ManagedFilesystem} filesystem - * The filesystem associated with the file being refreshed. - * - * @param {ManagedFilesystem.File} file - * The file being refreshed. - */ - ManagedFilesystem.refresh = function updateDirectory(filesystem, file) { - - // Do not attempt to refresh the contents of directories - if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) - return; - - // Request contents of given file - filesystem.object.requestInputStream(file.streamName, function handleStream(stream, mimetype) { - - // Ignore stream if mimetype is wrong - if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) { - stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED); - return; - } - - // Signal server that data is ready to be received - stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS); - - // Read stream as JSON - var reader = new Guacamole.JSONReader(stream); - - // Acknowledge received JSON blobs - reader.onprogress = function onprogress() { - stream.sendAck("Received", Guacamole.Status.Code.SUCCESS); - }; - - // Reset contents of directory - reader.onend = function jsonReady() { - $rootScope.$evalAsync(function updateFileContents() { - - // Empty contents - file.files = {}; - - // Determine the expected filename prefix of each stream - var expectedPrefix = file.streamName; - if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/') - expectedPrefix += '/'; - - // For each received stream name - var mimetypes = reader.getJSON(); - for (var name in mimetypes) { - - // Assert prefix is correct - if (name.substring(0, expectedPrefix.length) !== expectedPrefix) - continue; - - // Extract filename from stream name - var filename = name.substring(expectedPrefix.length); - - // Deduce type from mimetype - var type = ManagedFilesystem.File.Type.NORMAL; - if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) - type = ManagedFilesystem.File.Type.DIRECTORY; - - // Add file entry - file.files[filename] = new ManagedFilesystem.File({ - mimetype : mimetypes[name], - streamName : name, - type : type, - parent : file, - name : filename - }); - - } - - }); - }; - - }); - - }; - - /** - * Creates a new ManagedFilesystem instance from the given Guacamole.Object - * and human-readable name. Upon creation, a request to populate the - * contents of the root directory will be automatically dispatched. - * - * @param {ManagedClient} client - * The client that originally received the "filesystem" instruction - * that resulted in the creation of this ManagedFilesystem. - * - * @param {Guacamole.Object} object - * The Guacamole.Object defining the filesystem. - * - * @param {String} name - * A human-readable name for the filesystem. - * - * @returns {ManagedFilesystem} - * The newly-created ManagedFilesystem. - */ - ManagedFilesystem.getInstance = function getInstance(client, object, name) { - - // Init new filesystem object - var managedFilesystem = new ManagedFilesystem({ - client : client, - object : object, - name : name, - root : new ManagedFilesystem.File({ - mimetype : Guacamole.Object.STREAM_INDEX_MIMETYPE, - streamName : Guacamole.Object.ROOT_STREAM, - type : ManagedFilesystem.File.Type.DIRECTORY - }) - }); - - // Retrieve contents of root - ManagedFilesystem.refresh(managedFilesystem, managedFilesystem.root); - - return managedFilesystem; - - }; - - /** - * Downloads the given file from the server using the given Guacamole - * client and filesystem. The browser will automatically start the - * download upon completion of this function. - * - * @param {ManagedFilesystem} managedFilesystem - * The ManagedFilesystem from which the file is to be downloaded. Any - * path information provided must be relative to this filesystem. - * - * @param {String} path - * The full, absolute path of the file to download. - */ - ManagedFilesystem.downloadFile = function downloadFile(managedFilesystem, path) { - - // Request download - managedFilesystem.object.requestInputStream(path, function downloadStreamReceived(stream, mimetype) { - - // Parse filename from string - var filename = path.match(/(.*[\\/])?(.*)/)[2]; - - // Start download - tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid, stream, mimetype, filename); - - }); - - }; - - /** - * Changes the current directory of the given filesystem, automatically - * refreshing the contents of that directory. - * - * @param {ManagedFilesystem} filesystem - * The filesystem whose current directory should be changed. - * - * @param {ManagedFilesystem.File} file - * The directory to change to. - */ - ManagedFilesystem.changeDirectory = function changeDirectory(filesystem, file) { - - // Refresh contents - ManagedFilesystem.refresh(filesystem, file); - - // Set current directory - filesystem.currentDirectory = file; - - }; - - /** - * A file within a ManagedFilesystem. Each ManagedFilesystem.File provides - * sufficient information for retrieval or replacement of the file's - * contents, as well as the file's name and type. - * - * @param {ManagedFilesystem|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedFilesystem.File. - */ - ManagedFilesystem.File = function File(template) { - - /** - * The mimetype of the data contained within this file. - * - * @type String - */ - this.mimetype = template.mimetype; - - /** - * The name of the stream representing this files contents within its - * associated filesystem object. - * - * @type String - */ - this.streamName = template.streamName; - - /** - * The type of this file. All legal file type strings are defined - * within ManagedFilesystem.File.Type. - * - * @type String - */ - this.type = template.type; - - /** - * The name of this file. - * - * @type String - */ - this.name = template.name; - - /** - * The parent directory of this file. In the case of the root - * directory, this will be null. - * - * @type ManagedFilesystem.File - */ - this.parent = template.parent; - - /** - * Map of all known files containined within this file by name. This is - * only applicable to directories. - * - * @type Object. - */ - this.files = template.files || {}; - - }; - - /** - * All legal type strings for a ManagedFilesystem.File. - * - * @type Object. - */ - ManagedFilesystem.File.Type = { - - /** - * A normal file. As ManagedFilesystem does not currently represent any - * other non-directory types of files, like symbolic links, this type - * string may be used for any non-directory file. - * - * @type String - */ - NORMAL : 'NORMAL', - - /** - * A directory. - * - * @type String - */ - DIRECTORY : 'DIRECTORY' - - }; - - return ManagedFilesystem; - -}]); diff --git a/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js b/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js deleted file mode 100644 index 315b08b5f8..0000000000 --- a/guacamole/src/main/frontend/src/app/clipboard/directives/guacClipboard.js +++ /dev/null @@ -1,133 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive provides an editor for the clipboard content maintained by - * clipboardService. Changes to the clipboard by clipboardService will - * automatically be reflected in the editor, and changes in the editor will - * automatically be reflected in the clipboard by clipboardService. - */ -angular.module('clipboard').directive('guacClipboard', ['$injector', - function guacClipboard($injector) { - - // Required types - const ClipboardData = $injector.get('ClipboardData'); - - // Required services - const $window = $injector.get('$window'); - const clipboardService = $injector.get('clipboardService'); - - /** - * Configuration object for the guacClipboard directive. - * - * @type Object. - */ - var config = { - restrict : 'E', - replace : true, - templateUrl : 'app/clipboard/templates/guacClipboard.html' - }; - - // guacClipboard directive controller - config.controller = ['$scope', '$injector', '$element', - function guacClipboardController($scope, $injector, $element) { - - /** - * The DOM element which will contain the clipboard contents within the - * user interface provided by this directive. We populate the clipboard - * editor via this DOM element rather than updating a model so that we - * are prepared for future support of rich text contents. - * - * @type {!Element} - */ - var element = $element[0].querySelectorAll('.clipboard')[0]; - - /** - * Whether clipboard contents should be displayed in the clipboard - * editor. If false, clipboard contents will not be displayed until - * the user manually reveals them. - * - * @type {!boolean} - */ - $scope.contentsShown = false; - - /** - * Reveals the contents of the clipboard editor, automatically - * assigning input focus to the editor if possible. - */ - $scope.showContents = function showContents() { - $scope.contentsShown = true; - $window.setTimeout(function setFocus() { - element.focus(); - }, 0); - }; - - /** - * Rereads the contents of the clipboard field, updating the - * ClipboardData object on the scope as necessary. The type of data - * stored within the ClipboardData object will be heuristically - * determined from the HTML contents of the clipboard field. - */ - var updateClipboardData = function updateClipboardData() { - - // Read contents of clipboard textarea - clipboardService.setClipboard(new ClipboardData({ - type : 'text/plain', - data : element.value - })); - - }; - - /** - * Updates the contents of the clipboard editor to the given data. - * - * @param {ClipboardData} data - * The ClipboardData to display within the clipboard editor for - * editing. - */ - const updateClipboardEditor = function updateClipboardEditor(data) { - - // If the clipboard data is a string, render it as text - if (typeof data.data === 'string') - element.value = data.data; - - // Ignore other data types for now - - }; - - // Update the internally-stored clipboard data when events are fired - // that indicate the clipboard field may have been changed - element.addEventListener('input', updateClipboardData); - element.addEventListener('change', updateClipboardData); - - // Update remote clipboard if local clipboard changes - $scope.$on('guacClipboard', function clipboardChanged(event, data) { - updateClipboardEditor(data); - }); - - // Init clipboard editor with current clipboard contents - clipboardService.getClipboard().then((data) => { - updateClipboardEditor(data); - }, angular.noop); - - }]; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js b/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js deleted file mode 100644 index 4ec9d04b8b..0000000000 --- a/guacamole/src/main/frontend/src/app/clipboard/services/clipboardService.js +++ /dev/null @@ -1,625 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for maintaining and accessing clipboard data. If possible, this - * service will leverage the local clipboard. If the local clipboard is not - * available, an internal in-memory clipboard will be used instead. - */ -angular.module('clipboard').factory('clipboardService', ['$injector', - function clipboardService($injector) { - - // Get required services - const $q = $injector.get('$q'); - const $window = $injector.get('$window'); - const $rootScope = $injector.get('$rootScope'); - const sessionStorageFactory = $injector.get('sessionStorageFactory'); - - // Required types - const ClipboardData = $injector.get('ClipboardData'); - - /** - * Getter/setter which retrieves or sets the current stored clipboard - * contents. The stored clipboard contents are strictly internal to - * Guacamole, and may not reflect the local clipboard if local clipboard - * access is unavailable. - * - * @type Function - */ - const storedClipboardData = sessionStorageFactory.create(new ClipboardData()); - - var service = {}; - - /** - * The amount of time to wait before actually serving a request to read - * clipboard data, in milliseconds. Providing a reasonable delay between - * request and read attempt allows the cut/copy operation to settle, in - * case the data we are anticipating to be present is not actually present - * in the clipboard yet. - * - * @constant - * @type Number - */ - var CLIPBOARD_READ_DELAY = 100; - - /** - * The promise associated with the current pending clipboard read attempt. - * If no clipboard read is active, this will be null. - * - * @type Promise. - */ - var pendingRead = null; - - /** - * Reference to the window.document object. - * - * @private - * @type HTMLDocument - */ - var document = $window.document; - - /** - * The textarea that will be used to hold the local clipboard contents. - * - * @type Element - */ - var clipboardContent = document.createElement('textarea'); - - // Ensure clipboard target is selectable but not visible - clipboardContent.className = 'clipboard-service-target'; - - // Add clipboard target to DOM - document.body.appendChild(clipboardContent); - - /** - * Stops the propagation of the given event through the DOM tree. This is - * identical to invoking stopPropagation() on the event directly, except - * that this function is usable as an event handler itself. - * - * @param {Event} e - * The event whose propagation through the DOM tree should be stopped. - */ - var stopEventPropagation = function stopEventPropagation(e) { - e.stopPropagation(); - }; - - // Prevent events generated due to execCommand() from disturbing external things - clipboardContent.addEventListener('cut', stopEventPropagation); - clipboardContent.addEventListener('copy', stopEventPropagation); - clipboardContent.addEventListener('paste', stopEventPropagation); - clipboardContent.addEventListener('input', stopEventPropagation); - - /** - * A stack of past node selection ranges. A range convering the nodes - * currently selected within the document can be pushed onto this stack - * with pushSelection(), and the most recently pushed selection can be - * popped off the stack (and thus re-selected) with popSelection(). - * - * @type Range[] - */ - var selectionStack = []; - - /** - * Pushes the current selection range to the selection stack such that it - * can later be restored with popSelection(). - */ - var pushSelection = function pushSelection() { - - // Add a range representing the current selection to the stack - var selection = $window.getSelection(); - if (selection.getRangeAt && selection.rangeCount) - selectionStack.push(selection.getRangeAt(0)); - - }; - - /** - * Pops a selection range off the selection stack restoring the document's - * previous selection state. The selection range will be the most recent - * selection range pushed by pushSelection(). If there are no selection - * ranges currently on the stack, this function has no effect. - */ - var popSelection = function popSelection() { - - // Pull one selection range from the stack - var range = selectionStack.pop(); - if (!range) - return; - - // Replace any current selection with the retrieved selection - var selection = $window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - }; - - /** - * Selects all nodes within the given element. This will replace the - * current selection with a new selection range that covers the element's - * contents. If the original selection should be preserved, use - * pushSelection() and popSelection(). - * - * @param {Element} element - * The element whose contents should be selected. - */ - var selectAll = function selectAll(element) { - - // Use the select() function defined for input elements, if available - if (element.select) - element.select(); - - // Fallback to manual manipulation of the selection - else { - - // Generate a range which selects all nodes within the given element - var range = document.createRange(); - range.selectNodeContents(element); - - // Replace any current selection with the generated range - var selection = $window.getSelection(); - selection.removeAllRanges(); - selection.addRange(range); - - } - - }; - - /** - * Sets the local clipboard, if possible, to the given text. - * - * @param {ClipboardData} data - * The data to assign to the local clipboard should be set. - * - * @return {Promise} - * A promise that will resolve if setting the clipboard was successful, - * and will reject if it failed. - */ - const setLocalClipboard = function setLocalClipboard(data) { - - var deferred = $q.defer(); - - try { - - // Attempt to read the clipboard using the Asynchronous Clipboard - // API, if it's available - if (navigator.clipboard && navigator.clipboard.writeText) { - if (data.type === 'text/plain') { - navigator.clipboard.writeText(data.data).then(deferred.resolve, deferred.reject); - return deferred.promise; - } - } - - } - - // Ignore any hard failures to use Asynchronous Clipboard API, falling - // back to traditional document.execCommand() - catch (ignore) {} - - // Track the originally-focused element prior to changing focus - var originalElement = document.activeElement; - pushSelection(); - - // Copy the given value into the clipboard DOM element - if (typeof data.data === 'string') - clipboardContent.value = data.data; - else { - clipboardContent.innerHTML = ''; - var img = document.createElement('img'); - img.src = URL.createObjectURL(data.data); - clipboardContent.appendChild(img); - } - - // Select all data within the clipboard target - clipboardContent.focus(); - selectAll(clipboardContent); - - // Attempt to copy data from clipboard element into local clipboard - if (document.execCommand('copy')) - deferred.resolve(); - else - deferred.reject(); - - // Unfocus the clipboard DOM event to avoid mobile keyboard opening, - // restoring whichever element was originally focused - clipboardContent.blur(); - originalElement.focus(); - popSelection(); - - return deferred.promise; - }; - - /** - * Parses the given data URL, returning its decoded contents as a new Blob. - * If the URL is not a valid data URL, null will be returned instead. - * - * @param {String} url - * The data URL to parse. - * - * @returns {Blob} - * A new Blob containing the decoded contents of the data URL, or null - * if the URL is not a valid data URL. - */ - service.parseDataURL = function parseDataURL(url) { - - // Parse given string as a data URL - var result = /^data:([^;]*);base64,([a-zA-Z0-9+/]*[=]*)$/.exec(url); - if (!result) - return null; - - // Pull the mimetype and base64 contents of the data URL - var type = result[1]; - var data = $window.atob(result[2]); - - // Convert the decoded binary string into a typed array - var buffer = new Uint8Array(data.length); - for (var i = 0; i < data.length; i++) - buffer[i] = data.charCodeAt(i); - - // Produce a proper blob containing the data and type provided in - // the data URL - return new Blob([buffer], { type : type }); - - }; - - /** - * Returns the content of the given element as plain, unformatted text, - * preserving only individual characters and newlines. Formatting, images, - * etc. are not taken into account. - * - * @param {Element} element - * The element whose text content should be returned. - * - * @returns {String} - * The plain text contents of the given element, including newlines and - * spacing but otherwise without any formatting. - */ - service.getTextContent = function getTextContent(element) { - - var blocks = []; - var currentBlock = ''; - - // For each child of the given element - var current = element.firstChild; - while (current) { - - // Simply append the content of any text nodes - if (current.nodeType === Node.TEXT_NODE) - currentBlock += current.nodeValue; - - // Render
    as a newline character - else if (current.nodeName === 'BR') - currentBlock += '\n'; - - // Render as alt text, if available - else if (current.nodeName === 'IMG') - currentBlock += current.getAttribute('alt') || ''; - - // For all other nodes, handling depends on whether they are - // block-level elements - else { - - // If we are entering a new block context, start a new block if - // the current block is non-empty - if (currentBlock.length && $window.getComputedStyle(current).display === 'block') { - - // Trim trailing newline (would otherwise inflate the line count by 1) - if (currentBlock.substring(currentBlock.length - 1) === '\n') - currentBlock = currentBlock.substring(0, currentBlock.length - 1); - - // Finish current block and start a new block - blocks.push(currentBlock); - currentBlock = ''; - - } - - // Append the content of the current element to the current block - currentBlock += service.getTextContent(current); - - } - - current = current.nextSibling; - - } - - // Add any in-progress block - if (currentBlock.length) - blocks.push(currentBlock); - - // Combine all non-empty blocks, separated by newlines - return blocks.join('\n'); - - }; - - /** - * Replaces the current text content of the given element with the given - * text. To avoid affecting the position of the cursor within an editable - * element, or firing unnecessary DOM modification events, the underlying - * textContent property of the element is only touched if - * doing so would actually change the text. - * - * @param {Element} element - * The element whose text content should be changed. - * - * @param {String} text - * The text content to assign to the given element. - */ - service.setTextContent = function setTextContent(element, text) { - - // Strip out any images - $(element).find('img').remove(); - - // Reset text content only if doing so will actually change the content - if (service.getTextContent(element) !== text) - element.textContent = text; - - }; - - /** - * Returns the URL of the single image within the given element, if the - * element truly contains only one child and that child is an image. If the - * content of the element is mixed or not an image, null is returned. - * - * @param {Element} element - * The element whose image content should be retrieved. - * - * @returns {String} - * The URL of the image contained within the given element, if that - * element contains only a single child element which happens to be an - * image, or null if the content of the element is not purely an image. - */ - service.getImageContent = function getImageContent(element) { - - // Return the source of the single child element, if it is an image - var firstChild = element.firstChild; - if (firstChild && firstChild.nodeName === 'IMG' && !firstChild.nextSibling) - return firstChild.getAttribute('src'); - - // Otherwise, the content of this element is not simply an image - return null; - - }; - - /** - * Replaces the current contents of the given element with a single image - * having the given URL. To avoid affecting the position of the cursor - * within an editable element, or firing unnecessary DOM modification - * events, the content of the element is only touched if doing so would - * actually change content. - * - * @param {Element} element - * The element whose image content should be changed. - * - * @param {String} url - * The URL of the image which should be assigned as the contents of the - * given element. - */ - service.setImageContent = function setImageContent(element, url) { - - // Retrieve the URL of the current image contents, if any - var currentImage = service.getImageContent(element); - - // If the current contents are not the given image (or not an image - // at all), reassign the contents - if (currentImage !== url) { - - // Clear current contents - element.innerHTML = ''; - - // Add a new image as the sole contents of the element - var img = document.createElement('img'); - img.src = url; - element.appendChild(img); - - } - - }; - - /** - * Get the current value of the local clipboard. - * - * @return {Promise.} - * A promise that will resolve with the contents of the local clipboard - * if getting the clipboard was successful, and will reject if it - * failed. - */ - const getLocalClipboard = function getLocalClipboard() { - - // If the clipboard is already being read, do not overlap the read - // attempts; instead share the result across all requests - if (pendingRead) - return pendingRead; - - var deferred = $q.defer(); - - try { - - // Attempt to read the clipboard using the Asynchronous Clipboard - // API, if it's available - if (navigator.clipboard && navigator.clipboard.readText) { - - navigator.clipboard.readText().then(function textRead(text) { - deferred.resolve(new ClipboardData({ - type : 'text/plain', - data : text - })); - }, deferred.reject); - - return deferred.promise; - - } - - } - - // Ignore any hard failures to use Asynchronous Clipboard API, falling - // back to traditional document.execCommand() - catch (ignore) {} - - // Track the originally-focused element prior to changing focus - var originalElement = document.activeElement; - - /** - * Attempts to paste the clipboard contents into the - * currently-focused element. The promise related to the current - * attempt to read the clipboard will be resolved or rejected - * depending on whether the attempt to paste succeeds. - */ - var performPaste = function performPaste() { - - // Attempt paste local clipboard into clipboard DOM element - if (document.execCommand('paste')) { - - // If the pasted data is a single image, resolve with a blob - // containing that image - var currentImage = service.getImageContent(clipboardContent); - if (currentImage) { - - // Convert the image's data URL into a blob - var blob = service.parseDataURL(currentImage); - if (blob) { - deferred.resolve(new ClipboardData({ - type : blob.type, - data : blob - })); - } - - // Reject if conversion fails - else - deferred.reject(); - - } // end if clipboard is an image - - // Otherwise, assume the clipboard contains plain text - else - deferred.resolve(new ClipboardData({ - type : 'text/plain', - data : clipboardContent.value - })); - - } - - // Otherwise, reading from the clipboard has failed - else - deferred.reject(); - - }; - - // Mark read attempt as in progress, cleaning up event listener and - // selection once the paste attempt has completed - pendingRead = deferred.promise['finally'](function cleanupReadAttempt() { - - // Do not use future changes in focus - clipboardContent.removeEventListener('focus', performPaste); - - // Unfocus the clipboard DOM event to avoid mobile keyboard opening, - // restoring whichever element was originally focused - clipboardContent.blur(); - originalElement.focus(); - popSelection(); - - // No read is pending any longer - pendingRead = null; - - }); - - // Wait for the next event queue run before attempting to read - // clipboard data (in case the copy/cut has not yet completed) - $window.setTimeout(function deferredClipboardRead() { - - pushSelection(); - - // Ensure clipboard element is blurred (and that the "focus" event - // will fire) - clipboardContent.blur(); - clipboardContent.addEventListener('focus', performPaste); - - // Clear and select the clipboard DOM element - clipboardContent.value = ''; - clipboardContent.focus(); - selectAll(clipboardContent); - - // If focus failed to be set, we cannot read the clipboard - if (document.activeElement !== clipboardContent) - deferred.reject(); - - }, CLIPBOARD_READ_DELAY); - - return pendingRead; - - }; - - /** - * Returns the current value of the internal clipboard shared across all - * active Guacamole connections running within the current browser tab. If - * access to the local clipboard is available, the internal clipboard is - * first synchronized with the current local clipboard contents. If access - * to the local clipboard is unavailable, only the internal clipboard will - * be used. - * - * @return {Promise.} - * A promise that will resolve with the contents of the internal - * clipboard, first retrieving those contents from the local clipboard - * if permission to do so has been granted. This promise is always - * resolved. - */ - service.getClipboard = function getClipboard() { - return getLocalClipboard().then((data) => storedClipboardData(data), () => storedClipboardData()); - }; - - /** - * Sets the content of the internal clipboard shared across all active - * Guacamole connections running within the current browser tab. If - * access to the local clipboard is available, the local clipboard is - * first set to the provided clipboard content. If access to the local - * clipboard is unavailable, only the internal clipboard will be used. A - * "guacClipboard" event will be broadcast with the assigned data once the - * operation has completed. - * - * @param {ClipboardData} data - * The data to assign to the clipboard. - * - * @return {Promise} - * A promise that will resolve after the clipboard content has been - * set. This promise is always resolved. - */ - service.setClipboard = function setClipboard(data) { - return setLocalClipboard(data)['catch'](angular.noop).finally(() => { - - // Update internal clipboard and broadcast event notifying of - // updated contents - storedClipboardData(data); - $rootScope.$broadcast('guacClipboard', data); - - }); - }; - - /** - * Resynchronizes the local and internal clipboards, setting the contents - * of the internal clipboard to that of the local clipboard (if local - * clipboard access is granted) and broadcasting a "guacClipboard" event - * with the current internal clipboard contents for consumption by external - * components like the "guacClient" directive. - */ - service.resyncClipboard = function resyncClipboard() { - getLocalClipboard().then(function clipboardRead(data) { - return service.setClipboard(data); - }, angular.noop); - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/clipboard/templates/guacClipboard.html b/guacamole/src/main/frontend/src/app/clipboard/templates/guacClipboard.html deleted file mode 100644 index 159ee356a1..0000000000 --- a/guacamole/src/main/frontend/src/app/clipboard/templates/guacClipboard.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - -
    -

    {{ 'CLIENT.ACTION_SHOW_CLIPBOARD' | translate }}

    -
    -
    diff --git a/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js b/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js deleted file mode 100644 index 34f699a29d..0000000000 --- a/guacamole/src/main/frontend/src/app/clipboard/types/ClipboardData.js +++ /dev/null @@ -1,68 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the ClipboardData class used for interchange between the - * guacClipboard directive, clipboardService service, etc. - */ -angular.module('clipboard').factory('ClipboardData', [function defineClipboardData() { - - /** - * Arbitrary data which can be contained by the clipboard. - * - * @constructor - * @param {ClipboardData|Object} [template={}] - * The object whose properties should be copied within the new - * ClipboardData. - */ - var ClipboardData = function ClipboardData(template) { - - // Use empty object by default - template = template || {}; - - /** - * The ID of the ManagedClient handling the remote desktop connection - * that originated this clipboard data, or null if the data originated - * from the clipboard editor or local clipboard. - * - * @type {string} - */ - this.source = template.source; - - /** - * The mimetype of the data currently stored within the clipboard. - * - * @type String - */ - this.type = template.type || 'text/plain'; - - /** - * The data currently stored within the clipboard. Depending on the - * nature of the stored data, this may be either a String, a Blob, or a - * File. - * - * @type String|Blob|File - */ - this.data = template.data || ''; - - }; - - return ClipboardData; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacClick.js b/guacamole/src/main/frontend/src/app/element/directives/guacClick.js deleted file mode 100644 index 0847d3c538..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacClick.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides handling of click and click-like touch events. - * The state of Shift and Ctrl modifiers is tracked through these click events - * to allow for specific handling of Shift+Click and Ctrl+Click. - */ -angular.module('element').directive('guacClick', [function guacClick() { - - return { - restrict: 'A', - - link: function linkGuacClick($scope, $element, $attrs) { - - /** - * A callback that is invoked by the guacClick directive when a - * click or click-like event is received. - * - * @callback guacClick~callback - * @param {boolean} shift - * Whether Shift was held down at the time the click occurred. - * - * @param {boolean} ctrl - * Whether Ctrl or Meta (the Mac "Command" key) was held down - * at the time the click occurred. - */ - - /** - * The callback to invoke when a click or click-like event is - * received on the associated element. - * - * @type guacClick~callback - */ - const guacClick = $scope.$eval($attrs.guacClick); - - /** - * The element which will register the click. - * - * @type Element - */ - const element = $element[0]; - - /** - * Whether either Shift key is currently pressed. - * - * @type boolean - */ - let shift = false; - - /** - * Whether either Ctrl key is currently pressed. To allow the - * Command key to be used on Mac platforms, this flag also - * considers the state of either Meta key. - * - * @type boolean - */ - let ctrl = false; - - /** - * Updates the state of the {@link shift} and {@link ctrl} flags - * based on which keys are currently marked as held down by the - * given Guacamole.Keyboard. - * - * @param {Guacamole.Keyboard} keyboard - * The Guacamole.Keyboard instance to read key states from. - */ - const updateModifiers = function updateModifiers(keyboard) { - - shift = !!( - keyboard.pressed[0xFFE1] // Left shift - || keyboard.pressed[0xFFE2] // Right shift - ); - - ctrl = !!( - keyboard.pressed[0xFFE3] // Left ctrl - || keyboard.pressed[0xFFE4] // Right ctrl - || keyboard.pressed[0xFFE7] // Left meta (command) - || keyboard.pressed[0xFFE8] // Right meta (command) - ); - - }; - - // Update tracking of modifier states for each key press - $scope.$on('guacKeydown', function keydownListener(event, keysym, keyboard) { - updateModifiers(keyboard); - }); - - // Update tracking of modifier states for each key release - $scope.$on('guacKeyup', function keyupListener(event, keysym, keyboard) { - updateModifiers(keyboard); - }); - - // Fire provided callback for each mouse-initiated "click" event ... - element.addEventListener('click', function elementClicked(e) { - if (element.contains(e.target)) - $scope.$apply(() => guacClick(shift, ctrl)); - }); - - // ... and for touch-initiated click-like events - element.addEventListener('touchstart', function elementClicked(e) { - if (element.contains(e.target)) - $scope.$apply(() => guacClick(shift, ctrl)); - }); - - } // end guacClick link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js b/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js deleted file mode 100644 index 3f7070694f..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacDrop.js +++ /dev/null @@ -1,171 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows multiple files to be uploaded. Dragging files onto - * the associated element will call the provided callback function with any - * dragged files. - */ -angular.module('element').directive('guacDrop', ['$injector', function guacDrop($injector) { - - // Required services - const guacNotification = $injector.get('guacNotification'); - - return { - restrict: 'A', - - link: function linkGuacDrop($scope, $element, $attrs) { - - /** - * The function to call whenever files are dragged. The callback is - * provided a single parameter: the FileList containing all dragged - * files. - * - * @type Function - */ - const guacDrop = $scope.$eval($attrs.guacDrop); - - /** - * Any number of space-seperated classes to be applied to the - * element a drop is pending: when the user has dragged something - * over the element, but not yet dropped. These classes will be - * removed when a drop is not pending. - * - * @type String - */ - const guacDraggedClass = $scope.$eval($attrs.guacDraggedClass); - - /** - * Whether upload of multiple files should be allowed. If false, an - * error will be displayed explaining the restriction, otherwise - * any number of files may be dragged. Defaults to true if not set. - * - * @type Boolean - */ - const guacMultiple = 'guacMultiple' in $attrs - ? $scope.$eval($attrs.guacMultiple) : true; - - /** - * The element which will register drag event. - * - * @type Element - */ - const element = $element[0]; - - /** - * Applies any classes provided in the guacDraggedClass attribute. - * Further propagation and default behavior of the given event is - * automatically prevented. - * - * @param {Event} e - * The event related to the in-progress drag/drop operation. - */ - const notifyDragStart = function notifyDragStart(e) { - - e.preventDefault(); - e.stopPropagation(); - - // Skip further processing if no classes were provided - if (!guacDraggedClass) - return; - - // Add each provided class - guacDraggedClass.split(' ').forEach(classToApply => - element.classList.add(classToApply)); - - }; - - /** - * Removes any classes provided in the guacDraggedClass attribute. - * Further propagation and default behavior of the given event is - * automatically prevented. - * - * @param {Event} e - * The event related to the end of the drag/drop operation. - */ - const notifyDragEnd = function notifyDragEnd(e) { - - e.preventDefault(); - e.stopPropagation(); - - // Skip further processing if no classes were provided - if (!guacDraggedClass) - return; - - // Remove each provided class - guacDraggedClass.split(' ').forEach(classToRemove => - element.classList.remove(classToRemove)); - - }; - - // Add listeners to the drop target to ensure that the visual state - // stays up to date - element.addEventListener('dragenter', notifyDragStart); - element.addEventListener('dragover', notifyDragStart); - element.addEventListener('dragleave', notifyDragEnd); - - /** - * Event listener that will be invoked if the user drops anything - * onto the event. If a valid file is provided, the onFile callback - * provided to this directive will be called; otherwise an error - * will be displayed, if appropriate. - * - * @param {Event} e - * The drop event that triggered this handler. - */ - element.addEventListener('drop', e => { - - notifyDragEnd(e); - - const files = e.dataTransfer.files; - - // Ignore any non-files that are dragged into the drop area - if (files.length < 1) - return; - - // If multi-file upload is disabled, If more than one file was - // provided, print an error explaining the problem - if (!guacMultiple && files.length >= 2) { - - guacNotification.showStatus({ - className : 'error', - title : 'APP.DIALOG_HEADER_ERROR', - text: { key : 'APP.ERROR_SINGLE_FILE_ONLY'}, - - // Add a button to hide the error - actions : [{ - name : 'APP.ACTION_ACKNOWLEDGE', - callback : () => guacNotification.showStatus(false) - }] - }); - return; - - } - - // Invoke the callback with the files. Note that if guacMultiple - // is set to false, this will always be a single file. - guacDrop(files); - - }); - - } // end guacDrop link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacFocus.js b/guacamole/src/main/frontend/src/app/element/directives/guacFocus.js deleted file mode 100644 index 5087e7f92c..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacFocus.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows elements to be manually focused / blurred. - */ -angular.module('element').directive('guacFocus', ['$injector', function guacFocus($injector) { - - // Required services - var $parse = $injector.get('$parse'); - var $timeout = $injector.get('$timeout'); - - return { - restrict: 'A', - - link: function linkGuacFocus($scope, $element, $attrs) { - - /** - * Whether the element associated with this directive should be - * focussed. - * - * @type Boolean - */ - var guacFocus = $parse($attrs.guacFocus); - - /** - * The element which will be focused / blurred. - * - * @type Element - */ - var element = $element[0]; - - // Set/unset focus depending on value of guacFocus - $scope.$watch(guacFocus, function updateFocus(value) { - $timeout(function updateFocusAfterRender() { - if (value) - element.focus(); - else - element.blur(); - }); - }); - - } // end guacFocus link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacMarker.js b/guacamole/src/main/frontend/src/app/element/directives/guacMarker.js deleted file mode 100644 index 3ca15bdcca..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacMarker.js +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which stores a marker which refers to a specific element, - * allowing that element to be scrolled into view when desired. - */ -angular.module('element').directive('guacMarker', ['$injector', function guacMarker($injector) { - - // Required types - var Marker = $injector.get('Marker'); - - // Required services - var $parse = $injector.get('$parse'); - - return { - restrict: 'A', - - link: function linkGuacMarker($scope, $element, $attrs) { - - /** - * The property in which a new Marker should be stored. The new - * Marker will refer to the element associated with this directive. - * - * @type Marker - */ - var guacMarker = $parse($attrs.guacMarker); - - /** - * The element to associate with the new Marker. - * - * @type Element - */ - var element = $element[0]; - - // Assign new marker - guacMarker.assign($scope, new Marker(element)); - - } - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacResize.js b/guacamole/src/main/frontend/src/app/element/directives/guacResize.js deleted file mode 100644 index 0d8794a778..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacResize.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which calls a given callback when its associated element is - * resized. This will modify the internal DOM tree of the associated element, - * and the associated element MUST have position (for example, - * "position: relative"). - */ -angular.module('element').directive('guacResize', ['$document', function guacResize($document) { - - return { - restrict: 'A', - - link: function linkGuacResize($scope, $element, $attrs) { - - /** - * The function to call whenever the associated element is - * resized. The function will be passed the width and height of - * the element, in pixels. - * - * @type Function - */ - var guacResize = $scope.$eval($attrs.guacResize); - - /** - * The element which will monitored for size changes. - * - * @type Element - */ - var element = $element[0]; - - /** - * The resize sensor - an HTML object element. - * - * @type HTMLObjectElement - */ - var resizeSensor = $document[0].createElement('object'); - - /** - * The width of the associated element, in pixels. - * - * @type Number - */ - var lastWidth = element.offsetWidth; - - /** - * The height of the associated element, in pixels. - * - * @type Number - */ - var lastHeight = element.offsetHeight; - - /** - * Checks whether the size of the associated element has changed - * and, if so, calls the resize callback with the new width and - * height as parameters. - */ - var checkSize = function checkSize() { - - // Call callback only if size actually changed - if (element.offsetWidth !== lastWidth - || element.offsetHeight !== lastHeight) { - - // Call resize callback, if defined - if (guacResize) { - $scope.$evalAsync(function elementSizeChanged() { - guacResize(element.offsetWidth, element.offsetHeight); - }); - } - - // Update stored size - lastWidth = element.offsetWidth; - lastHeight = element.offsetHeight; - - } - - }; - - // Register event listener once window object exists - resizeSensor.onload = function resizeSensorReady() { - resizeSensor.contentDocument.defaultView.addEventListener('resize', checkSize); - checkSize(); - }; - - // Load blank contents - resizeSensor.className = 'resize-sensor'; - resizeSensor.type = 'text/html'; - resizeSensor.data = 'app/element/templates/blank.html'; - - // Add resize sensor to associated element - element.insertBefore(resizeSensor, element.firstChild); - - } // end guacResize link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacScroll.js b/guacamole/src/main/frontend/src/app/element/directives/guacScroll.js deleted file mode 100644 index 630897d238..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacScroll.js +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows elements to be manually scrolled, and for their - * scroll state to be observed. - */ -angular.module('element').directive('guacScroll', [function guacScroll() { - - return { - restrict: 'A', - - link: function linkGuacScroll($scope, $element, $attrs) { - - /** - * The current scroll state of the element. - * - * @type ScrollState - */ - var guacScroll = $scope.$eval($attrs.guacScroll); - - /** - * The element which is being scrolled, or monitored for changes - * in scroll. - * - * @type Element - */ - var element = $element[0]; - - /** - * Returns the current left edge of the scrolling rectangle. - * - * @returns {Number} - * The current left edge of the scrolling rectangle. - */ - var getScrollLeft = function getScrollLeft() { - return guacScroll.left; - }; - - /** - * Returns the current top edge of the scrolling rectangle. - * - * @returns {Number} - * The current top edge of the scrolling rectangle. - */ - var getScrollTop = function getScrollTop() { - return guacScroll.top; - }; - - // Update underlying scrollLeft property when left changes - $scope.$watch(getScrollLeft, function scrollLeftChanged(left) { - element.scrollLeft = left; - guacScroll.left = element.scrollLeft; - }); - - // Update underlying scrollTop property when top changes - $scope.$watch(getScrollTop, function scrollTopChanged(top) { - element.scrollTop = top; - guacScroll.top = element.scrollTop; - }); - - } // end guacScroll link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js b/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js deleted file mode 100644 index d1c10a9daa..0000000000 --- a/guacamole/src/main/frontend/src/app/element/directives/guacUpload.js +++ /dev/null @@ -1,103 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows files to be uploaded. Clicking on the associated - * element will result in a file selector dialog, which then calls the provided - * callback function with any chosen files. - */ -angular.module('element').directive('guacUpload', ['$document', function guacUpload($document) { - - return { - restrict: 'A', - - link: function linkGuacUpload($scope, $element, $attrs) { - - /** - * The function to call whenever files are chosen. The callback is - * provided a single parameter: the FileList containing all chosen - * files. - * - * @type Function - */ - const guacUpload = $scope.$eval($attrs.guacUpload); - - /** - * Whether upload of multiple files should be allowed. If false, the - * file dialog will only allow a single file to be chosen at once, - * otherwise any number of files may be chosen. Defaults to true if - * not set. - * - * @type Boolean - */ - const guacMultiple = 'guacMultiple' in $attrs - ? $scope.$eval($attrs.guacMultiple) : true; - - /** - * The element which will register the click. - * - * @type Element - */ - const element = $element[0]; - - /** - * Internal form, containing a single file input element. - * - * @type HTMLFormElement - */ - const form = $document[0].createElement('form'); - - /** - * Internal file input element. - * - * @type HTMLInputElement - */ - const input = $document[0].createElement('input'); - - // Init input element - input.type = 'file'; - input.multiple = guacMultiple; - - // Add input element to internal form - form.appendChild(input); - - // Notify of any chosen files - input.addEventListener('change', function filesSelected() { - $scope.$apply(function setSelectedFiles() { - - // Only set chosen files selection is not canceled - if (guacUpload && input.files.length > 0) - guacUpload(input.files); - - // Reset selection - form.reset(); - - }); - }); - - // Open file chooser when element is clicked - element.addEventListener('click', function elementClicked() { - input.click(); - }); - - } // end guacUpload link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/element/templates/blank.html b/guacamole/src/main/frontend/src/app/element/templates/blank.html deleted file mode 100644 index 47dc938be6..0000000000 --- a/guacamole/src/main/frontend/src/app/element/templates/blank.html +++ /dev/null @@ -1,8 +0,0 @@ - - - - - _ - - - diff --git a/guacamole/src/main/frontend/src/app/form/controllers/dateFieldController.js b/guacamole/src/main/frontend/src/app/form/controllers/dateFieldController.js deleted file mode 100644 index 31d85a8a8c..0000000000 --- a/guacamole/src/main/frontend/src/app/form/controllers/dateFieldController.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * Controller for date fields. - */ -angular.module('form').controller('dateFieldController', ['$scope', '$injector', - function dateFieldController($scope, $injector) { - - // Required services - var $filter = $injector.get('$filter'); - - /** - * Options which dictate the behavior of the input field model, as defined - * by https://docs.angularjs.org/api/ng/directive/ngModelOptions - * - * @type Object. - */ - $scope.modelOptions = { - - /** - * Space-delimited list of events on which the model will be updated. - * - * @type String - */ - updateOn : 'blur', - - /** - * The time zone to use when reading/writing the Date object of the - * model. - * - * @type String - */ - timezone : 'UTC' - - }; - - /** - * Parses the date components of the given string into a Date with only the - * date components set. The resulting Date will be in the UTC timezone, - * with the time left as midnight. The input string must be in the format - * YYYY-MM-DD (zero-padded). - * - * @param {String} str - * The date string to parse. - * - * @returns {Date} - * A Date object, in the UTC timezone, with only the date components - * set. - */ - var parseDate = function parseDate(str) { - - // Parse date, return blank if invalid - var parsedDate = new Date(str + 'T00:00Z'); - if (isNaN(parsedDate.getTime())) - return null; - - return parsedDate; - - }; - - // Update typed value when model is changed - $scope.$watch('model', function modelChanged(model) { - $scope.typedValue = (model ? parseDate(model) : null); - }); - - // Update string value in model when typed value is changed - $scope.$watch('typedValue', function typedValueChanged(typedValue) { - $scope.model = (typedValue ? $filter('date')(typedValue, 'yyyy-MM-dd', 'UTC') : ''); - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/controllers/languageFieldController.js b/guacamole/src/main/frontend/src/app/form/controllers/languageFieldController.js deleted file mode 100644 index b7669ffc9d..0000000000 --- a/guacamole/src/main/frontend/src/app/form/controllers/languageFieldController.js +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * Controller for the language field type. The language field type allows the - * user to select a language from the set of languages supported by the - * Guacamole web application. - */ -angular.module('form').controller('languageFieldController', ['$scope', '$injector', - function languageFieldController($scope, $injector) { - - // Required services - var languageService = $injector.get('languageService'); - var requestService = $injector.get('requestService'); - - /** - * A map of all available language keys to their human-readable - * names. - * - * @type Object. - */ - $scope.languages = null; - - // Retrieve defined languages - languageService.getLanguages().then(function languagesRetrieved(languages) { - $scope.languages = languages; - }, requestService.DIE); - - // Interpret undefined/null as empty string - $scope.$watch('model', function setModel(model) { - if (!model && model !== '') - $scope.model = ''; - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/controllers/terminalColorSchemeFieldController.js b/guacamole/src/main/frontend/src/app/form/controllers/terminalColorSchemeFieldController.js deleted file mode 100644 index fb85a501d6..0000000000 --- a/guacamole/src/main/frontend/src/app/form/controllers/terminalColorSchemeFieldController.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * Controller for terminal color scheme fields. - */ -angular.module('form').controller('terminalColorSchemeFieldController', ['$scope', '$injector', - function terminalColorSchemeFieldController($scope, $injector) { - - // Required types - var ColorScheme = $injector.get('ColorScheme'); - - /** - * The currently selected color scheme. If a pre-defined color scheme is - * selected, this will be the connection parameter value associated with - * that color scheme. If a custom color scheme is selected, this will be - * the string "custom". - * - * @type String - */ - $scope.selectedColorScheme = ''; - - /** - * The current custom color scheme, if a custom color scheme has been - * specified. If no custom color scheme has yet been specified, this will - * be a ColorScheme instance that has been initialized to the default - * colors. - * - * @type ColorScheme - */ - $scope.customColorScheme = new ColorScheme(); - - /** - * The array of colors to include within the color picker as pre-defined - * options for convenience. - * - * @type String[] - */ - $scope.defaultPalette = new ColorScheme().colors; - - /** - * Whether the raw details of the custom color scheme should be shown. By - * default, such details are hidden. - * - * @type Boolean - */ - $scope.detailsShown = false; - - /** - * The palette indices of all colors which are considered low-intensity. - * - * @type Number[] - */ - $scope.lowIntensity = [ 0, 1, 2, 3, 4, 5, 6, 7 ]; - - /** - * The palette indices of all colors which are considered high-intensity. - * - * @type Number[] - */ - $scope.highIntensity = [ 8, 9, 10, 11, 12, 13, 14, 15 ]; - - /** - * The string value which is assigned to selectedColorScheme if a custom - * color scheme is selected. - * - * @constant - * @type String - */ - var CUSTOM_COLOR_SCHEME = 'custom'; - - /** - * Returns whether a custom color scheme has been selected. - * - * @returns {Boolean} - * true if a custom color scheme has been selected, false otherwise. - */ - $scope.isCustom = function isCustom() { - return $scope.selectedColorScheme === CUSTOM_COLOR_SCHEME; - }; - - /** - * Shows the raw details of the custom color scheme. If the details are - * already shown, this function has no effect. - */ - $scope.showDetails = function showDetails() { - $scope.detailsShown = true; - }; - - /** - * Hides the raw details of the custom color scheme. If the details are - * already hidden, this function has no effect. - */ - $scope.hideDetails = function hideDetails() { - $scope.detailsShown = false; - }; - - // Keep selected color scheme and custom color scheme in sync with changes - // to model - $scope.$watch('model', function modelChanged(model) { - if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME || (model && !_.includes($scope.field.options, model))) { - $scope.customColorScheme = ColorScheme.fromString(model); - $scope.selectedColorScheme = CUSTOM_COLOR_SCHEME; - } - else - $scope.selectedColorScheme = model || ''; - }); - - // Keep model in sync with changes to selected color scheme - $scope.$watch('selectedColorScheme', function selectedColorSchemeChanged(selectedColorScheme) { - if (!selectedColorScheme) - $scope.model = ''; - else if (selectedColorScheme === CUSTOM_COLOR_SCHEME) - $scope.model = ColorScheme.toString($scope.customColorScheme); - else - $scope.model = selectedColorScheme; - }); - - // Keep model in sync with changes to custom color scheme - $scope.$watch('customColorScheme', function customColorSchemeChanged(customColorScheme) { - if ($scope.selectedColorScheme === CUSTOM_COLOR_SCHEME) - $scope.model = ColorScheme.toString(customColorScheme); - }, true); - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/controllers/timeFieldController.js b/guacamole/src/main/frontend/src/app/form/controllers/timeFieldController.js deleted file mode 100644 index 174ec601d2..0000000000 --- a/guacamole/src/main/frontend/src/app/form/controllers/timeFieldController.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * Controller for time fields. - */ -angular.module('form').controller('timeFieldController', ['$scope', '$injector', - function timeFieldController($scope, $injector) { - - // Required services - var $filter = $injector.get('$filter'); - - /** - * Options which dictate the behavior of the input field model, as defined - * by https://docs.angularjs.org/api/ng/directive/ngModelOptions - * - * @type Object. - */ - $scope.modelOptions = { - - /** - * Space-delimited list of events on which the model will be updated. - * - * @type String - */ - updateOn : 'blur', - - /** - * The time zone to use when reading/writing the Date object of the - * model. - * - * @type String - */ - timezone : 'UTC' - - }; - - /** - * Parses the time components of the given string into a Date with only the - * time components set. The resulting Date will be in the UTC timezone, - * with the date left as 1970-01-01. The input string must be in the format - * HH:MM:SS (zero-padded, 24-hour). - * - * @param {String} str - * The time string to parse. - * - * @returns {Date} - * A Date object, in the UTC timezone, with only the time components - * set. - */ - var parseTime = function parseTime(str) { - - // Parse time, return blank if invalid - var parsedDate = new Date('1970-01-01T' + str + 'Z'); - if (isNaN(parsedDate.getTime())) - return null; - - return parsedDate; - - }; - - // Update typed value when model is changed - $scope.$watch('model', function modelChanged(model) { - $scope.typedValue = (model ? parseTime(model) : null); - }); - - // Update string value in model when typed value is changed - $scope.$watch('typedValue', function typedValueChanged(typedValue) { - $scope.model = (typedValue ? $filter('date')(typedValue, 'HH:mm:ss', 'UTC') : ''); - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/controllers/timeZoneFieldController.js b/guacamole/src/main/frontend/src/app/form/controllers/timeZoneFieldController.js deleted file mode 100644 index 39f0c38ec6..0000000000 --- a/guacamole/src/main/frontend/src/app/form/controllers/timeZoneFieldController.js +++ /dev/null @@ -1,708 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * Controller for time zone fields. Time zone fields use IANA time zone - * database identifiers as the standard representation for each supported time - * zone. These identifiers are also legal Java time zone IDs. - */ -angular.module('form').controller('timeZoneFieldController', ['$scope', '$injector', - function timeZoneFieldController($scope, $injector) { - - /** - * Map of time zone regions to the map of all time zone name/ID pairs - * within those regions. - * - * @type Object.> - */ - $scope.timeZones = { - - "Africa" : { - "Abidjan" : "Africa/Abidjan", - "Accra" : "Africa/Accra", - "Addis Ababa" : "Africa/Addis_Ababa", - "Algiers" : "Africa/Algiers", - "Asmara" : "Africa/Asmara", - "Asmera" : "Africa/Asmera", - "Bamako" : "Africa/Bamako", - "Bangui" : "Africa/Bangui", - "Banjul" : "Africa/Banjul", - "Bissau" : "Africa/Bissau", - "Blantyre" : "Africa/Blantyre", - "Brazzaville" : "Africa/Brazzaville", - "Bujumbura" : "Africa/Bujumbura", - "Cairo" : "Africa/Cairo", - "Casablanca" : "Africa/Casablanca", - "Ceuta" : "Africa/Ceuta", - "Conakry" : "Africa/Conakry", - "Dakar" : "Africa/Dakar", - "Dar es Salaam" : "Africa/Dar_es_Salaam", - "Djibouti" : "Africa/Djibouti", - "Douala" : "Africa/Douala", - "El Aaiun" : "Africa/El_Aaiun", - "Freetown" : "Africa/Freetown", - "Gaborone" : "Africa/Gaborone", - "Harare" : "Africa/Harare", - "Johannesburg" : "Africa/Johannesburg", - "Juba" : "Africa/Juba", - "Kampala" : "Africa/Kampala", - "Khartoum" : "Africa/Khartoum", - "Kigali" : "Africa/Kigali", - "Kinshasa" : "Africa/Kinshasa", - "Lagos" : "Africa/Lagos", - "Libreville" : "Africa/Libreville", - "Lome" : "Africa/Lome", - "Luanda" : "Africa/Luanda", - "Lubumbashi" : "Africa/Lubumbashi", - "Lusaka" : "Africa/Lusaka", - "Malabo" : "Africa/Malabo", - "Maputo" : "Africa/Maputo", - "Maseru" : "Africa/Maseru", - "Mbabane" : "Africa/Mbabane", - "Mogadishu" : "Africa/Mogadishu", - "Monrovia" : "Africa/Monrovia", - "Nairobi" : "Africa/Nairobi", - "Ndjamena" : "Africa/Ndjamena", - "Niamey" : "Africa/Niamey", - "Nouakchott" : "Africa/Nouakchott", - "Ouagadougou" : "Africa/Ouagadougou", - "Porto-Novo" : "Africa/Porto-Novo", - "Sao Tome" : "Africa/Sao_Tome", - "Timbuktu" : "Africa/Timbuktu", - "Tripoli" : "Africa/Tripoli", - "Tunis" : "Africa/Tunis", - "Windhoek" : "Africa/Windhoek" - }, - - "America" : { - "Adak" : "America/Adak", - "Anchorage" : "America/Anchorage", - "Anguilla" : "America/Anguilla", - "Antigua" : "America/Antigua", - "Araguaina" : "America/Araguaina", - "Argentina / Buenos Aires" : "America/Argentina/Buenos_Aires", - "Argentina / Catamarca" : "America/Argentina/Catamarca", - "Argentina / Comodoro Rivadavia" : "America/Argentina/ComodRivadavia", - "Argentina / Cordoba" : "America/Argentina/Cordoba", - "Argentina / Jujuy" : "America/Argentina/Jujuy", - "Argentina / La Rioja" : "America/Argentina/La_Rioja", - "Argentina / Mendoza" : "America/Argentina/Mendoza", - "Argentina / Rio Gallegos" : "America/Argentina/Rio_Gallegos", - "Argentina / Salta" : "America/Argentina/Salta", - "Argentina / San Juan" : "America/Argentina/San_Juan", - "Argentina / San Luis" : "America/Argentina/San_Luis", - "Argentina / Tucuman" : "America/Argentina/Tucuman", - "Argentina / Ushuaia" : "America/Argentina/Ushuaia", - "Aruba" : "America/Aruba", - "Asuncion" : "America/Asuncion", - "Atikokan" : "America/Atikokan", - "Atka" : "America/Atka", - "Bahia" : "America/Bahia", - "Bahia Banderas" : "America/Bahia_Banderas", - "Barbados" : "America/Barbados", - "Belem" : "America/Belem", - "Belize" : "America/Belize", - "Blanc-Sablon" : "America/Blanc-Sablon", - "Boa Vista" : "America/Boa_Vista", - "Bogota" : "America/Bogota", - "Boise" : "America/Boise", - "Buenos Aires" : "America/Buenos_Aires", - "Cambridge Bay" : "America/Cambridge_Bay", - "Campo Grande" : "America/Campo_Grande", - "Cancun" : "America/Cancun", - "Caracas" : "America/Caracas", - "Catamarca" : "America/Catamarca", - "Cayenne" : "America/Cayenne", - "Cayman" : "America/Cayman", - "Chicago" : "America/Chicago", - "Chihuahua" : "America/Chihuahua", - "Coral Harbour" : "America/Coral_Harbour", - "Cordoba" : "America/Cordoba", - "Costa Rica" : "America/Costa_Rica", - "Creston" : "America/Creston", - "Cuiaba" : "America/Cuiaba", - "Curacao" : "America/Curacao", - "Danmarkshavn" : "America/Danmarkshavn", - "Dawson" : "America/Dawson", - "Dawson Creek" : "America/Dawson_Creek", - "Denver" : "America/Denver", - "Detroit" : "America/Detroit", - "Dominica" : "America/Dominica", - "Edmonton" : "America/Edmonton", - "Eirunepe" : "America/Eirunepe", - "El Salvador" : "America/El_Salvador", - "Ensenada" : "America/Ensenada", - "Fort Wayne" : "America/Fort_Wayne", - "Fortaleza" : "America/Fortaleza", - "Glace Bay" : "America/Glace_Bay", - "Godthab" : "America/Godthab", - "Goose Bay" : "America/Goose_Bay", - "Grand Turk" : "America/Grand_Turk", - "Grenada" : "America/Grenada", - "Guadeloupe" : "America/Guadeloupe", - "Guatemala" : "America/Guatemala", - "Guayaquil" : "America/Guayaquil", - "Guyana" : "America/Guyana", - "Halifax" : "America/Halifax", - "Havana" : "America/Havana", - "Hermosillo" : "America/Hermosillo", - "Indiana / Indianapolis" : "America/Indiana/Indianapolis", - "Indiana / Knox" : "America/Indiana/Knox", - "Indiana / Marengo" : "America/Indiana/Marengo", - "Indiana / Petersburg" : "America/Indiana/Petersburg", - "Indiana / Tell City" : "America/Indiana/Tell_City", - "Indiana / Vevay" : "America/Indiana/Vevay", - "Indiana / Vincennes" : "America/Indiana/Vincennes", - "Indiana / Winamac" : "America/Indiana/Winamac", - "Indianapolis" : "America/Indianapolis", - "Inuvik" : "America/Inuvik", - "Iqaluit" : "America/Iqaluit", - "Jamaica" : "America/Jamaica", - "Jujuy" : "America/Jujuy", - "Juneau" : "America/Juneau", - "Kentucky / Louisville" : "America/Kentucky/Louisville", - "Kentucky / Monticello" : "America/Kentucky/Monticello", - "Kralendijk" : "America/Kralendijk", - "La Paz" : "America/La_Paz", - "Lima" : "America/Lima", - "Los Angeles" : "America/Los_Angeles", - "Louisville" : "America/Louisville", - "Lower Princes" : "America/Lower_Princes", - "Maceio" : "America/Maceio", - "Managua" : "America/Managua", - "Manaus" : "America/Manaus", - "Marigot" : "America/Marigot", - "Martinique" : "America/Martinique", - "Matamoros" : "America/Matamoros", - "Mazatlan" : "America/Mazatlan", - "Mendoza" : "America/Mendoza", - "Menominee" : "America/Menominee", - "Merida" : "America/Merida", - "Metlakatla" : "America/Metlakatla", - "Mexico City" : "America/Mexico_City", - "Miquelon" : "America/Miquelon", - "Moncton" : "America/Moncton", - "Monterrey" : "America/Monterrey", - "Montevideo" : "America/Montevideo", - "Montreal" : "America/Montreal", - "Montserrat" : "America/Montserrat", - "Nassau" : "America/Nassau", - "New York" : "America/New_York", - "Nipigon" : "America/Nipigon", - "Nome" : "America/Nome", - "Noronha" : "America/Noronha", - "North Dakota / Beulah" : "America/North_Dakota/Beulah", - "North Dakota / Center" : "America/North_Dakota/Center", - "North Dakota / New Salem" : "America/North_Dakota/New_Salem", - "Ojinaga" : "America/Ojinaga", - "Panama" : "America/Panama", - "Pangnirtung" : "America/Pangnirtung", - "Paramaribo" : "America/Paramaribo", - "Phoenix" : "America/Phoenix", - "Port-au-Prince" : "America/Port-au-Prince", - "Port of Spain" : "America/Port_of_Spain", - "Porto Acre" : "America/Porto_Acre", - "Porto Velho" : "America/Porto_Velho", - "Puerto Rico" : "America/Puerto_Rico", - "Rainy River" : "America/Rainy_River", - "Rankin Inlet" : "America/Rankin_Inlet", - "Recife" : "America/Recife", - "Regina" : "America/Regina", - "Resolute" : "America/Resolute", - "Rio Branco" : "America/Rio_Branco", - "Rosario" : "America/Rosario", - "Santa Isabel" : "America/Santa_Isabel", - "Santarem" : "America/Santarem", - "Santiago" : "America/Santiago", - "Santo Domingo" : "America/Santo_Domingo", - "Sao Paulo" : "America/Sao_Paulo", - "Scoresbysund" : "America/Scoresbysund", - "Shiprock" : "America/Shiprock", - "Sitka" : "America/Sitka", - "St. Barthelemy" : "America/St_Barthelemy", - "St. Johns" : "America/St_Johns", - "St. Kitts" : "America/St_Kitts", - "St. Lucia" : "America/St_Lucia", - "St. Thomas" : "America/St_Thomas", - "St. Vincent" : "America/St_Vincent", - "Swift Current" : "America/Swift_Current", - "Tegucigalpa" : "America/Tegucigalpa", - "Thule" : "America/Thule", - "Thunder Bay" : "America/Thunder_Bay", - "Tijuana" : "America/Tijuana", - "Toronto" : "America/Toronto", - "Tortola" : "America/Tortola", - "Vancouver" : "America/Vancouver", - "Virgin" : "America/Virgin", - "Whitehorse" : "America/Whitehorse", - "Winnipeg" : "America/Winnipeg", - "Yakutat" : "America/Yakutat", - "Yellowknife" : "America/Yellowknife" - }, - - "Antarctica" : { - "Casey" : "Antarctica/Casey", - "Davis" : "Antarctica/Davis", - "Dumont d'Urville" : "Antarctica/DumontDUrville", - "Macquarie" : "Antarctica/Macquarie", - "Mawson" : "Antarctica/Mawson", - "McMurdo" : "Antarctica/McMurdo", - "Palmer" : "Antarctica/Palmer", - "Rothera" : "Antarctica/Rothera", - "South Pole" : "Antarctica/South_Pole", - "Syowa" : "Antarctica/Syowa", - "Troll" : "Antarctica/Troll", - "Vostok" : "Antarctica/Vostok" - }, - - "Arctic" : { - "Longyearbyen" : "Arctic/Longyearbyen" - }, - - "Asia" : { - "Aden" : "Asia/Aden", - "Almaty" : "Asia/Almaty", - "Amman" : "Asia/Amman", - "Anadyr" : "Asia/Anadyr", - "Aqtau" : "Asia/Aqtau", - "Aqtobe" : "Asia/Aqtobe", - "Ashgabat" : "Asia/Ashgabat", - "Ashkhabad" : "Asia/Ashkhabad", - "Baghdad" : "Asia/Baghdad", - "Bahrain" : "Asia/Bahrain", - "Baku" : "Asia/Baku", - "Bangkok" : "Asia/Bangkok", - "Beirut" : "Asia/Beirut", - "Bishkek" : "Asia/Bishkek", - "Brunei" : "Asia/Brunei", - "Calcutta" : "Asia/Calcutta", - "Chita" : "Asia/Chita", - "Choibalsan" : "Asia/Choibalsan", - "Chongqing" : "Asia/Chongqing", - "Colombo" : "Asia/Colombo", - "Dacca" : "Asia/Dacca", - "Damascus" : "Asia/Damascus", - "Dhaka" : "Asia/Dhaka", - "Dili" : "Asia/Dili", - "Dubai" : "Asia/Dubai", - "Dushanbe" : "Asia/Dushanbe", - "Gaza" : "Asia/Gaza", - "Harbin" : "Asia/Harbin", - "Hebron" : "Asia/Hebron", - "Ho Chi Minh" : "Asia/Ho_Chi_Minh", - "Hong Kong" : "Asia/Hong_Kong", - "Hovd" : "Asia/Hovd", - "Irkutsk" : "Asia/Irkutsk", - "Istanbul" : "Asia/Istanbul", - "Jakarta" : "Asia/Jakarta", - "Jayapura" : "Asia/Jayapura", - "Jerusalem" : "Asia/Jerusalem", - "Kabul" : "Asia/Kabul", - "Kamchatka" : "Asia/Kamchatka", - "Karachi" : "Asia/Karachi", - "Kashgar" : "Asia/Kashgar", - "Kathmandu" : "Asia/Kathmandu", - "Katmandu" : "Asia/Katmandu", - "Khandyga" : "Asia/Khandyga", - "Kolkata" : "Asia/Kolkata", - "Krasnoyarsk" : "Asia/Krasnoyarsk", - "Kuala Lumpur" : "Asia/Kuala_Lumpur", - "Kuching" : "Asia/Kuching", - "Kuwait" : "Asia/Kuwait", - "Macao" : "Asia/Macao", - "Macau" : "Asia/Macau", - "Magadan" : "Asia/Magadan", - "Makassar" : "Asia/Makassar", - "Manila" : "Asia/Manila", - "Muscat" : "Asia/Muscat", - "Nicosia" : "Asia/Nicosia", - "Novokuznetsk" : "Asia/Novokuznetsk", - "Novosibirsk" : "Asia/Novosibirsk", - "Omsk" : "Asia/Omsk", - "Oral" : "Asia/Oral", - "Phnom Penh" : "Asia/Phnom_Penh", - "Pontianak" : "Asia/Pontianak", - "Pyongyang" : "Asia/Pyongyang", - "Qatar" : "Asia/Qatar", - "Qyzylorda" : "Asia/Qyzylorda", - "Rangoon" : "Asia/Rangoon", - "Riyadh" : "Asia/Riyadh", - "Saigon" : "Asia/Saigon", - "Sakhalin" : "Asia/Sakhalin", - "Samarkand" : "Asia/Samarkand", - "Seoul" : "Asia/Seoul", - "Shanghai" : "Asia/Shanghai", - "Singapore" : "Asia/Singapore", - "Srednekolymsk" : "Asia/Srednekolymsk", - "Taipei" : "Asia/Taipei", - "Tashkent" : "Asia/Tashkent", - "Tbilisi" : "Asia/Tbilisi", - "Tehran" : "Asia/Tehran", - "Tel Aviv" : "Asia/Tel_Aviv", - "Thimbu" : "Asia/Thimbu", - "Thimphu" : "Asia/Thimphu", - "Tokyo" : "Asia/Tokyo", - "Ujung Pandang" : "Asia/Ujung_Pandang", - "Ulaanbaatar" : "Asia/Ulaanbaatar", - "Ulan Bator" : "Asia/Ulan_Bator", - "Urumqi" : "Asia/Urumqi", - "Ust-Nera" : "Asia/Ust-Nera", - "Vientiane" : "Asia/Vientiane", - "Vladivostok" : "Asia/Vladivostok", - "Yakutsk" : "Asia/Yakutsk", - "Yekaterinburg" : "Asia/Yekaterinburg", - "Yerevan" : "Asia/Yerevan" - }, - - "Atlantic" : { - "Azores" : "Atlantic/Azores", - "Bermuda" : "Atlantic/Bermuda", - "Canary" : "Atlantic/Canary", - "Cape Verde" : "Atlantic/Cape_Verde", - "Faeroe" : "Atlantic/Faeroe", - "Faroe" : "Atlantic/Faroe", - "Jan Mayen" : "Atlantic/Jan_Mayen", - "Madeira" : "Atlantic/Madeira", - "Reykjavik" : "Atlantic/Reykjavik", - "South Georgia" : "Atlantic/South_Georgia", - "St. Helena" : "Atlantic/St_Helena", - "Stanley" : "Atlantic/Stanley" - }, - - "Australia" : { - "Adelaide" : "Australia/Adelaide", - "Brisbane" : "Australia/Brisbane", - "Broken Hill" : "Australia/Broken_Hill", - "Canberra" : "Australia/Canberra", - "Currie" : "Australia/Currie", - "Darwin" : "Australia/Darwin", - "Eucla" : "Australia/Eucla", - "Hobart" : "Australia/Hobart", - "Lindeman" : "Australia/Lindeman", - "Lord Howe" : "Australia/Lord_Howe", - "Melbourne" : "Australia/Melbourne", - "North" : "Australia/North", - "Perth" : "Australia/Perth", - "Queensland" : "Australia/Queensland", - "South" : "Australia/South", - "Sydney" : "Australia/Sydney", - "Tasmania" : "Australia/Tasmania", - "Victoria" : "Australia/Victoria", - "West" : "Australia/West", - "Yancowinna" : "Australia/Yancowinna" - }, - - "Brazil" : { - "Acre" : "Brazil/Acre", - "Fernando de Noronha" : "Brazil/DeNoronha", - "East" : "Brazil/East", - "West" : "Brazil/West" - }, - - "Canada" : { - "Atlantic" : "Canada/Atlantic", - "Central" : "Canada/Central", - "Eastern" : "Canada/Eastern", - "Mountain" : "Canada/Mountain", - "Newfoundland" : "Canada/Newfoundland", - "Pacific" : "Canada/Pacific", - "Saskatchewan" : "Canada/Saskatchewan", - "Yukon" : "Canada/Yukon" - }, - - "Chile" : { - "Continental" : "Chile/Continental", - "Easter Island" : "Chile/EasterIsland" - }, - - "Europe" : { - "Amsterdam" : "Europe/Amsterdam", - "Andorra" : "Europe/Andorra", - "Athens" : "Europe/Athens", - "Belfast" : "Europe/Belfast", - "Belgrade" : "Europe/Belgrade", - "Berlin" : "Europe/Berlin", - "Bratislava" : "Europe/Bratislava", - "Brussels" : "Europe/Brussels", - "Bucharest" : "Europe/Bucharest", - "Budapest" : "Europe/Budapest", - "Busingen" : "Europe/Busingen", - "Chisinau" : "Europe/Chisinau", - "Copenhagen" : "Europe/Copenhagen", - "Dublin" : "Europe/Dublin", - "Gibraltar" : "Europe/Gibraltar", - "Guernsey" : "Europe/Guernsey", - "Helsinki" : "Europe/Helsinki", - "Isle of Man" : "Europe/Isle_of_Man", - "Istanbul" : "Europe/Istanbul", - "Jersey" : "Europe/Jersey", - "Kaliningrad" : "Europe/Kaliningrad", - "Kiev" : "Europe/Kiev", - "Lisbon" : "Europe/Lisbon", - "Ljubljana" : "Europe/Ljubljana", - "London" : "Europe/London", - "Luxembourg" : "Europe/Luxembourg", - "Madrid" : "Europe/Madrid", - "Malta" : "Europe/Malta", - "Mariehamn" : "Europe/Mariehamn", - "Minsk" : "Europe/Minsk", - "Monaco" : "Europe/Monaco", - "Moscow" : "Europe/Moscow", - "Nicosia" : "Europe/Nicosia", - "Oslo" : "Europe/Oslo", - "Paris" : "Europe/Paris", - "Podgorica" : "Europe/Podgorica", - "Prague" : "Europe/Prague", - "Riga" : "Europe/Riga", - "Rome" : "Europe/Rome", - "Samara" : "Europe/Samara", - "San Marino" : "Europe/San_Marino", - "Sarajevo" : "Europe/Sarajevo", - "Simferopol" : "Europe/Simferopol", - "Skopje" : "Europe/Skopje", - "Sofia" : "Europe/Sofia", - "Stockholm" : "Europe/Stockholm", - "Tallinn" : "Europe/Tallinn", - "Tirane" : "Europe/Tirane", - "Tiraspol" : "Europe/Tiraspol", - "Uzhgorod" : "Europe/Uzhgorod", - "Vaduz" : "Europe/Vaduz", - "Vatican" : "Europe/Vatican", - "Vienna" : "Europe/Vienna", - "Vilnius" : "Europe/Vilnius", - "Volgograd" : "Europe/Volgograd", - "Warsaw" : "Europe/Warsaw", - "Zagreb" : "Europe/Zagreb", - "Zaporozhye" : "Europe/Zaporozhye", - "Zurich" : "Europe/Zurich" - }, - - "GMT" : { - "GMT-14" : "Etc/GMT-14", - "GMT-13" : "Etc/GMT-13", - "GMT-12" : "Etc/GMT-12", - "GMT-11" : "Etc/GMT-11", - "GMT-10" : "Etc/GMT-10", - "GMT-9" : "Etc/GMT-9", - "GMT-8" : "Etc/GMT-8", - "GMT-7" : "Etc/GMT-7", - "GMT-6" : "Etc/GMT-6", - "GMT-5" : "Etc/GMT-5", - "GMT-4" : "Etc/GMT-4", - "GMT-3" : "Etc/GMT-3", - "GMT-2" : "Etc/GMT-2", - "GMT-1" : "Etc/GMT-1", - "GMT+0" : "Etc/GMT+0", - "GMT+1" : "Etc/GMT+1", - "GMT+2" : "Etc/GMT+2", - "GMT+3" : "Etc/GMT+3", - "GMT+4" : "Etc/GMT+4", - "GMT+5" : "Etc/GMT+5", - "GMT+6" : "Etc/GMT+6", - "GMT+7" : "Etc/GMT+7", - "GMT+8" : "Etc/GMT+8", - "GMT+9" : "Etc/GMT+9", - "GMT+10" : "Etc/GMT+10", - "GMT+11" : "Etc/GMT+11", - "GMT+12" : "Etc/GMT+12" - }, - - "Indian" : { - "Antananarivo" : "Indian/Antananarivo", - "Chagos" : "Indian/Chagos", - "Christmas" : "Indian/Christmas", - "Cocos" : "Indian/Cocos", - "Comoro" : "Indian/Comoro", - "Kerguelen" : "Indian/Kerguelen", - "Mahe" : "Indian/Mahe", - "Maldives" : "Indian/Maldives", - "Mauritius" : "Indian/Mauritius", - "Mayotte" : "Indian/Mayotte", - "Reunion" : "Indian/Reunion" - }, - - "Mexico" : { - "Baja Norte" : "Mexico/BajaNorte", - "Baja Sur" : "Mexico/BajaSur", - "General" : "Mexico/General" - }, - - "Pacific" : { - "Apia" : "Pacific/Apia", - "Auckland" : "Pacific/Auckland", - "Bougainville" : "Pacific/Bougainville", - "Chatham" : "Pacific/Chatham", - "Chuuk" : "Pacific/Chuuk", - "Easter" : "Pacific/Easter", - "Efate" : "Pacific/Efate", - "Enderbury" : "Pacific/Enderbury", - "Fakaofo" : "Pacific/Fakaofo", - "Fiji" : "Pacific/Fiji", - "Funafuti" : "Pacific/Funafuti", - "Galapagos" : "Pacific/Galapagos", - "Gambier" : "Pacific/Gambier", - "Guadalcanal" : "Pacific/Guadalcanal", - "Guam" : "Pacific/Guam", - "Honolulu" : "Pacific/Honolulu", - "Johnston" : "Pacific/Johnston", - "Kiritimati" : "Pacific/Kiritimati", - "Kosrae" : "Pacific/Kosrae", - "Kwajalein" : "Pacific/Kwajalein", - "Majuro" : "Pacific/Majuro", - "Marquesas" : "Pacific/Marquesas", - "Midway" : "Pacific/Midway", - "Nauru" : "Pacific/Nauru", - "Niue" : "Pacific/Niue", - "Norfolk" : "Pacific/Norfolk", - "Noumea" : "Pacific/Noumea", - "Pago Pago" : "Pacific/Pago_Pago", - "Palau" : "Pacific/Palau", - "Pitcairn" : "Pacific/Pitcairn", - "Pohnpei" : "Pacific/Pohnpei", - "Ponape" : "Pacific/Ponape", - "Port Moresby" : "Pacific/Port_Moresby", - "Rarotonga" : "Pacific/Rarotonga", - "Saipan" : "Pacific/Saipan", - "Samoa" : "Pacific/Samoa", - "Tahiti" : "Pacific/Tahiti", - "Tarawa" : "Pacific/Tarawa", - "Tongatapu" : "Pacific/Tongatapu", - "Truk" : "Pacific/Truk", - "Wake" : "Pacific/Wake", - "Wallis" : "Pacific/Wallis", - "Yap" : "Pacific/Yap" - } - - }; - - /** - * All selectable regions. - * - * @type String[] - */ - $scope.regions = (function collectRegions() { - - // Start with blank entry - var regions = [ '' ]; - - // Add each available region - for (var region in $scope.timeZones) - regions.push(region); - - return regions; - - })(); - - /** - * Direct mapping of all time zone IDs to the region containing that ID. - * - * @type Object. - */ - var timeZoneRegions = (function mapRegions() { - - var regions = {}; - - // For each available region - for (var region in $scope.timeZones) { - - // Get time zones within that region - var timeZonesInRegion = $scope.timeZones[region]; - - // For each of those time zones - for (var timeZoneName in timeZonesInRegion) { - - // Get corresponding ID - var timeZoneID = timeZonesInRegion[timeZoneName]; - - // Store region in map - regions[timeZoneID] = region; - - } - - } - - return regions; - - })(); - - /** - * Map of regions to the currently selected time zone for that region. - * Initially, all regions will be set to default selections (the first - * time zone, sorted lexicographically). - * - * @type Object. - */ - var selectedTimeZone = (function produceDefaultTimeZones() { - - var defaultTimeZone = {}; - - // For each available region - for (var region in $scope.timeZones) { - - // Get time zones within that region - var timeZonesInRegion = $scope.timeZones[region]; - - // No default initially - var defaultZoneName = null; - var defaultZoneID = null; - - // For each of those time zones - for (var timeZoneName in timeZonesInRegion) { - - // Get corresponding ID - var timeZoneID = timeZonesInRegion[timeZoneName]; - - // Set as default if earlier than existing default - if (!defaultZoneName || timeZoneName < defaultZoneName) { - defaultZoneName = timeZoneName; - defaultZoneID = timeZoneID; - } - - } - - // Store default zone - defaultTimeZone[region] = defaultZoneID; - - } - - return defaultTimeZone; - - })(); - - /** - * The name of the region currently selected. The selected region narrows - * which time zones are selectable. - * - * @type String - */ - $scope.region = ''; - - // Ensure corresponding region is selected - $scope.$watch('model', function setModel(model) { - $scope.region = timeZoneRegions[model] || ''; - selectedTimeZone[$scope.region] = model; - }); - - // Restore time zone selection when region changes - $scope.$watch('region', function restoreSelection(region) { - $scope.model = selectedTimeZone[region] || null; - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/directives/form.js b/guacamole/src/main/frontend/src/app/form/directives/form.js deleted file mode 100644 index 4de051bfd9..0000000000 --- a/guacamole/src/main/frontend/src/app/form/directives/form.js +++ /dev/null @@ -1,307 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global _ */ - -/** - * A directive that allows editing of a collection of fields. - */ -angular.module('form').directive('guacForm', [function form() { - - return { - // Element only - restrict: 'E', - replace: true, - scope: { - - /** - * The translation namespace of the translation strings that will - * be generated for all fields. This namespace is absolutely - * required. If this namespace is omitted, all generated - * translation strings will be placed within the MISSING_NAMESPACE - * namespace, as a warning. - * - * @type String - */ - namespace : '=', - - /** - * The form content to display. This may be a form, an array of - * forms, or a simple array of fields. - * - * @type Form[]|Form|Field[]|Field - */ - content : '=', - - /** - * The object which will receive all field values. Each field value - * will be assigned to the property of this object having the same - * name. - * - * @type Object. - */ - model : '=', - - /** - * Whether the contents of the form should be restricted to those - * fields/forms which match properties defined within the given - * model object. By default, all fields will be shown. - * - * @type Boolean - */ - modelOnly : '=', - - /** - * Whether the contents of the form should be rendered as disabled. - * By default, form fields are enabled. - * - * @type Boolean - */ - disabled : '=', - - /** - * The name of the field to be focused, if any. - * - * @type String - */ - focused : '=', - - /** - * The client associated with this form, if any. - * - * NOTE: If the provided client has any managed arguments in the - * pending state, any fields with the same name rendered by this - * form will be disabled. The fields will be re-enabled when guacd - * sends an updated argument with a the same name. - * - * @type ManagedClient - */ - client: '=' - - }, - templateUrl: 'app/form/templates/form.html', - controller: ['$scope', '$injector', function formController($scope, $injector) { - - // Required services - var formService = $injector.get('formService'); - var translationStringService = $injector.get('translationStringService'); - - /** - * The array of all forms to display. - * - * @type Form[] - */ - $scope.forms = []; - - /** - * The object which will receive all field values. Normally, this - * will be the object provided within the "model" attribute. If - * no such object has been provided, a blank model will be used - * instead as a placeholder, such that the fields of this form - * will have something to bind to. - * - * @type Object. - */ - $scope.values = {}; - - /** - * Produces the translation string for the section header of the - * given form. The translation string will be of the form: - * - * NAMESPACE.SECTION_HEADER_NAME - * - * where NAMESPACE is the namespace provided to the - * directive and NAME is the form name transformed - * via translationStringService.canonicalize(). - * - * @param {Form} form - * The form for which to produce the translation string. - * - * @returns {String} - * The translation string which produces the translated header - * of the form. - */ - $scope.getSectionHeader = function getSectionHeader(form) { - - // If no form, or no name, then no header - if (!form || !form.name) - return ''; - - return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') - + '.SECTION_HEADER_' + translationStringService.canonicalize(form.name); - - }; - - /** - * Returns an object as would be provided to the ngClass directive - * that defines the CSS classes that should be applied to the given - * form. - * - * @param {Form} form - * The form to generate the CSS classes for. - * - * @return {!Object.} - * The ngClass object defining the CSS classes for the given - * form. - */ - $scope.getFormClasses = function getFormClasses(form) { - return formService.getClasses('form-', form); - }; - - /** - * Determines whether the given object is a form, under the - * assumption that the object is either a form or a field. - * - * @param {Form|Field} obj - * The object to test. - * - * @returns {Boolean} - * true if the given object appears to be a form, false - * otherwise. - */ - var isForm = function isForm(obj) { - return !!('name' in obj && 'fields' in obj); - }; - - // Produce set of forms from any given content - $scope.$watch('content', function setContent(content) { - - // If no content provided, there are no forms - if (!content) { - $scope.forms = []; - return; - } - - // Ensure content is an array - if (!angular.isArray(content)) - content = [content]; - - // If content is an array of fields, convert to an array of forms - if (content.length && !isForm(content[0])) { - content = [{ - fields : content - }]; - } - - // Content is now an array of forms - $scope.forms = content; - - }); - - // Update string value and re-assign to model when field is changed - $scope.$watch('model', function setModel(model) { - - // Assign new model only if provided - if (model) - $scope.values = model; - - // Otherwise, use blank model - else - $scope.values = {}; - - }); - - /** - * Returns whether the given field should be focused or not. - * - * @param {Field} field - * The field to check. - * - * @returns {Boolean} - * true if the given field should be focused, false otherwise. - */ - $scope.isFocused = function isFocused(field) { - return field && (field.name === $scope.focused); - }; - - /** - * Returns whether the given field should be displayed to the - * current user. - * - * @param {Field} field - * The field to check. - * - * @returns {Boolean} - * true if the given field should be visible, false otherwise. - */ - $scope.isVisible = function isVisible(field) { - - // All fields are visible if contents are not restricted to - // model properties only - if (!$scope.modelOnly) - return true; - - // Otherwise, fields are only visible if they are present - // within the model - return field && (field.name in $scope.values); - - }; - - - /** - * Returns whether the given field should be disabled (read-only) - * when presented to the current user. - * - * @param {Field} field - * The field to check. - * - * @returns {Boolean} - * true if the given field should be disabled, false otherwise. - */ - $scope.isDisabled = function isDisabled(field) { - - /* - * The field is disabled if either the form as a whole is disabled, - * or if a client is provided to the directive, and the field is - * marked as pending. - */ - return $scope.disabled || - _.get($scope.client, ['arguments', field.name, 'pending']); - }; - - /** - * Returns whether at least one of the given fields should be - * displayed to the current user. - * - * @param {Field[]} fields - * The array of fields to check. - * - * @returns {Boolean} - * true if at least one field within the given array should be - * visible, false otherwise. - */ - $scope.containsVisible = function containsVisible(fields) { - - // If fields are defined, check whether at least one is visible - if (fields) { - for (var i = 0; i < fields.length; i++) { - if ($scope.isVisible(fields[i])) - return true; - } - } - - // Otherwise, there are no visible fields - return false; - - }; - - }] // end controller - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/directives/formField.js b/guacamole/src/main/frontend/src/app/form/directives/formField.js deleted file mode 100644 index 73ffb127e9..0000000000 --- a/guacamole/src/main/frontend/src/app/form/directives/formField.js +++ /dev/null @@ -1,210 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * A directive that allows editing of a field. - */ -angular.module('form').directive('guacFormField', [function formField() { - - return { - // Element only - restrict: 'E', - replace: true, - scope: { - - /** - * The translation namespace of the translation strings that will - * be generated for this field. This namespace is absolutely - * required. If this namespace is omitted, all generated - * translation strings will be placed within the MISSING_NAMESPACE - * namespace, as a warning. - * - * @type String - */ - namespace : '=', - - /** - * The field to display. - * - * @type Field - */ - field : '=', - - /** - * The property which contains this fields current value. When this - * field changes, the property will be updated accordingly. - * - * @type String - */ - model : '=', - - /** - * Whether this field should be rendered as disabled. By default, - * form fields are enabled. - * - * @type Boolean - */ - disabled : '=', - - /** - * Whether this field should be focused. - * - * @type Boolean - */ - focused : '=', - - /** - * The client associated with this form field, if any. - * - * @type ManagedClient - */ - client: '=' - - }, - templateUrl: 'app/form/templates/formField.html', - controller: ['$scope', '$injector', '$element', function formFieldController($scope, $injector, $element) { - - // Required services - var $log = $injector.get('$log'); - var formService = $injector.get('formService'); - var translationStringService = $injector.get('translationStringService'); - - /** - * The element which should contain any compiled field content. The - * actual content of a field is dynamically determined by its type. - * - * @type Element[] - */ - var fieldContent = $element.find('.form-field'); - - /** - * An ID value which is reasonably likely to be unique relative to - * other elements on the page. This ID should be used to associate - * the relevant input element with the label provided by the - * guacFormField directive, if there is such an input element. - * - * @type String - */ - $scope.fieldId = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() { - return Math.floor(Math.random() * 36).toString(36); - }) + '-' + new Date().getTime().toString(36); - - /** - * Produces the translation string for the header of the current - * field. The translation string will be of the form: - * - * NAMESPACE.FIELD_HEADER_NAME - * - * where NAMESPACE is the namespace provided to the - * directive and NAME is the field name transformed - * via translationStringService.canonicalize(). - * - * @returns {String} - * The translation string which produces the translated header - * of the field. - */ - $scope.getFieldHeader = function getFieldHeader() { - - // If no field, or no name, then no header - if (!$scope.field || !$scope.field.name) - return ''; - - return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') - + '.FIELD_HEADER_' + translationStringService.canonicalize($scope.field.name); - - }; - - /** - * Produces the translation string for the given field option - * value. The translation string will be of the form: - * - * NAMESPACE.FIELD_OPTION_NAME_VALUE - * - * where NAMESPACE is the namespace provided to the - * directive, NAME is the field name transformed - * via translationStringService.canonicalize(), and - * VALUE is the option value transformed via - * translationStringService.canonicalize() - * - * @param {String} value - * The name of the option value. - * - * @returns {String} - * The translation string which produces the translated name of the - * value specified. - */ - $scope.getFieldOption = function getFieldOption(value) { - - // If no field, or no value, then no corresponding translation string - if (!$scope.field || !$scope.field.name) - return ''; - - return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') - + '.FIELD_OPTION_' + translationStringService.canonicalize($scope.field.name) - + '_' + translationStringService.canonicalize(value || 'EMPTY'); - - }; - - /** - * Returns an object as would be provided to the ngClass directive - * that defines the CSS classes that should be applied to this - * field. - * - * @return {!Object.} - * The ngClass object defining the CSS classes for the current - * field. - */ - $scope.getFieldClasses = function getFieldClasses() { - return formService.getClasses('labeled-field-', $scope.field, { - empty: !$scope.model - }); - }; - - /** - * Returns whether the current field should be displayed. - * - * @returns {Boolean} - * true if the current field should be displayed, false - * otherwise. - */ - $scope.isFieldVisible = function isFieldVisible() { - return fieldContent[0].hasChildNodes(); - }; - - // Update field contents when field definition is changed - $scope.$watch('field', function setField(field) { - - // Reset contents - fieldContent.innerHTML = ''; - - // Append field content - if (field) { - formService.insertFieldElement(fieldContent[0], - field.type, $scope)['catch'](function fieldCreationFailed() { - $log.warn('Failed to retrieve field with type "' + field.type + '"'); - }); - } - - }); - - }] // end controller - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/directives/guacInputColor.js b/guacamole/src/main/frontend/src/app/form/directives/guacInputColor.js deleted file mode 100644 index 1762c3172e..0000000000 --- a/guacamole/src/main/frontend/src/app/form/directives/guacInputColor.js +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which implements a color input field. If the underlying color - * picker implementation cannot be used due to a lack of browser support, this - * directive will become read-only, functioning essentially as a color preview. - * - * @see colorPickerService - */ -angular.module('form').directive('guacInputColor', [function guacInputColor() { - - var config = { - restrict: 'E', - replace: true, - templateUrl: 'app/form/templates/guacInputColor.html', - transclude: true - }; - - config.scope = { - - /** - * The current selected color value, in standard 6-digit hexadecimal - * RGB notation. When the user selects a different color using this - * directive, this value will updated accordingly. - * - * @type String - */ - model: '=', - - /** - * An optional array of colors to include within the color picker as a - * convenient selection of pre-defined colors. The colors within the - * array must be in standard 6-digit hexadecimal RGB notation. - * - * @type String[] - */ - palette: '=' - - }; - - config.controller = ['$scope', '$element', '$injector', - function guacInputColorController($scope, $element, $injector) { - - // Required services - var colorPickerService = $injector.get('colorPickerService'); - - /** - * @borrows colorPickerService.isAvailable() - */ - $scope.isColorPickerAvailable = colorPickerService.isAvailable; - - /** - * Returns whether the color currently selected is "dark" in the sense - * that the color white will have higher contrast against it than the - * color black. - * - * @returns {Boolean} - * true if the currently selected color is relatively dark (white - * text would provide better contrast than black), false otherwise. - */ - $scope.isDark = function isDark() { - - // Assume not dark if color is invalid or undefined - var rgb = $scope.model && /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec($scope.model); - if (!rgb) - return false; - - // Parse color component values as hexadecimal - var red = parseInt(rgb[1], 16); - var green = parseInt(rgb[2], 16); - var blue = parseInt(rgb[3], 16); - - // Convert RGB to luminance in HSL space (as defined by the - // relative luminance formula given by the W3C for accessibility) - var luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; - - // Consider the background to be dark if white text over that - // background would provide better contrast than black - return luminance <= 153; // 153 is the component value 0.6 converted from 0-1 to the 0-255 range - - }; - - /** - * Prompts the user to choose a color by displaying a color selection - * dialog. If the user chooses a color, this directive's model is - * automatically updated. If the user cancels the dialog, the model is - * left untouched. - */ - $scope.selectColor = function selectColor() { - colorPickerService.selectColor($element[0], $scope.model, $scope.palette) - .then(function colorSelected(color) { - $scope.model = color; - }, angular.noop); - }; - - }]; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/directives/guacLenientDate.js b/guacamole/src/main/frontend/src/app/form/directives/guacLenientDate.js deleted file mode 100644 index 6b46bfcae9..0000000000 --- a/guacamole/src/main/frontend/src/app/form/directives/guacLenientDate.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which modifies the parsing and formatting of ngModel when used - * on an HTML5 date input field, relaxing the otherwise strict parsing and - * validation behavior. The behavior of this directive for other input elements - * is undefined. - */ -angular.module('form').directive('guacLenientDate', ['$injector', - function guacLenientDate($injector) { - - // Required services - var $filter = $injector.get('$filter'); - - /** - * Directive configuration object. - * - * @type Object. - */ - var config = { - restrict : 'A', - require : 'ngModel' - }; - - // Linking function - config.link = function linkGuacLenientDate($scope, $element, $attrs, ngModel) { - - // Parse date strings leniently - ngModel.$parsers = [function parse(viewValue) { - - // If blank, return null - if (!viewValue) - return null; - - // Match basic date pattern - var match = /([0-9]*)(?:-([0-9]*)(?:-([0-9]*))?)?/.exec(viewValue); - if (!match) - return null; - - // Determine year, month, and day based on pattern - var year = parseInt(match[1] || '0') || new Date().getFullYear(); - var month = parseInt(match[2] || '0') || 1; - var day = parseInt(match[3] || '0') || 1; - - // Convert to Date object - var parsedDate = new Date(Date.UTC(year, month - 1, day)); - if (isNaN(parsedDate.getTime())) - return null; - - return parsedDate; - - }]; - - // Format date strings as "yyyy-MM-dd" - ngModel.$formatters = [function format(modelValue) { - return modelValue ? $filter('date')(modelValue, 'yyyy-MM-dd', 'UTC') : ''; - }]; - - }; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/directives/guacLenientTime.js b/guacamole/src/main/frontend/src/app/form/directives/guacLenientTime.js deleted file mode 100644 index acdb53a671..0000000000 --- a/guacamole/src/main/frontend/src/app/form/directives/guacLenientTime.js +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which modifies the parsing and formatting of ngModel when used - * on an HTML5 time input field, relaxing the otherwise strict parsing and - * validation behavior. The behavior of this directive for other input elements - * is undefined. - */ -angular.module('form').directive('guacLenientTime', ['$injector', - function guacLenientTime($injector) { - - // Required services - var $filter = $injector.get('$filter'); - - /** - * Directive configuration object. - * - * @type Object. - */ - var config = { - restrict : 'A', - require : 'ngModel' - }; - - // Linking function - config.link = function linkGuacLenientTIme($scope, $element, $attrs, ngModel) { - - // Parse time strings leniently - ngModel.$parsers = [function parse(viewValue) { - - // If blank, return null - if (!viewValue) - return null; - - // Match basic time pattern - var match = /([0-9]*)(?::([0-9]*)(?::([0-9]*))?)?(?:\s*(a|p))?/.exec(viewValue.toLowerCase()); - if (!match) - return null; - - // Determine hour, minute, and second based on pattern - var hour = parseInt(match[1] || '0'); - var minute = parseInt(match[2] || '0'); - var second = parseInt(match[3] || '0'); - - // Handle AM/PM - if (match[4]) { - - // Interpret 12 AM as 00:00 and 12 PM as 12:00 - if (hour === 12) - hour = 0; - - // Increment hour to evening if PM - if (match[4] === 'p') - hour += 12; - - } - - // Wrap seconds and minutes into minutes and hours - minute += second / 60; second %= 60; - hour += minute / 60; minute %= 60; - - // Constrain hours to 0 - 23 - hour %= 24; - - // Convert to Date object - var parsedDate = new Date(Date.UTC(1970, 0, 1, hour, minute, second)); - if (isNaN(parsedDate.getTime())) - return null; - - return parsedDate; - - }]; - - // Format time strings as "HH:mm:ss" - ngModel.$formatters = [function format(modelValue) { - return modelValue ? $filter('date')(modelValue, 'HH:mm:ss', 'UTC') : ''; - }]; - - }; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/services/colorPickerService.js b/guacamole/src/main/frontend/src/app/form/services/colorPickerService.js deleted file mode 100644 index d8494a6a6c..0000000000 --- a/guacamole/src/main/frontend/src/app/form/services/colorPickerService.js +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -import '@simonwep/pickr/dist/themes/monolith.min.css' - -/** - * A service for prompting the user to choose a color using the "Pickr" color - * picker. As the Pickr color picker might not be available if the JavaScript - * features it requires are not supported by the browser (Internet Explorer), - * the isAvailable() function should be used to test for usability. - */ -angular.module('form').provider('colorPickerService', function colorPickerServiceProvider() { - - /** - * A singleton instance of the "Pickr" color picker, shared by all users of - * this service. Pickr does not initialize synchronously, nor is it - * supported by all browsers. If Pickr is not yet initialized, or is - * unsupported, this will be null. - * - * @type {Pickr} - */ - var pickr = null; - - /** - * Whether Pickr has completed initialization. - * - * @type {Boolean} - */ - var pickrInitComplete = false; - - /** - * The HTML element to provide to Pickr as the root element. - * - * @type {HTMLDivElement} - */ - var pickerContainer = document.createElement('div'); - pickerContainer.className = 'shared-color-picker'; - - /** - * An instance of Deferred which represents an active request for the - * user to choose a color. The promise associated with the Deferred will - * be resolved with the chosen color once a color is chosen, and rejected - * if the request is cancelled or Pickr is not available. If no request is - * active, this will be null. - * - * @type {Deferred} - */ - var activeRequest = null; - - /** - * Resolves the current active request with the given color value. If no - * color value is provided, the active request is rejected. If no request - * is active, this function has no effect. - * - * @param {String} [color] - * The color value to resolve the active request with. - */ - var completeActiveRequest = function completeActiveRequest(color) { - if (activeRequest) { - - // Hide color picker, if shown - pickr.hide(); - - // Resolve/reject active request depending on value provided - if (color) - activeRequest.resolve(color); - else - activeRequest.reject(); - - // No active request - activeRequest = null; - - } - }; - - try { - pickr = Pickr.create({ - - // Bind color picker to the container element - el : pickerContainer, - - // Wrap color picker dialog in Guacamole-specific class for - // sake of additional styling - appClass : 'guac-input-color-picker', - - 'default' : '#000000', - - // Display color details as hex - defaultRepresentation : 'HEX', - - // Use "monolith" theme, as a nice balance between "nano" (does - // not work in Internet Explorer) and "classic" (too big) - theme : 'monolith', - - // Leverage the container element as the button which shows the - // picker, relying on our own styling for that button - useAsButton : true, - appendToBody : true, - - // Do not include opacity controls - lockOpacity : true, - - // Include a selection of palette entries for convenience and - // reference - swatches : [], - - components: { - - // Include hue and color preview controls - preview : true, - hue : true, - - // Display only a text color input field and the save and - // cancel buttons (no clear button) - interaction: { - input : true, - save : true, - cancel : true - } - - } - - }); - - // Hide color picker after user clicks "cancel" - pickr.on('cancel', function colorChangeCanceled() { - completeActiveRequest(); - }); - - // Keep model in sync with changes to the color picker - pickr.on('save', function colorChanged(color) { - completeActiveRequest(color.toHEXA().toString()); - activeRequest = null; - }); - - // Keep color picker in sync with changes to the model - pickr.on('init', function pickrReady() { - pickrInitComplete = true; - }); - } - catch (e) { - // If the "Pickr" color picker cannot be loaded (Internet Explorer), - // the available flag will remain set to false - } - - // Factory method required by provider - this.$get = ['$injector', function colorPickerServiceFactory($injector) { - - // Required services - var $q = $injector.get('$q'); - var $translate = $injector.get('$translate'); - - var service = {}; - - /** - * Promise which is resolved when Pickr initialization has completed - * and rejected if Pickr cannot be used. - * - * @type {Promise} - */ - var pickrPromise = (function getPickr() { - - var deferred = $q.defer(); - - // Resolve promise when Pickr has completed initialization - if (pickrInitComplete) - deferred.resolve(); - else if (pickr) - pickr.on('init', deferred.resolve); - - // Reject promise if Pickr cannot be used at all - else - deferred.reject(); - - return deferred.promise; - - })(); - - /** - * Returns whether the underlying color picker (Pickr) can be used by - * calling selectColor(). If the browser cannot support the color - * picker, false is returned. - * - * @returns {Boolean} - * true if the underlying color picker can be used by calling - * selectColor(), false otherwise. - */ - service.isAvailable = function isAvailable() { - return pickrInitComplete; - }; - - /** - * Prompts the user to choose a color, returning the color chosen via a - * Promise. - * - * @param {Element} element - * The element that the user interacted with to indicate their - * desire to choose a color. - * - * @param {String} current - * The color that should be selected by default, in standard - * 6-digit hexadecimal RGB format, including "#" prefix. - * - * @param {String[]} [palette] - * An array of color choices which should be exposed to the user - * within the color chooser for convenience. Each color must be in - * standard 6-digit hexadecimal RGB format, including "#" prefix. - * - * @returns {Promise.} - * A Promise which is resolved with the color chosen by the user, - * in standard 6-digit hexadecimal RGB format with "#" prefix, and - * rejected if the selection operation was cancelled or the color - * picker cannot be used. - */ - service.selectColor = function selectColor(element, current, palette) { - - // Show picker once the relevant translation strings have been - // retrieved and Pickr is ready for use - return $q.all({ - 'saveString' : $translate('APP.ACTION_SAVE'), - 'cancelString' : $translate('APP.ACTION_CANCEL'), - 'pickr' : pickrPromise - }).then(function dependenciesReady(deps) { - - // Cancel any active request - completeActiveRequest(); - - // Reset state of color picker to provided parameters - pickr.setColor(current); - element.appendChild(pickerContainer); - - // Assign translated strings to button text - var pickrRoot = pickr.getRoot(); - pickrRoot.interaction.save.value = deps.saveString; - pickrRoot.interaction.cancel.value = deps.cancelString; - - // Replace all color swatches with the palette of colors given - while (pickr.removeSwatch(0)) {} - angular.forEach(palette, pickr.addSwatch.bind(pickr)); - - // Show color picker and wait for user to complete selection - activeRequest = $q.defer(); - pickr.show(); - return activeRequest.promise; - - }); - - }; - - return service; - - }]; - -}); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/form/services/formService.js b/guacamole/src/main/frontend/src/app/form/services/formService.js deleted file mode 100644 index d2ac6be735..0000000000 --- a/guacamole/src/main/frontend/src/app/form/services/formService.js +++ /dev/null @@ -1,428 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for maintaining form-related metadata and linking that data to - * corresponding controllers and templates. - */ -angular.module('form').provider('formService', function formServiceProvider() { - - /** - * Reference to the provider itself. - * - * @type formServiceProvider - */ - var provider = this; - - /** - * Map of all registered field type definitions by name. - * - * @type Object. - */ - this.fieldTypes = { - - /** - * Text field type. - * - * @see {@link Field.Type.TEXT} - * @type FieldType - */ - 'TEXT' : { - module : 'form', - controller : 'textFieldController', - templateUrl : 'app/form/templates/textField.html' - }, - - /** - * Email address field type. - * - * @see {@link Field.Type.EMAIL} - * @type FieldType - */ - 'EMAIL' : { - templateUrl : 'app/form/templates/emailField.html' - }, - - /** - * Numeric field type. - * - * @see {@link Field.Type.NUMERIC} - * @type FieldType - */ - 'NUMERIC' : { - module : 'form', - controller : 'numberFieldController', - templateUrl : 'app/form/templates/numberField.html' - }, - - /** - * Boolean field type. - * - * @see {@link Field.Type.BOOLEAN} - * @type FieldType - */ - 'BOOLEAN' : { - module : 'form', - controller : 'checkboxFieldController', - templateUrl : 'app/form/templates/checkboxField.html' - }, - - /** - * Username field type. Identical in principle to a text field, but may - * have different semantics. - * - * @see {@link Field.Type.USERNAME} - * @type FieldType - */ - 'USERNAME' : { - templateUrl : 'app/form/templates/usernameField.html' - }, - - /** - * Password field type. Similar to a text field, but the contents of - * the field are masked. - * - * @see {@link Field.Type.PASSWORD} - * @type FieldType - */ - 'PASSWORD' : { - module : 'form', - controller : 'passwordFieldController', - templateUrl : 'app/form/templates/passwordField.html' - }, - - /** - * Enumerated field type. The user is presented a finite list of values - * to choose from. - * - * @see {@link Field.Type.ENUM} - * @type FieldType - */ - 'ENUM' : { - module : 'form', - controller : 'selectFieldController', - templateUrl : 'app/form/templates/selectField.html' - }, - - /** - * Multiline field type. The user may enter multiple lines of text. - * - * @see {@link Field.Type.MULTILINE} - * @type FieldType - */ - 'MULTILINE' : { - templateUrl : 'app/form/templates/textAreaField.html' - }, - - /** - * Field type which allows selection of languages. The languages - * displayed are the set of languages supported by the Guacamole web - * application. Legal values are valid language IDs, as dictated by - * the filenames of Guacamole's available translations. - * - * @see {@link Field.Type.LANGUAGE} - * @type FieldType - */ - 'LANGUAGE' : { - module : 'form', - controller : 'languageFieldController', - templateUrl : 'app/form/templates/languageField.html' - }, - - /** - * Field type which allows selection of time zones. - * - * @see {@link Field.Type.TIMEZONE} - * @type FieldType - */ - 'TIMEZONE' : { - module : 'form', - controller : 'timeZoneFieldController', - templateUrl : 'app/form/templates/timeZoneField.html' - }, - - /** - * Field type which allows selection of individual dates. - * - * @see {@link Field.Type.DATE} - * @type FieldType - */ - 'DATE' : { - module : 'form', - controller : 'dateFieldController', - templateUrl : 'app/form/templates/dateField.html' - }, - - /** - * Field type which allows selection of times of day. - * - * @see {@link Field.Type.TIME} - * @type FieldType - */ - 'TIME' : { - module : 'form', - controller : 'timeFieldController', - templateUrl : 'app/form/templates/timeField.html' - }, - - /** - * Field type which allows selection of color schemes accepted by the - * Guacamole server terminal emulator and protocols which leverage it. - * - * @see {@link Field.Type.TERMINAL_COLOR_SCHEME} - * @type FieldType - */ - 'TERMINAL_COLOR_SCHEME' : { - module : 'form', - controller : 'terminalColorSchemeFieldController', - templateUrl : 'app/form/templates/terminalColorSchemeField.html' - }, - - /** - * Field type that supports redirecting the client browser to another - * URL. - * - * @see {@link Field.Type.REDIRECT} - * @type FieldType - */ - 'REDIRECT' : { - module : 'form', - controller : 'redirectFieldController', - templateUrl : 'app/form/templates/redirectField.html' - } - - }; - - /** - * Registers a new field type under the given name. - * - * @param {String} fieldTypeName - * The name which uniquely identifies the field type being registered. - * - * @param {FieldType} fieldType - * The field type definition to associate with the given name. - */ - this.registerFieldType = function registerFieldType(fieldTypeName, fieldType) { - - // Store field type - provider.fieldTypes[fieldTypeName] = fieldType; - - }; - - // Factory method required by provider - this.$get = ['$injector', function formServiceFactory($injector) { - - // Required services - var $compile = $injector.get('$compile'); - var $q = $injector.get('$q'); - var $templateRequest = $injector.get('$templateRequest'); - - /** - * Map of module name to the injector instance created for that module. - * - * @type {Object.} - */ - var injectors = {}; - - var service = {}; - - service.fieldTypes = provider.fieldTypes; - - /** - * Given the name of a module, returns an injector instance which - * injects dependencies within that module. A new injector may be - * created and initialized if no such injector has yet been requested. - * If the injector available to formService already includes the - * requested module, that injector will simply be returned. - * - * @param {String} module - * The name of the module to produce an injector for. - * - * @returns {injector} - * An injector instance which injects dependencies for the given - * module. - */ - var getInjector = function getInjector(module) { - - // Use the formService's injector if possible - if ($injector.modules[module]) - return $injector; - - // If the formService's injector does not include the requested - // module, create the necessary injector, reusing that injector for - // future calls - injectors[module] = injectors[module] || angular.injector(['ng', module]); - return injectors[module]; - - }; - - /** - * Given form content and an arbitrary prefix, returns a corresponding - * CSS class object as would be provided to the ngClass directive that - * assigns a content-specific CSS class based on the prefix and - * form/field name. Generated class names follow the lowercase with - * dashes naming convention. For example, if the prefix is "field-" and - * the provided content is a field with the name "Swap red/blue", the - * object { 'field-swap-red-blue' : true } would be returned. - * - * @param {!string} prefix - * The arbitrary prefix to prepend to the name of the generated CSS - * class. - * - * @param {!(Form|Field)} [content] - * The form or field whose name should be used to produce the CSS - * class name. - * - * @param {Object.} [object={}] - * The optional base ngClass object that should be used to provide - * additional name/value pairs within the returned object. - * - * @return {!Object.} - * The ngClass object based on the provided object and defining a - * CSS class name for the given content. - */ - service.getClasses = function getClasses(prefix, content, object) { - - // Default to no additional properties - object = object || {}; - - // Perform no transformation if there is no content or - // corresponding name - if (!content || !content.name) - return object; - - // Transform content name and prefix into lowercase-with-dashes - // CSS class name - var className = prefix + content.name.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase(); - - // Add CSS class name to provided base object (without touching - // base object) - var classes = angular.extend({}, object); - classes[className] = true; - return classes; - - }; - - /** - * Compiles and links the field associated with the given name to the given - * scope, producing a distinct and independent DOM Element which functions - * as an instance of that field. The scope object provided must include at - * least the following properties: - * - * namespace: - * A String which defines the unique namespace associated the - * translation strings used by the form using a field of this type. - * - * fieldId: - * A String value which is reasonably likely to be unique and may - * be used to associate the main element of the field with its - * label. - * - * field: - * The Field object that is being rendered, representing a field of - * this type. - * - * model: - * The current String value of the field, if any. - * - * disabled: - * A boolean value which is true if the field should be disabled. - * If false or undefined, the field should be enabled. - * - * @param {Element} fieldContainer - * The DOM Element whose contents should be replaced with the - * compiled field template. - * - * @param {String} fieldTypeName - * The name of the field type defining the nature of the element to be - * created. - * - * @param {Object} scope - * The scope to which the new element will be linked. - * - * @return {Promise.} - * A Promise which resolves to the compiled Element. If an error occurs - * while retrieving the field type, this Promise will be rejected. - */ - service.insertFieldElement = function insertFieldElement(fieldContainer, - fieldTypeName, scope) { - - // Ensure field type is defined - var fieldType = provider.fieldTypes[fieldTypeName]; - if (!fieldType) - return $q.reject(); - - var templateRequest; - - // Use raw HTML template if provided - if (fieldType.template) { - var deferredTemplate = $q.defer(); - deferredTemplate.resolve(fieldType.template); - templateRequest = deferredTemplate.promise; - } - - // If no raw HTML template is provided, retrieve template from URL - else if (fieldType.templateUrl) - templateRequest = $templateRequest(fieldType.templateUrl); - - // Otherwise, use empty template - else { - var emptyTemplate= $q.defer(); - emptyTemplate.resolve(''); - templateRequest = emptyTemplate.promise; - } - - // Defer compilation of template pending successful retrieval - var compiledTemplate = $q.defer(); - - // Resolve with compiled HTML upon success - templateRequest.then(function templateRetrieved(html) { - - // Insert template into DOM - fieldContainer.innerHTML = html; - - // Populate scope using defined controller - if (fieldType.module && fieldType.controller) { - var $controller = getInjector(fieldType.module).get('$controller'); - $controller(fieldType.controller, { - '$scope' : scope, - '$element' : angular.element(fieldContainer.childNodes) - }); - } - - // Compile DOM with populated scope - compiledTemplate.resolve($compile(fieldContainer.childNodes)(scope)); - - }) - - // Reject on failure - ['catch'](function templateError() { - compiledTemplate.reject(); - }); - - // Return promise which resolves to the compiled template - return compiledTemplate.promise; - - }; - - return service; - - }]; - -}); diff --git a/guacamole/src/main/frontend/src/app/form/templates/checkboxField.html b/guacamole/src/main/frontend/src/app/form/templates/checkboxField.html deleted file mode 100644 index 39d1f6b7d0..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/checkboxField.html +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/dateField.html b/guacamole/src/main/frontend/src/app/form/templates/dateField.html deleted file mode 100644 index 93a231f354..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/dateField.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/emailField.html b/guacamole/src/main/frontend/src/app/form/templates/emailField.html deleted file mode 100644 index c5170b796a..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/emailField.html +++ /dev/null @@ -1,12 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/form/templates/form.html b/guacamole/src/main/frontend/src/app/form/templates/form.html deleted file mode 100644 index 642c57783d..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/form.html +++ /dev/null @@ -1,21 +0,0 @@ -
    -
    - - -

    {{getSectionHeader(form) | translate}}

    - - -
    - -
    - -
    -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/formField.html b/guacamole/src/main/frontend/src/app/form/templates/formField.html deleted file mode 100644 index 8534515a22..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/formField.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - - -
    - -
    - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/guacInputColor.html b/guacamole/src/main/frontend/src/app/form/templates/guacInputColor.html deleted file mode 100644 index eae1f66996..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/guacInputColor.html +++ /dev/null @@ -1,11 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/form/templates/languageField.html b/guacamole/src/main/frontend/src/app/form/templates/languageField.html deleted file mode 100644 index a19d21cd0c..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/languageField.html +++ /dev/null @@ -1,7 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/numberField.html b/guacamole/src/main/frontend/src/app/form/templates/numberField.html deleted file mode 100644 index 1bf58a1758..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/numberField.html +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/passwordField.html b/guacamole/src/main/frontend/src/app/form/templates/passwordField.html deleted file mode 100644 index 17cfc0a6cc..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/passwordField.html +++ /dev/null @@ -1,12 +0,0 @@ -
    - -
    -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/redirectField.html b/guacamole/src/main/frontend/src/app/form/templates/redirectField.html deleted file mode 100644 index 2dffa634af..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/redirectField.html +++ /dev/null @@ -1,8 +0,0 @@ -
    -
    -

    -

    -
    -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/form/templates/selectField.html b/guacamole/src/main/frontend/src/app/form/templates/selectField.html deleted file mode 100644 index 635527557d..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/selectField.html +++ /dev/null @@ -1,8 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/terminalColorSchemeField.html b/guacamole/src/main/frontend/src/app/form/templates/terminalColorSchemeField.html deleted file mode 100644 index 027f4e751c..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/terminalColorSchemeField.html +++ /dev/null @@ -1,65 +0,0 @@ -
    - - - - - -
    - - -
    - - {{ 'COLOR_SCHEME.FIELD_HEADER_FOREGROUND' | translate }} - -
    - - -
    - - {{ 'COLOR_SCHEME.FIELD_HEADER_BACKGROUND' | translate }} - -
    - - -
    - - {{ index }} - -
    - - -
    - - {{ index }} - -
    - -
    - - -

    - {{'COLOR_SCHEME.SECTION_HEADER_DETAILS' | translate}} - {{'COLOR_SCHEME.ACTION_SHOW_DETAILS' | translate}} - {{'COLOR_SCHEME.ACTION_HIDE_DETAILS' | translate}} -

    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/textAreaField.html b/guacamole/src/main/frontend/src/app/form/templates/textAreaField.html deleted file mode 100644 index 4e63eb53ef..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/textAreaField.html +++ /dev/null @@ -1,9 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/textField.html b/guacamole/src/main/frontend/src/app/form/templates/textField.html deleted file mode 100644 index fac7b38189..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/textField.html +++ /dev/null @@ -1,15 +0,0 @@ -
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/timeField.html b/guacamole/src/main/frontend/src/app/form/templates/timeField.html deleted file mode 100644 index 0b33f5fb17..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/timeField.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/timeZoneField.html b/guacamole/src/main/frontend/src/app/form/templates/timeZoneField.html deleted file mode 100644 index 690bf68a48..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/timeZoneField.html +++ /dev/null @@ -1,18 +0,0 @@ -
    - - - - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/form/templates/usernameField.html b/guacamole/src/main/frontend/src/app/form/templates/usernameField.html deleted file mode 100644 index 6a5c664d4a..0000000000 --- a/guacamole/src/main/frontend/src/app/form/templates/usernameField.html +++ /dev/null @@ -1,10 +0,0 @@ -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/form/types/ColorScheme.js b/guacamole/src/main/frontend/src/app/form/types/ColorScheme.js deleted file mode 100644 index f51a667f9e..0000000000 --- a/guacamole/src/main/frontend/src/app/form/types/ColorScheme.js +++ /dev/null @@ -1,949 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ColorScheme class. - */ -angular.module('form').factory('ColorScheme', [function defineColorScheme() { - - /** - * Intermediate representation of a custom color scheme which can be - * converted to the color scheme format used by Guacamole's terminal - * emulator. All colors must be represented in the six-digit hexadecimal - * RGB notation used by HTML ("#000000" for black, etc.). - * - * @constructor - * @param {ColorScheme|Object} [template={}] - * The object whose properties should be copied within the new - * ColorScheme. - */ - var ColorScheme = function ColorScheme(template) { - - // Use empty object by default - template = template || {}; - - /** - * The terminal background color. This will be the default foreground - * color of the Guacamole terminal emulator ("#000000") by default. - * - * @type {String} - */ - this.background = template.background || '#000000'; - - /** - * The terminal foreground color. This will be the default foreground - * color of the Guacamole terminal emulator ("#999999") by default. - * - * @type {String} - */ - this.foreground = template.foreground || '#999999'; - - /** - * The terminal color palette. Default values are provided for the - * normal 16 terminal colors using the default values of the Guacamole - * terminal emulator, however the terminal emulator and this - * representation support up to 256 colors. - * - * @type {String[]} - */ - this.colors = template.colors || [ - - // Normal colors - '#000000', // Black - '#993E3E', // Red - '#3E993E', // Green - '#99993E', // Brown - '#3E3E99', // Blue - '#993E99', // Magenta - '#3E9999', // Cyan - '#999999', // White - - // Intense colors - '#3E3E3E', // Black - '#FF6767', // Red - '#67FF67', // Green - '#FFFF67', // Brown - '#6767FF', // Blue - '#FF67FF', // Magenta - '#67FFFF', // Cyan - '#FFFFFF' // White - - ]; - - /** - * The string which was parsed to produce this ColorScheme instance, if - * ColorScheme.fromString() was used to produce this ColorScheme. - * - * @private - * @type {String} - */ - this._originalString = template._originalString; - - }; - - /** - * Given a color string in the standard 6-digit hexadecimal RGB format, - * returns a X11 color spec which represents the same color. - * - * @param {String} color - * The hexadecimal color string to convert. - * - * @returns {String} - * The X11 color spec representing the same color as the given - * hexadecimal string, or null if the given string is not a valid - * 6-digit hexadecimal RGB color. - */ - var fromHexColor = function fromHexColor(color) { - - var groups = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(color); - if (!groups) - return null; - - return 'rgb:' + groups[1] + '/' + groups[2] + '/' + groups[3]; - - }; - - /** - * Parses the same subset of the X11 color spec supported by the Guacamole - * terminal emulator (the "rgb:*" format), returning the equivalent 6-digit - * hexadecimal color string supported by the ColorScheme representation. - * The X11 color spec defined by Xlib's XParseColor(). The human-readable - * color names supported by the Guacamole terminal emulator (the same color - * names as supported by xterm) may also be used. - * - * @param {String} color - * The X11 color spec to parse, or the name of a known named color. - * - * @returns {String} - * The 6-digit hexadecimal color string which represents the same color - * as the given X11 color spec/name, or null if the given spec/name is - * invalid. - */ - var toHexColor = function toHexColor(color) { - - /** - * Shifts or truncates the given hexadecimal string such that it - * contains exactly two hexadecimal digits, as required by any - * individual color component of the 6-digit hexadecimal RGB format. - * - * @param {String} component - * The hexadecimal string to shift or truncate to two digits. - * - * @returns {String} - * A new 2-digit hexadecimal string containing the same digits as - * the provided string, shifted or truncated as necessary to fit - * within the 2-digit length limit. - */ - var toHexComponent = function toHexComponent(component) { - return (component + '0').substring(0, 2).toUpperCase(); - }; - - // Attempt to parse any non-RGB color as a named color - var groups = /^rgb:([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})$/.exec(color); - if (!groups) - return ColorScheme.NAMED_COLORS[color.toLowerCase()] || null; - - // Convert to standard 6-digit hexadecimal RGB format - return '#' + toHexComponent(groups[1]) + toHexComponent(groups[2]) + toHexComponent(groups[3]); - - }; - - /** - * Converts the given string representation of a color scheme which is - * supported by the Guacamole terminal emulator to a corresponding, - * intermediate ColorScheme object. - * - * @param {String} str - * An arbitrary color scheme, in the string format supported by the - * Guacamole terminal emulator. - * - * @returns {ColorScheme} - * A new ColorScheme instance which represents the same color scheme as - * the given string. - */ - ColorScheme.fromString = function fromString(str) { - - var scheme = new ColorScheme({ _originalString : str }); - - // For each semicolon-separated statement in the provided color scheme - var statements = str.split(/;/); - for (var i = 0; i < statements.length; i++) { - - // Skip any statements which cannot be parsed - var statement = statements[i]; - var groups = /^\s*(background|foreground|color([0-9]+))\s*:\s*(\S*)\s*$/.exec(statement); - if (!groups) - continue; - - // If the statement is valid and contains a valid color, map that - // color to the appropriate property of the ColorScheme object - var color = toHexColor(groups[3]); - if (color) { - if (groups[1] === 'background') - scheme.background = color; - else if (groups[1] === 'foreground') - scheme.foreground = color; - else - scheme.colors[parseInt(groups[2])] = color; - } - - } - - return scheme; - - }; - - /** - * Returns whether the two given color schemes define the exact same - * colors. - * - * @param {ColorScheme} a - * The first ColorScheme to compare. - * - * @param {ColorScheme} b - * The second ColorScheme to compare. - * - * @returns {Boolean} - * true if both color schemes contain the same colors, false otherwise. - */ - ColorScheme.equals = function equals(a, b) { - return a.foreground === b.foreground - && a.background === b.background - && _.isEqual(a.colors, b.colors); - }; - - /** - * Converts the given ColorScheme to a string representation which is - * supported by the Guacamole terminal emulator. - * - * @param {ColorScheme} scheme - * The ColorScheme to convert to a string. - * - * @returns {String} - * The given color scheme, converted to the string format supported by - * the Guacamole terminal emulator. - */ - ColorScheme.toString = function toString(scheme) { - - // Use originally-provided string if it equates to the exact same color scheme - if (!_.isUndefined(scheme._originalString) && ColorScheme.equals(scheme, ColorScheme.fromString(scheme._originalString))) - return scheme._originalString; - - // Add background and foreground - var str = 'background: ' + fromHexColor(scheme.background) + ';\n' - + 'foreground: ' + fromHexColor(scheme.foreground) + ';'; - - // Add color definitions for each palette entry - for (var index in scheme.colors) - str += '\ncolor' + index + ': ' + fromHexColor(scheme.colors[index]) + ';'; - - return str; - - }; - - /** - * The set of all named colors supported by the Guacamole terminal - * emulator and their corresponding 6-digit hexadecimal RGB - * representations. This set should contain all colors supported by xterm. - * - * @constant - * @type {Object.} - */ - ColorScheme.NAMED_COLORS = { - 'aliceblue' : '#F0F8FF', - 'antiquewhite' : '#FAEBD7', - 'antiquewhite1' : '#FFEFDB', - 'antiquewhite2' : '#EEDFCC', - 'antiquewhite3' : '#CDC0B0', - 'antiquewhite4' : '#8B8378', - 'aqua' : '#00FFFF', - 'aquamarine' : '#7FFFD4', - 'aquamarine1' : '#7FFFD4', - 'aquamarine2' : '#76EEC6', - 'aquamarine3' : '#66CDAA', - 'aquamarine4' : '#458B74', - 'azure' : '#F0FFFF', - 'azure1' : '#F0FFFF', - 'azure2' : '#E0EEEE', - 'azure3' : '#C1CDCD', - 'azure4' : '#838B8B', - 'beige' : '#F5F5DC', - 'bisque' : '#FFE4C4', - 'bisque1' : '#FFE4C4', - 'bisque2' : '#EED5B7', - 'bisque3' : '#CDB79E', - 'bisque4' : '#8B7D6B', - 'black' : '#000000', - 'blanchedalmond' : '#FFEBCD', - 'blue' : '#0000FF', - 'blue1' : '#0000FF', - 'blue2' : '#0000EE', - 'blue3' : '#0000CD', - 'blue4' : '#00008B', - 'blueviolet' : '#8A2BE2', - 'brown' : '#A52A2A', - 'brown1' : '#FF4040', - 'brown2' : '#EE3B3B', - 'brown3' : '#CD3333', - 'brown4' : '#8B2323', - 'burlywood' : '#DEB887', - 'burlywood1' : '#FFD39B', - 'burlywood2' : '#EEC591', - 'burlywood3' : '#CDAA7D', - 'burlywood4' : '#8B7355', - 'cadetblue' : '#5F9EA0', - 'cadetblue1' : '#98F5FF', - 'cadetblue2' : '#8EE5EE', - 'cadetblue3' : '#7AC5CD', - 'cadetblue4' : '#53868B', - 'chartreuse' : '#7FFF00', - 'chartreuse1' : '#7FFF00', - 'chartreuse2' : '#76EE00', - 'chartreuse3' : '#66CD00', - 'chartreuse4' : '#458B00', - 'chocolate' : '#D2691E', - 'chocolate1' : '#FF7F24', - 'chocolate2' : '#EE7621', - 'chocolate3' : '#CD661D', - 'chocolate4' : '#8B4513', - 'coral' : '#FF7F50', - 'coral1' : '#FF7256', - 'coral2' : '#EE6A50', - 'coral3' : '#CD5B45', - 'coral4' : '#8B3E2F', - 'cornflowerblue' : '#6495ED', - 'cornsilk' : '#FFF8DC', - 'cornsilk1' : '#FFF8DC', - 'cornsilk2' : '#EEE8CD', - 'cornsilk3' : '#CDC8B1', - 'cornsilk4' : '#8B8878', - 'crimson' : '#DC143C', - 'cyan' : '#00FFFF', - 'cyan1' : '#00FFFF', - 'cyan2' : '#00EEEE', - 'cyan3' : '#00CDCD', - 'cyan4' : '#008B8B', - 'darkblue' : '#00008B', - 'darkcyan' : '#008B8B', - 'darkgoldenrod' : '#B8860B', - 'darkgoldenrod1' : '#FFB90F', - 'darkgoldenrod2' : '#EEAD0E', - 'darkgoldenrod3' : '#CD950C', - 'darkgoldenrod4' : '#8B6508', - 'darkgray' : '#A9A9A9', - 'darkgreen' : '#006400', - 'darkgrey' : '#A9A9A9', - 'darkkhaki' : '#BDB76B', - 'darkmagenta' : '#8B008B', - 'darkolivegreen' : '#556B2F', - 'darkolivegreen1' : '#CAFF70', - 'darkolivegreen2' : '#BCEE68', - 'darkolivegreen3' : '#A2CD5A', - 'darkolivegreen4' : '#6E8B3D', - 'darkorange' : '#FF8C00', - 'darkorange1' : '#FF7F00', - 'darkorange2' : '#EE7600', - 'darkorange3' : '#CD6600', - 'darkorange4' : '#8B4500', - 'darkorchid' : '#9932CC', - 'darkorchid1' : '#BF3EFF', - 'darkorchid2' : '#B23AEE', - 'darkorchid3' : '#9A32CD', - 'darkorchid4' : '#68228B', - 'darkred' : '#8B0000', - 'darksalmon' : '#E9967A', - 'darkseagreen' : '#8FBC8F', - 'darkseagreen1' : '#C1FFC1', - 'darkseagreen2' : '#B4EEB4', - 'darkseagreen3' : '#9BCD9B', - 'darkseagreen4' : '#698B69', - 'darkslateblue' : '#483D8B', - 'darkslategray' : '#2F4F4F', - 'darkslategray1' : '#97FFFF', - 'darkslategray2' : '#8DEEEE', - 'darkslategray3' : '#79CDCD', - 'darkslategray4' : '#528B8B', - 'darkslategrey' : '#2F4F4F', - 'darkturquoise' : '#00CED1', - 'darkviolet' : '#9400D3', - 'deeppink' : '#FF1493', - 'deeppink1' : '#FF1493', - 'deeppink2' : '#EE1289', - 'deeppink3' : '#CD1076', - 'deeppink4' : '#8B0A50', - 'deepskyblue' : '#00BFFF', - 'deepskyblue1' : '#00BFFF', - 'deepskyblue2' : '#00B2EE', - 'deepskyblue3' : '#009ACD', - 'deepskyblue4' : '#00688B', - 'dimgray' : '#696969', - 'dimgrey' : '#696969', - 'dodgerblue' : '#1E90FF', - 'dodgerblue1' : '#1E90FF', - 'dodgerblue2' : '#1C86EE', - 'dodgerblue3' : '#1874CD', - 'dodgerblue4' : '#104E8B', - 'firebrick' : '#B22222', - 'firebrick1' : '#FF3030', - 'firebrick2' : '#EE2C2C', - 'firebrick3' : '#CD2626', - 'firebrick4' : '#8B1A1A', - 'floralwhite' : '#FFFAF0', - 'forestgreen' : '#228B22', - 'fuchsia' : '#FF00FF', - 'gainsboro' : '#DCDCDC', - 'ghostwhite' : '#F8F8FF', - 'gold' : '#FFD700', - 'gold1' : '#FFD700', - 'gold2' : '#EEC900', - 'gold3' : '#CDAD00', - 'gold4' : '#8B7500', - 'goldenrod' : '#DAA520', - 'goldenrod1' : '#FFC125', - 'goldenrod2' : '#EEB422', - 'goldenrod3' : '#CD9B1D', - 'goldenrod4' : '#8B6914', - 'gray' : '#BEBEBE', - 'gray0' : '#000000', - 'gray1' : '#030303', - 'gray10' : '#1A1A1A', - 'gray100' : '#FFFFFF', - 'gray11' : '#1C1C1C', - 'gray12' : '#1F1F1F', - 'gray13' : '#212121', - 'gray14' : '#242424', - 'gray15' : '#262626', - 'gray16' : '#292929', - 'gray17' : '#2B2B2B', - 'gray18' : '#2E2E2E', - 'gray19' : '#303030', - 'gray2' : '#050505', - 'gray20' : '#333333', - 'gray21' : '#363636', - 'gray22' : '#383838', - 'gray23' : '#3B3B3B', - 'gray24' : '#3D3D3D', - 'gray25' : '#404040', - 'gray26' : '#424242', - 'gray27' : '#454545', - 'gray28' : '#474747', - 'gray29' : '#4A4A4A', - 'gray3' : '#080808', - 'gray30' : '#4D4D4D', - 'gray31' : '#4F4F4F', - 'gray32' : '#525252', - 'gray33' : '#545454', - 'gray34' : '#575757', - 'gray35' : '#595959', - 'gray36' : '#5C5C5C', - 'gray37' : '#5E5E5E', - 'gray38' : '#616161', - 'gray39' : '#636363', - 'gray4' : '#0A0A0A', - 'gray40' : '#666666', - 'gray41' : '#696969', - 'gray42' : '#6B6B6B', - 'gray43' : '#6E6E6E', - 'gray44' : '#707070', - 'gray45' : '#737373', - 'gray46' : '#757575', - 'gray47' : '#787878', - 'gray48' : '#7A7A7A', - 'gray49' : '#7D7D7D', - 'gray5' : '#0D0D0D', - 'gray50' : '#7F7F7F', - 'gray51' : '#828282', - 'gray52' : '#858585', - 'gray53' : '#878787', - 'gray54' : '#8A8A8A', - 'gray55' : '#8C8C8C', - 'gray56' : '#8F8F8F', - 'gray57' : '#919191', - 'gray58' : '#949494', - 'gray59' : '#969696', - 'gray6' : '#0F0F0F', - 'gray60' : '#999999', - 'gray61' : '#9C9C9C', - 'gray62' : '#9E9E9E', - 'gray63' : '#A1A1A1', - 'gray64' : '#A3A3A3', - 'gray65' : '#A6A6A6', - 'gray66' : '#A8A8A8', - 'gray67' : '#ABABAB', - 'gray68' : '#ADADAD', - 'gray69' : '#B0B0B0', - 'gray7' : '#121212', - 'gray70' : '#B3B3B3', - 'gray71' : '#B5B5B5', - 'gray72' : '#B8B8B8', - 'gray73' : '#BABABA', - 'gray74' : '#BDBDBD', - 'gray75' : '#BFBFBF', - 'gray76' : '#C2C2C2', - 'gray77' : '#C4C4C4', - 'gray78' : '#C7C7C7', - 'gray79' : '#C9C9C9', - 'gray8' : '#141414', - 'gray80' : '#CCCCCC', - 'gray81' : '#CFCFCF', - 'gray82' : '#D1D1D1', - 'gray83' : '#D4D4D4', - 'gray84' : '#D6D6D6', - 'gray85' : '#D9D9D9', - 'gray86' : '#DBDBDB', - 'gray87' : '#DEDEDE', - 'gray88' : '#E0E0E0', - 'gray89' : '#E3E3E3', - 'gray9' : '#171717', - 'gray90' : '#E5E5E5', - 'gray91' : '#E8E8E8', - 'gray92' : '#EBEBEB', - 'gray93' : '#EDEDED', - 'gray94' : '#F0F0F0', - 'gray95' : '#F2F2F2', - 'gray96' : '#F5F5F5', - 'gray97' : '#F7F7F7', - 'gray98' : '#FAFAFA', - 'gray99' : '#FCFCFC', - 'green' : '#00FF00', - 'green1' : '#00FF00', - 'green2' : '#00EE00', - 'green3' : '#00CD00', - 'green4' : '#008B00', - 'greenyellow' : '#ADFF2F', - 'grey' : '#BEBEBE', - 'grey0' : '#000000', - 'grey1' : '#030303', - 'grey10' : '#1A1A1A', - 'grey100' : '#FFFFFF', - 'grey11' : '#1C1C1C', - 'grey12' : '#1F1F1F', - 'grey13' : '#212121', - 'grey14' : '#242424', - 'grey15' : '#262626', - 'grey16' : '#292929', - 'grey17' : '#2B2B2B', - 'grey18' : '#2E2E2E', - 'grey19' : '#303030', - 'grey2' : '#050505', - 'grey20' : '#333333', - 'grey21' : '#363636', - 'grey22' : '#383838', - 'grey23' : '#3B3B3B', - 'grey24' : '#3D3D3D', - 'grey25' : '#404040', - 'grey26' : '#424242', - 'grey27' : '#454545', - 'grey28' : '#474747', - 'grey29' : '#4A4A4A', - 'grey3' : '#080808', - 'grey30' : '#4D4D4D', - 'grey31' : '#4F4F4F', - 'grey32' : '#525252', - 'grey33' : '#545454', - 'grey34' : '#575757', - 'grey35' : '#595959', - 'grey36' : '#5C5C5C', - 'grey37' : '#5E5E5E', - 'grey38' : '#616161', - 'grey39' : '#636363', - 'grey4' : '#0A0A0A', - 'grey40' : '#666666', - 'grey41' : '#696969', - 'grey42' : '#6B6B6B', - 'grey43' : '#6E6E6E', - 'grey44' : '#707070', - 'grey45' : '#737373', - 'grey46' : '#757575', - 'grey47' : '#787878', - 'grey48' : '#7A7A7A', - 'grey49' : '#7D7D7D', - 'grey5' : '#0D0D0D', - 'grey50' : '#7F7F7F', - 'grey51' : '#828282', - 'grey52' : '#858585', - 'grey53' : '#878787', - 'grey54' : '#8A8A8A', - 'grey55' : '#8C8C8C', - 'grey56' : '#8F8F8F', - 'grey57' : '#919191', - 'grey58' : '#949494', - 'grey59' : '#969696', - 'grey6' : '#0F0F0F', - 'grey60' : '#999999', - 'grey61' : '#9C9C9C', - 'grey62' : '#9E9E9E', - 'grey63' : '#A1A1A1', - 'grey64' : '#A3A3A3', - 'grey65' : '#A6A6A6', - 'grey66' : '#A8A8A8', - 'grey67' : '#ABABAB', - 'grey68' : '#ADADAD', - 'grey69' : '#B0B0B0', - 'grey7' : '#121212', - 'grey70' : '#B3B3B3', - 'grey71' : '#B5B5B5', - 'grey72' : '#B8B8B8', - 'grey73' : '#BABABA', - 'grey74' : '#BDBDBD', - 'grey75' : '#BFBFBF', - 'grey76' : '#C2C2C2', - 'grey77' : '#C4C4C4', - 'grey78' : '#C7C7C7', - 'grey79' : '#C9C9C9', - 'grey8' : '#141414', - 'grey80' : '#CCCCCC', - 'grey81' : '#CFCFCF', - 'grey82' : '#D1D1D1', - 'grey83' : '#D4D4D4', - 'grey84' : '#D6D6D6', - 'grey85' : '#D9D9D9', - 'grey86' : '#DBDBDB', - 'grey87' : '#DEDEDE', - 'grey88' : '#E0E0E0', - 'grey89' : '#E3E3E3', - 'grey9' : '#171717', - 'grey90' : '#E5E5E5', - 'grey91' : '#E8E8E8', - 'grey92' : '#EBEBEB', - 'grey93' : '#EDEDED', - 'grey94' : '#F0F0F0', - 'grey95' : '#F2F2F2', - 'grey96' : '#F5F5F5', - 'grey97' : '#F7F7F7', - 'grey98' : '#FAFAFA', - 'grey99' : '#FCFCFC', - 'honeydew' : '#F0FFF0', - 'honeydew1' : '#F0FFF0', - 'honeydew2' : '#E0EEE0', - 'honeydew3' : '#C1CDC1', - 'honeydew4' : '#838B83', - 'hotpink' : '#FF69B4', - 'hotpink1' : '#FF6EB4', - 'hotpink2' : '#EE6AA7', - 'hotpink3' : '#CD6090', - 'hotpink4' : '#8B3A62', - 'indianred' : '#CD5C5C', - 'indianred1' : '#FF6A6A', - 'indianred2' : '#EE6363', - 'indianred3' : '#CD5555', - 'indianred4' : '#8B3A3A', - 'indigo' : '#4B0082', - 'ivory' : '#FFFFF0', - 'ivory1' : '#FFFFF0', - 'ivory2' : '#EEEEE0', - 'ivory3' : '#CDCDC1', - 'ivory4' : '#8B8B83', - 'khaki' : '#F0E68C', - 'khaki1' : '#FFF68F', - 'khaki2' : '#EEE685', - 'khaki3' : '#CDC673', - 'khaki4' : '#8B864E', - 'lavender' : '#E6E6FA', - 'lavenderblush' : '#FFF0F5', - 'lavenderblush1' : '#FFF0F5', - 'lavenderblush2' : '#EEE0E5', - 'lavenderblush3' : '#CDC1C5', - 'lavenderblush4' : '#8B8386', - 'lawngreen' : '#7CFC00', - 'lemonchiffon' : '#FFFACD', - 'lemonchiffon1' : '#FFFACD', - 'lemonchiffon2' : '#EEE9BF', - 'lemonchiffon3' : '#CDC9A5', - 'lemonchiffon4' : '#8B8970', - 'lightblue' : '#ADD8E6', - 'lightblue1' : '#BFEFFF', - 'lightblue2' : '#B2DFEE', - 'lightblue3' : '#9AC0CD', - 'lightblue4' : '#68838B', - 'lightcoral' : '#F08080', - 'lightcyan' : '#E0FFFF', - 'lightcyan1' : '#E0FFFF', - 'lightcyan2' : '#D1EEEE', - 'lightcyan3' : '#B4CDCD', - 'lightcyan4' : '#7A8B8B', - 'lightgoldenrod' : '#EEDD82', - 'lightgoldenrod1' : '#FFEC8B', - 'lightgoldenrod2' : '#EEDC82', - 'lightgoldenrod3' : '#CDBE70', - 'lightgoldenrod4' : '#8B814C', - 'lightgoldenrodyellow' : '#FAFAD2', - 'lightgray' : '#D3D3D3', - 'lightgreen' : '#90EE90', - 'lightgrey' : '#D3D3D3', - 'lightpink' : '#FFB6C1', - 'lightpink1' : '#FFAEB9', - 'lightpink2' : '#EEA2AD', - 'lightpink3' : '#CD8C95', - 'lightpink4' : '#8B5F65', - 'lightsalmon' : '#FFA07A', - 'lightsalmon1' : '#FFA07A', - 'lightsalmon2' : '#EE9572', - 'lightsalmon3' : '#CD8162', - 'lightsalmon4' : '#8B5742', - 'lightseagreen' : '#20B2AA', - 'lightskyblue' : '#87CEFA', - 'lightskyblue1' : '#B0E2FF', - 'lightskyblue2' : '#A4D3EE', - 'lightskyblue3' : '#8DB6CD', - 'lightskyblue4' : '#607B8B', - 'lightslateblue' : '#8470FF', - 'lightslategray' : '#778899', - 'lightslategrey' : '#778899', - 'lightsteelblue' : '#B0C4DE', - 'lightsteelblue1' : '#CAE1FF', - 'lightsteelblue2' : '#BCD2EE', - 'lightsteelblue3' : '#A2B5CD', - 'lightsteelblue4' : '#6E7B8B', - 'lightyellow' : '#FFFFE0', - 'lightyellow1' : '#FFFFE0', - 'lightyellow2' : '#EEEED1', - 'lightyellow3' : '#CDCDB4', - 'lightyellow4' : '#8B8B7A', - 'lime' : '#00FF00', - 'limegreen' : '#32CD32', - 'linen' : '#FAF0E6', - 'magenta' : '#FF00FF', - 'magenta1' : '#FF00FF', - 'magenta2' : '#EE00EE', - 'magenta3' : '#CD00CD', - 'magenta4' : '#8B008B', - 'maroon' : '#B03060', - 'maroon1' : '#FF34B3', - 'maroon2' : '#EE30A7', - 'maroon3' : '#CD2990', - 'maroon4' : '#8B1C62', - 'mediumaquamarine' : '#66CDAA', - 'mediumblue' : '#0000CD', - 'mediumorchid' : '#BA55D3', - 'mediumorchid1' : '#E066FF', - 'mediumorchid2' : '#D15FEE', - 'mediumorchid3' : '#B452CD', - 'mediumorchid4' : '#7A378B', - 'mediumpurple' : '#9370DB', - 'mediumpurple1' : '#AB82FF', - 'mediumpurple2' : '#9F79EE', - 'mediumpurple3' : '#8968CD', - 'mediumpurple4' : '#5D478B', - 'mediumseagreen' : '#3CB371', - 'mediumslateblue' : '#7B68EE', - 'mediumspringgreen' : '#00FA9A', - 'mediumturquoise' : '#48D1CC', - 'mediumvioletred' : '#C71585', - 'midnightblue' : '#191970', - 'mintcream' : '#F5FFFA', - 'mistyrose' : '#FFE4E1', - 'mistyrose1' : '#FFE4E1', - 'mistyrose2' : '#EED5D2', - 'mistyrose3' : '#CDB7B5', - 'mistyrose4' : '#8B7D7B', - 'moccasin' : '#FFE4B5', - 'navajowhite' : '#FFDEAD', - 'navajowhite1' : '#FFDEAD', - 'navajowhite2' : '#EECFA1', - 'navajowhite3' : '#CDB38B', - 'navajowhite4' : '#8B795E', - 'navy' : '#000080', - 'navyblue' : '#000080', - 'oldlace' : '#FDF5E6', - 'olive' : '#808000', - 'olivedrab' : '#6B8E23', - 'olivedrab1' : '#C0FF3E', - 'olivedrab2' : '#B3EE3A', - 'olivedrab3' : '#9ACD32', - 'olivedrab4' : '#698B22', - 'orange' : '#FFA500', - 'orange1' : '#FFA500', - 'orange2' : '#EE9A00', - 'orange3' : '#CD8500', - 'orange4' : '#8B5A00', - 'orangered' : '#FF4500', - 'orangered1' : '#FF4500', - 'orangered2' : '#EE4000', - 'orangered3' : '#CD3700', - 'orangered4' : '#8B2500', - 'orchid' : '#DA70D6', - 'orchid1' : '#FF83FA', - 'orchid2' : '#EE7AE9', - 'orchid3' : '#CD69C9', - 'orchid4' : '#8B4789', - 'palegoldenrod' : '#EEE8AA', - 'palegreen' : '#98FB98', - 'palegreen1' : '#9AFF9A', - 'palegreen2' : '#90EE90', - 'palegreen3' : '#7CCD7C', - 'palegreen4' : '#548B54', - 'paleturquoise' : '#AFEEEE', - 'paleturquoise1' : '#BBFFFF', - 'paleturquoise2' : '#AEEEEE', - 'paleturquoise3' : '#96CDCD', - 'paleturquoise4' : '#668B8B', - 'palevioletred' : '#DB7093', - 'palevioletred1' : '#FF82AB', - 'palevioletred2' : '#EE799F', - 'palevioletred3' : '#CD6889', - 'palevioletred4' : '#8B475D', - 'papayawhip' : '#FFEFD5', - 'peachpuff' : '#FFDAB9', - 'peachpuff1' : '#FFDAB9', - 'peachpuff2' : '#EECBAD', - 'peachpuff3' : '#CDAF95', - 'peachpuff4' : '#8B7765', - 'peru' : '#CD853F', - 'pink' : '#FFC0CB', - 'pink1' : '#FFB5C5', - 'pink2' : '#EEA9B8', - 'pink3' : '#CD919E', - 'pink4' : '#8B636C', - 'plum' : '#DDA0DD', - 'plum1' : '#FFBBFF', - 'plum2' : '#EEAEEE', - 'plum3' : '#CD96CD', - 'plum4' : '#8B668B', - 'powderblue' : '#B0E0E6', - 'purple' : '#A020F0', - 'purple1' : '#9B30FF', - 'purple2' : '#912CEE', - 'purple3' : '#7D26CD', - 'purple4' : '#551A8B', - 'rebeccapurple' : '#663399', - 'red' : '#FF0000', - 'red1' : '#FF0000', - 'red2' : '#EE0000', - 'red3' : '#CD0000', - 'red4' : '#8B0000', - 'rosybrown' : '#BC8F8F', - 'rosybrown1' : '#FFC1C1', - 'rosybrown2' : '#EEB4B4', - 'rosybrown3' : '#CD9B9B', - 'rosybrown4' : '#8B6969', - 'royalblue' : '#4169E1', - 'royalblue1' : '#4876FF', - 'royalblue2' : '#436EEE', - 'royalblue3' : '#3A5FCD', - 'royalblue4' : '#27408B', - 'saddlebrown' : '#8B4513', - 'salmon' : '#FA8072', - 'salmon1' : '#FF8C69', - 'salmon2' : '#EE8262', - 'salmon3' : '#CD7054', - 'salmon4' : '#8B4C39', - 'sandybrown' : '#F4A460', - 'seagreen' : '#2E8B57', - 'seagreen1' : '#54FF9F', - 'seagreen2' : '#4EEE94', - 'seagreen3' : '#43CD80', - 'seagreen4' : '#2E8B57', - 'seashell' : '#FFF5EE', - 'seashell1' : '#FFF5EE', - 'seashell2' : '#EEE5DE', - 'seashell3' : '#CDC5BF', - 'seashell4' : '#8B8682', - 'sienna' : '#A0522D', - 'sienna1' : '#FF8247', - 'sienna2' : '#EE7942', - 'sienna3' : '#CD6839', - 'sienna4' : '#8B4726', - 'silver' : '#C0C0C0', - 'skyblue' : '#87CEEB', - 'skyblue1' : '#87CEFF', - 'skyblue2' : '#7EC0EE', - 'skyblue3' : '#6CA6CD', - 'skyblue4' : '#4A708B', - 'slateblue' : '#6A5ACD', - 'slateblue1' : '#836FFF', - 'slateblue2' : '#7A67EE', - 'slateblue3' : '#6959CD', - 'slateblue4' : '#473C8B', - 'slategray' : '#708090', - 'slategray1' : '#C6E2FF', - 'slategray2' : '#B9D3EE', - 'slategray3' : '#9FB6CD', - 'slategray4' : '#6C7B8B', - 'slategrey' : '#708090', - 'snow' : '#FFFAFA', - 'snow1' : '#FFFAFA', - 'snow2' : '#EEE9E9', - 'snow3' : '#CDC9C9', - 'snow4' : '#8B8989', - 'springgreen' : '#00FF7F', - 'springgreen1' : '#00FF7F', - 'springgreen2' : '#00EE76', - 'springgreen3' : '#00CD66', - 'springgreen4' : '#008B45', - 'steelblue' : '#4682B4', - 'steelblue1' : '#63B8FF', - 'steelblue2' : '#5CACEE', - 'steelblue3' : '#4F94CD', - 'steelblue4' : '#36648B', - 'tan' : '#D2B48C', - 'tan1' : '#FFA54F', - 'tan2' : '#EE9A49', - 'tan3' : '#CD853F', - 'tan4' : '#8B5A2B', - 'teal' : '#008080', - 'thistle' : '#D8BFD8', - 'thistle1' : '#FFE1FF', - 'thistle2' : '#EED2EE', - 'thistle3' : '#CDB5CD', - 'thistle4' : '#8B7B8B', - 'tomato' : '#FF6347', - 'tomato1' : '#FF6347', - 'tomato2' : '#EE5C42', - 'tomato3' : '#CD4F39', - 'tomato4' : '#8B3626', - 'turquoise' : '#40E0D0', - 'turquoise1' : '#00F5FF', - 'turquoise2' : '#00E5EE', - 'turquoise3' : '#00C5CD', - 'turquoise4' : '#00868B', - 'violet' : '#EE82EE', - 'violetred' : '#D02090', - 'violetred1' : '#FF3E96', - 'violetred2' : '#EE3A8C', - 'violetred3' : '#CD3278', - 'violetred4' : '#8B2252', - 'webgray' : '#808080', - 'webgreen' : '#008000', - 'webgrey' : '#808080', - 'webmaroon' : '#800000', - 'webpurple' : '#800080', - 'wheat' : '#F5DEB3', - 'wheat1' : '#FFE7BA', - 'wheat2' : '#EED8AE', - 'wheat3' : '#CDBA96', - 'wheat4' : '#8B7E66', - 'white' : '#FFFFFF', - 'whitesmoke' : '#F5F5F5', - 'x11gray' : '#BEBEBE', - 'x11green' : '#00FF00', - 'x11grey' : '#BEBEBE', - 'x11maroon' : '#B03060', - 'x11purple' : '#A020F0', - 'yellow' : '#FFFF00', - 'yellow1' : '#FFFF00', - 'yellow2' : '#EEEE00', - 'yellow3' : '#CDCD00', - 'yellow4' : '#8B8B00', - 'yellowgreen' : '#9ACD32' - }; - - return ColorScheme; - -}]); diff --git a/guacamole/src/main/frontend/src/app/form/types/FieldType.js b/guacamole/src/main/frontend/src/app/form/types/FieldType.js deleted file mode 100644 index 386d623efa..0000000000 --- a/guacamole/src/main/frontend/src/app/form/types/FieldType.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the FieldType class. - */ -angular.module('form').factory('FieldType', [function defineFieldType() { - - /** - * The object used by the formService for describing field types. - * - * @constructor - * @param {FieldType|Object} [template={}] - * The object whose properties should be copied within the new - * FieldType. - */ - var FieldType = function FieldType(template) { - - // Use empty object by default - template = template || {}; - - /** - * The raw HTML of the template that should be injected into the DOM of - * a form using this field type. If provided, this will be used instead - * of templateUrl. - * - * @type String - */ - this.template = template.template; - - /** - * The URL of the template that should be injected into the DOM of a - * form using this field type. This property will be ignored if a raw - * HTML template is supplied via the template property. - * - * @type String - */ - this.templateUrl = template.templateUrl; - - /** - * The name of the AngularJS module defining the controller for this - * field type. This is optional, as not all field types will need - * controllers. - * - * @type String - */ - this.module = template.module; - - /** - * The name of the controller for this field type. This is optional, as - * not all field types will need controllers. If a controller is - * specified, it will receive the following properties on the scope: - * - * namespace: - * A String which defines the unique namespace associated the - * translation strings used by the form using a field of this type. - * - * field: - * The Field object that is being rendered, representing a field of - * this type. - * - * model: - * The current String value of the field, if any. - * - * @type String - */ - this.controller = template.controller; - - }; - - return FieldType; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js b/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js deleted file mode 100644 index 16ff9ddc0f..0000000000 --- a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupList.js +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays the contents of a connection group within an - * automatically-paginated view. - */ -angular.module('groupList').directive('guacGroupList', [function guacGroupList() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The connection groups to display as a map of data source - * identifier to corresponding root group. - * - * @type Object. - */ - connectionGroups : '=', - - /** - * Arbitrary object which shall be made available to the connection - * and connection group templates within the scope as - * context. - * - * @type Object - */ - context : '=', - - /** - * The map of @link{GroupListItem} type to the URL or ID of the - * Angular template to use when rendering a @link{GroupListItem} of - * that type. The @link{GroupListItem} itself will be within the - * scope of the template as item, and the arbitrary - * context object, if any, will be exposed as context. - * If the template for a type is omitted, items of that type will - * not be rendered. All standard types are defined by - * @link{GroupListItem.Type}, but use of custom types is legal. - * - * @type Object. - */ - templates : '=', - - /** - * Whether the root of the connection group hierarchy given should - * be shown. If false (the default), only the descendants of the - * given connection group will be listed. - * - * @type Boolean - */ - showRootGroup : '=', - - /** - * The maximum number of connections or groups to show per page. - * - * @type Number - */ - pageSize : '=', - - /** - * A callback which accepts an array of GroupListItems as its sole - * parameter. If provided, the callback will be invoked whenever an - * array of root-level GroupListItems is about to be rendered. - * Changes may be made by this function to that array or to the - * GroupListItems themselves. - * - * @type Function - */ - decorator : '=' - - }, - - templateUrl: 'app/groupList/templates/guacGroupList.html', - controller: ['$scope', '$injector', function guacGroupListController($scope, $injector) { - - // Required services - var activeConnectionService = $injector.get('activeConnectionService'); - var dataSourceService = $injector.get('dataSourceService'); - var requestService = $injector.get('requestService'); - - // Required types - var GroupListItem = $injector.get('GroupListItem'); - - /** - * Map of data source identifier to the number of active - * connections associated with a given connection identifier. - * If this information is unknown, or there are no active - * connections for a given identifier, no number will be stored. - * - * @type Object.> - */ - var connectionCount = {}; - - /** - * A list of all items which should appear at the root level. As - * connections and connection groups from multiple data sources may - * be included in a guacGroupList, there may be multiple root - * items, even if the root connection group is shown. - * - * @type GroupListItem[] - */ - $scope.rootItems = []; - - /** - * Returns the number of active usages of a given connection. - * - * @param {String} dataSource - * The identifier of the data source containing the given - * connection. - * - * @param {Connection} connection - * The connection whose active connections should be counted. - * - * @returns {Number} - * The number of currently-active usages of the given - * connection. - */ - var countActiveConnections = function countActiveConnections(dataSource, connection) { - return connectionCount[dataSource][connection.identifier]; - }; - - /** - * Returns whether a @link{GroupListItem} of the given type can be - * displayed. If there is no template associated with the given - * type, then a @link{GroupListItem} of that type cannot be - * displayed. - * - * @param {String} type - * The type to check. - * - * @returns {Boolean} - * true if the given @link{GroupListItem} type can be displayed, - * false otherwise. - */ - $scope.isVisible = function isVisible(type) { - return !!$scope.templates[type]; - }; - - // Set contents whenever the connection group is assigned or changed - $scope.$watch('connectionGroups', function setContents(connectionGroups) { - - // Reset stored data - var dataSources = []; - $scope.rootItems = []; - connectionCount = {}; - - // If connection groups are given, add them to the interface - if (connectionGroups) { - - // Add each provided connection group - angular.forEach(connectionGroups, function addConnectionGroup(connectionGroup, dataSource) { - - var rootItem; - - // Prepare data source for active connection counting - dataSources.push(dataSource); - connectionCount[dataSource] = {}; - - // If the provided connection group is already a - // GroupListItem, no need to create a new item - if (connectionGroup instanceof GroupListItem) - rootItem = connectionGroup; - - // Create root item for current connection group - else - rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, - $scope.isVisible(GroupListItem.Type.CONNECTION), - $scope.isVisible(GroupListItem.Type.SHARING_PROFILE), - countActiveConnections); - - // If root group is to be shown, add it as a root item - if ($scope.showRootGroup) - $scope.rootItems.push(rootItem); - - // Otherwise, add its children as root items - else { - angular.forEach(rootItem.children, function addRootItem(child) { - $scope.rootItems.push(child); - }); - } - - }); - - // Count active connections by connection identifier - dataSourceService.apply( - activeConnectionService.getActiveConnections, - dataSources - ) - .then(function activeConnectionsRetrieved(activeConnectionMap) { - - // Within each data source, count each active connection by identifier - angular.forEach(activeConnectionMap, function addActiveConnections(activeConnections, dataSource) { - angular.forEach(activeConnections, function addActiveConnection(activeConnection) { - - // If counter already exists, increment - var identifier = activeConnection.connectionIdentifier; - if (connectionCount[dataSource][identifier]) - connectionCount[dataSource][identifier]++; - - // Otherwise, initialize counter to 1 - else - connectionCount[dataSource][identifier] = 1; - - }); - }); - - }, requestService.DIE); - - } - - // Invoke item decorator, if provided - if ($scope.decorator) - $scope.decorator($scope.rootItems); - - }); - - /** - * Toggle the open/closed status of a group list item. - * - * @param {GroupListItem} groupListItem - * The list item to expand, which should represent a - * connection group. - */ - $scope.toggleExpanded = function toggleExpanded(groupListItem) { - groupListItem.expanded = !groupListItem.expanded; - }; - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupListFilter.js b/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupListFilter.js deleted file mode 100644 index ad1bd43106..0000000000 --- a/guacamole/src/main/frontend/src/app/groupList/directives/guacGroupListFilter.js +++ /dev/null @@ -1,304 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides a filtering text input field which automatically - * produces a filtered subset of the given connection groups. - */ -angular.module('groupList').directive('guacGroupListFilter', [function guacGroupListFilter() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The property to which a subset of the provided map of connection - * groups will be assigned. The type of each item within the - * original map is preserved within the filtered map. - * - * @type Object. - */ - filteredConnectionGroups : '=', - - /** - * The placeholder text to display within the filter input field - * when no filter has been provided. - * - * @type String - */ - placeholder : '&', - - /** - * The connection groups to filter, as a map of data source - * identifier to corresponding root group. A subset of this map - * will be exposed as filteredConnectionGroups. - * - * @type Object. - */ - connectionGroups : '&', - - /** - * An array of expressions to filter against for each connection in - * the hierarchy of connections and groups in the provided map. - * These expressions must be Angular expressions which resolve to - * properties on the connections in the provided map. - * - * @type String[] - */ - connectionProperties : '&', - - /** - * An array of expressions to filter against for each connection group - * in the hierarchy of connections and groups in the provided map. - * These expressions must be Angular expressions which resolve to - * properties on the connection groups in the provided map. - * - * @type String[] - */ - connectionGroupProperties : '&' - - }, - - templateUrl: 'app/groupList/templates/guacGroupListFilter.html', - controller: ['$scope', '$injector', function guacGroupListFilterController($scope, $injector) { - - // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var FilterPattern = $injector.get('FilterPattern'); - var GroupListItem = $injector.get('GroupListItem'); - - /** - * The pattern object to use when filtering connections. - * - * @type FilterPattern - */ - var connectionFilterPattern = new FilterPattern($scope.connectionProperties()); - - /** - * The pattern object to use when filtering connection groups. - * - * @type FilterPattern - */ - var connectionGroupFilterPattern = new FilterPattern($scope.connectionGroupProperties()); - - /** - * The filter search string to use to restrict the displayed - * connection groups. - * - * @type String - */ - $scope.searchString = null; - - /** - * Flattens the connection group hierarchy of the given connection - * group such that all descendants are copied as immediate - * children. The hierarchy of nested connection groups is otherwise - * completely preserved. A connection or connection group nested - * two or more levels deep within the hierarchy will thus appear - * within the returned connection group in two places: in its - * original location AND as an immediate child. - * - * @param {ConnectionGroup} connectionGroup - * The connection group whose descendents should be copied as - * first-level children. - * - * @returns {ConnectionGroup} - * A new connection group completely identical to the provided - * connection group, except that absolutely all descendents - * have been copied into the first level of children. - */ - var flattenConnectionGroup = function flattenConnectionGroup(connectionGroup) { - - // Replace connection group with shallow copy - connectionGroup = new ConnectionGroup(connectionGroup); - - // Ensure child arrays are defined and independent copies - connectionGroup.childConnections = angular.copy(connectionGroup.childConnections) || []; - connectionGroup.childConnectionGroups = angular.copy(connectionGroup.childConnectionGroups) || []; - - // Flatten all children to the top-level group - angular.forEach(connectionGroup.childConnectionGroups, function flattenChild(child) { - - var flattenedChild = flattenConnectionGroup(child); - - // Merge all child connections - Array.prototype.push.apply( - connectionGroup.childConnections, - flattenedChild.childConnections - ); - - // Merge all child connection groups - Array.prototype.push.apply( - connectionGroup.childConnectionGroups, - flattenedChild.childConnectionGroups - ); - - }); - - return connectionGroup; - - }; - - /** - * Flattens the connection group hierarchy of the given - * GroupListItem such that all descendants are copied as immediate - * children. The hierarchy of nested items is otherwise completely - * preserved. A connection or connection group nested two or more - * levels deep within the hierarchy will thus appear within the - * returned item in two places: in its original location AND as an - * immediate child. - * - * @param {GroupListItem} item - * The GroupListItem whose descendents should be copied as - * first-level children. - * - * @returns {GroupListItem} - * A new GroupListItem completely identical to the provided - * item, except that absolutely all descendents have been - * copied into the first level of children. - */ - var flattenGroupListItem = function flattenGroupListItem(item) { - - // Replace item with shallow copy - item = new GroupListItem(item); - - // Ensure children are defined and independent copies - item.children = angular.copy(item.children) || []; - - // Flatten all children to the top-level group - angular.forEach(item.children, function flattenChild(child) { - if (child.type === GroupListItem.Type.CONNECTION_GROUP) { - - var flattenedChild = flattenGroupListItem(child); - - // Merge all children - Array.prototype.push.apply( - item.children, - flattenedChild.children - ); - - } - }); - - return item; - - }; - - /** - * Replaces the set of children within the given GroupListItem such - * that only children which match the filter predicate for the - * current search string are present. - * - * @param {GroupListItem} item - * The GroupListItem whose children should be filtered. - */ - var filterGroupListItem = function filterGroupListItem(item) { - item.children = item.children.filter(function applyFilterPattern(child) { - - // Filter connections and connection groups by - // given pattern - switch (child.type) { - - case GroupListItem.Type.CONNECTION: - return connectionFilterPattern.predicate(child.wrappedItem); - - case GroupListItem.Type.CONNECTION_GROUP: - return connectionGroupFilterPattern.predicate(child.wrappedItem); - - } - - // Include all other children - return true; - - }); - }; - - /** - * Replaces the set of child connections and connection groups - * within the given connection group such that only children which - * match the filter predicate for the current search string are - * present. - * - * @param {ConnectionGroup} connectionGroup - * The connection group whose children should be filtered. - */ - var filterConnectionGroup = function filterConnectionGroup(connectionGroup) { - connectionGroup.childConnections = connectionGroup.childConnections.filter(connectionFilterPattern.predicate); - connectionGroup.childConnectionGroups = connectionGroup.childConnectionGroups.filter(connectionGroupFilterPattern.predicate); - }; - - /** - * Applies the current filter predicate, filtering all provided - * connection groups and storing the result in - * filteredConnectionGroups. - */ - var updateFilteredConnectionGroups = function updateFilteredConnectionGroups() { - - // Do not apply any filtering (and do not flatten) if no - // search string is provided - if (!$scope.searchString) { - $scope.filteredConnectionGroups = $scope.connectionGroups() || {}; - return; - } - - // Clear all current filtered groups - $scope.filteredConnectionGroups = {}; - - // Re-filter any provided groups - var connectionGroups = $scope.connectionGroups(); - if (connectionGroups) { - angular.forEach(connectionGroups, function updateFilteredConnectionGroup(connectionGroup, dataSource) { - - var filteredGroup; - - // Flatten and filter depending on type - if (connectionGroup instanceof GroupListItem) { - filteredGroup = flattenGroupListItem(connectionGroup); - filterGroupListItem(filteredGroup); - } - else { - filteredGroup = flattenConnectionGroup(connectionGroup); - filterConnectionGroup(filteredGroup); - } - - // Store now-filtered root - $scope.filteredConnectionGroups[dataSource] = filteredGroup; - - }); - } - - }; - - // Recompile and refilter when pattern is changed - $scope.$watch('searchString', function searchStringChanged(searchString) { - connectionFilterPattern.compile(searchString); - connectionGroupFilterPattern.compile(searchString); - updateFilteredConnectionGroups(); - }); - - // Refilter when items change - $scope.$watchCollection($scope.connectionGroups, function itemsChanged() { - updateFilteredConnectionGroups(); - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupList.html b/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupList.html deleted file mode 100644 index e76f2e394d..0000000000 --- a/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupList.html +++ /dev/null @@ -1,41 +0,0 @@ -
    - - - - -
    -
    -
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupListFilter.html b/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupListFilter.html deleted file mode 100644 index bbaec1c885..0000000000 --- a/guacamole/src/main/frontend/src/app/groupList/templates/guacGroupListFilter.html +++ /dev/null @@ -1,6 +0,0 @@ -
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/history/services/guacHistory.js b/guacamole/src/main/frontend/src/app/history/services/guacHistory.js deleted file mode 100644 index d5dcbfe071..0000000000 --- a/guacamole/src/main/frontend/src/app/history/services/guacHistory.js +++ /dev/null @@ -1,106 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for reading and manipulating the Guacamole connection history. - */ -angular.module('history').factory('guacHistory', ['$injector', - function guacHistory($injector) { - - // Required types - var HistoryEntry = $injector.get('HistoryEntry'); - - // Required services - var localStorageService = $injector.get('localStorageService'); - var preferenceService = $injector.get('preferenceService'); - - var service = {}; - - // The parameter name for getting the history from local storage - var GUAC_HISTORY_STORAGE_KEY = "GUAC_HISTORY"; - - /** - * The top few recent connections, sorted in order of most recent access. - * - * @type HistoryEntry[] - */ - service.recentConnections = []; - - /** - * Remove from the list of connection history the item having the given - * identfier. - * - * @param {String} id - * The identifier of the item to remove from the history list. - * - * @returns {boolean} - * True if the removal was successful, otherwise false. - */ - service.removeEntry = function removeEntry(id) { - - return _.remove(service.recentConnections, entry => entry.id === id).length > 0; - - }; - - /** - * Updates the thumbnail and access time of the history entry for the - * connection with the given ID. - * - * @param {String} id - * The ID of the connection whose history entry should be updated. - * - * @param {String} thumbnail - * The URL of the thumbnail image to associate with the history entry. - */ - service.updateThumbnail = function(id, thumbnail) { - - var i; - - // Remove any existing entry for this connection - for (i=0; i < service.recentConnections.length; i++) { - if (service.recentConnections[i].id === id) { - service.recentConnections.splice(i, 1); - break; - } - } - - // Store new entry in history - service.recentConnections.unshift(new HistoryEntry( - id, - thumbnail, - new Date().getTime() - )); - - // Truncate history to ideal length - if (service.recentConnections.length > preferenceService.preferences.numberOfRecentConnections) - service.recentConnections.length = preferenceService.preferences.numberOfRecentConnections; - - // Save updated history - localStorageService.setItem(GUAC_HISTORY_STORAGE_KEY, service.recentConnections); - - }; - - // Init stored connection history from localStorage - var storedHistory = localStorageService.getItem(GUAC_HISTORY_STORAGE_KEY) || []; - if (storedHistory instanceof Array) - service.recentConnections = storedHistory; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/home/controllers/homeController.js b/guacamole/src/main/frontend/src/app/home/controllers/homeController.js deleted file mode 100644 index 8d84b49e1a..0000000000 --- a/guacamole/src/main/frontend/src/app/home/controllers/homeController.js +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for the home page. - */ -angular.module('home').controller('homeController', ['$scope', '$injector', - function homeController($scope, $injector) { - - // Get required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var GroupListItem = $injector.get('GroupListItem'); - - // Get required services - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var preferenceService = $injector.get('preferenceService'); - var requestService = $injector.get('requestService'); - - /** - * Map of data source identifier to the root connection group of that data - * source, or null if the connection group hierarchy has not yet been - * loaded. - * - * @type Object. - */ - $scope.rootConnectionGroups = null; - - /** - * Array of all connection properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionProperties = [ - 'name' - ]; - - /** - * Array of all connection group properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionGroupProperties = [ - 'name' - ]; - - /** - * Returns whether the "Recent Connections" section should be displayed on - * the home screen. - * - * @returns {!boolean} - * true if recent connections should be displayed on the home screen, - * false otherwise. - */ - $scope.isRecentConnectionsVisible = function isRecentConnectionsVisible() { - return preferenceService.preferences.showRecentConnections; - }; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.rootConnectionGroups !== null; - - }; - - // Retrieve root groups and all descendants - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - authenticationService.getAvailableDataSources(), - ConnectionGroup.ROOT_IDENTIFIER - ) - .then(function rootGroupsRetrieved(rootConnectionGroups) { - $scope.rootConnectionGroups = rootConnectionGroups; - }, requestService.DIE); - -}]); diff --git a/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js b/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js deleted file mode 100644 index 4be6065248..0000000000 --- a/guacamole/src/main/frontend/src/app/home/directives/guacRecentConnections.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays the recently-accessed connections nested beneath - * each of the given connection groups. - */ -angular.module('home').directive('guacRecentConnections', [function guacRecentConnections() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The root connection groups to display, and all visible - * descendants, as a map of data source identifier to the root - * connection group within that data source. Recent connections - * will only be shown if they exist within this hierarchy, - * regardless of their existence within the history. - * - * @type Object. - */ - rootGroups : '=' - - }, - - templateUrl: 'app/home/templates/guacRecentConnections.html', - controller: ['$scope', '$injector', function guacRecentConnectionsController($scope, $injector) { - - // Required types - var ClientIdentifier = $injector.get('ClientIdentifier'); - var RecentConnection = $injector.get('RecentConnection'); - - // Required services - var guacHistory = $injector.get('guacHistory'); - var preferenceService = $injector.get('preferenceService'); - - /** - * Array of all known and visible recently-used connections. - * - * @type RecentConnection[] - */ - $scope.recentConnections = []; - - /** - * Remove the connection from the recent connection list having the - * given identifier. - * - * @param {!RecentConnection} recentConnection - * The recent connection to remove from the history list. - * - * @returns {boolean} - * True if the removal was successful, otherwise false. - */ - $scope.removeRecentConnection = function removeRecentConnection(recentConnection) { - return ($scope.recentConnections.splice($scope.recentConnections.indexOf(recentConnection), 1) - && guacHistory.removeEntry(recentConnection.entry.id)); - }; - - /** - * Returns whether recent connections are available for display. - * - * @returns {!boolean} - * true if recent connections are present, false otherwise. - */ - $scope.hasRecentConnections = function hasRecentConnections() { - return !!$scope.recentConnections.length; - }; - - /** - * Map of all visible objects, connections or connection groups, by - * object identifier. - * - * @type Object. - */ - var visibleObjects = {}; - - /** - * Adds the given connection to the internal set of visible - * objects. - * - * @param {String} dataSource - * The identifier of the data source associated with the - * given connection group. - * - * @param {Connection} connection - * The connection to add to the internal set of visible objects. - */ - var addVisibleConnection = function addVisibleConnection(dataSource, connection) { - - // Add given connection to set of visible objects - visibleObjects[ClientIdentifier.toString({ - dataSource : dataSource, - type : ClientIdentifier.Types.CONNECTION, - id : connection.identifier - })] = connection; - - }; - - /** - * Adds the given connection group to the internal set of visible - * objects, along with any descendants. - * - * @param {String} dataSource - * The identifier of the data source associated with the - * given connection group. - * - * @param {ConnectionGroup} connectionGroup - * The connection group to add to the internal set of visible - * objects, along with any descendants. - */ - var addVisibleConnectionGroup = function addVisibleConnectionGroup(dataSource, connectionGroup) { - - // Add given connection group to set of visible objects - visibleObjects[ClientIdentifier.toString({ - dataSource : dataSource, - type : ClientIdentifier.Types.CONNECTION_GROUP, - id : connectionGroup.identifier - })] = connectionGroup; - - // Add all child connections - if (connectionGroup.childConnections) - connectionGroup.childConnections.forEach(function addChildConnection(child) { - addVisibleConnection(dataSource, child); - }); - - // Add all child connection groups - if (connectionGroup.childConnectionGroups) - connectionGroup.childConnectionGroups.forEach(function addChildConnectionGroup(child) { - addVisibleConnectionGroup(dataSource, child); - }); - - }; - - // Update visible objects when root groups are set - $scope.$watch("rootGroups", function setRootGroups(rootGroups) { - - // Clear connection arrays - $scope.recentConnections = []; - - // Produce collection of visible objects - visibleObjects = {}; - if (rootGroups) { - angular.forEach(rootGroups, function addConnectionGroup(rootGroup, dataSource) { - addVisibleConnectionGroup(dataSource, rootGroup); - }); - } - - // Add any recent connections that are visible - guacHistory.recentConnections.forEach(function addRecentConnection(historyEntry) { - - // Add recent connections for history entries with associated visible objects - if (historyEntry.id in visibleObjects) { - - var object = visibleObjects[historyEntry.id]; - $scope.recentConnections.push(new RecentConnection(object.name, historyEntry)); - - } - - }); - - }); // end rootGroup scope watch - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/home/templates/connection.html b/guacamole/src/main/frontend/src/app/home/templates/connection.html deleted file mode 100644 index 85a7f573b5..0000000000 --- a/guacamole/src/main/frontend/src/app/home/templates/connection.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
    - - - {{item.name}} - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html deleted file mode 100644 index ba0a204ba1..0000000000 --- a/guacamole/src/main/frontend/src/app/home/templates/connectionGroup.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -
    - - - {{item.name}} - -
    diff --git a/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html b/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html deleted file mode 100644 index 2d25bc1db8..0000000000 --- a/guacamole/src/main/frontend/src/app/home/templates/guacRecentConnections.html +++ /dev/null @@ -1,25 +0,0 @@ -
    - - -

    {{'HOME.INFO_NO_RECENT_CONNECTIONS' | translate}}

    - - - diff --git a/guacamole/src/main/frontend/src/app/home/templates/home.html b/guacamole/src/main/frontend/src/app/home/templates/home.html deleted file mode 100644 index 872ac98033..0000000000 --- a/guacamole/src/main/frontend/src/app/home/templates/home.html +++ /dev/null @@ -1,36 +0,0 @@ -
    - -
    - - -
    -

    {{'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | translate}}

    - -
    - - - -
    -

    {{'HOME.SECTION_HEADER_ALL_CONNECTIONS' | translate}}

    - - -
    -
    - -
    - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js b/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js deleted file mode 100644 index b4688203f7..0000000000 --- a/guacamole/src/main/frontend/src/app/import/controllers/importConnectionsController.js +++ /dev/null @@ -1,699 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global _ */ - -/** - * The allowed MIME type for CSV files. - * - * @type String - */ -const CSV_MIME_TYPE = 'text/csv'; - -/** - * A fallback regular expression for CSV filenames, if no MIME type is provided - * by the browser. Any file that matches this regex will be considered to be a - * CSV file. - * - * @type RegExp - */ -const CSV_FILENAME_REGEX = /\.csv$/i; - -/** - * The allowed MIME type for JSON files. - * - * @type String - */ -const JSON_MIME_TYPE = 'application/json'; - -/** - * A fallback regular expression for JSON filenames, if no MIME type is provided - * by the browser. Any file that matches this regex will be considered to be a - * JSON file. - * - * @type RegExp - */ -const JSON_FILENAME_REGEX = /\.json$/i; - -/** - * The allowed MIME types for YAML files. - * NOTE: There is no registered MIME type for YAML files. This may result in a - * wide variety of possible browser-supplied MIME types. - * - * @type String[] - */ -const YAML_MIME_TYPES = [ - 'text/x-yaml', - 'text/yaml', - 'text/yml', - 'application/x-yaml', - 'application/x-yml', - 'application/yaml', - 'application/yml' -]; - -/** - * A fallback regular expression for YAML filenames, if no MIME type is provided - * by the browser. Any file that matches this regex will be considered to be a - * YAML file. - * - * @type RegExp - */ -const YAML_FILENAME_REGEX = /\.ya?ml$/i; - -/** - * Possible signatures for zip files (which include most modern Microsoft office - * documents - most notable excel). If any file, regardless of extension, has - * these starting bytes, it's invalid and must be rejected. - * For more, see https://en.wikipedia.org/wiki/List_of_file_signatures and - * https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files. - * - * @type String[] - */ -const ZIP_SIGNATURES = [ - 'PK\u0003\u0004', - 'PK\u0005\u0006', - 'PK\u0007\u0008' -]; - -/* - * All file types supported for connection import. - * - * @type {String[]} - */ -const LEGAL_MIME_TYPES = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES]; - -/** - * The controller for the connection import page. - */ -angular.module('import').controller('importConnectionsController', ['$scope', '$injector', - function importConnectionsController($scope, $injector) { - - // Required services - const $location = $injector.get('$location'); - const $q = $injector.get('$q'); - const $routeParams = $injector.get('$routeParams'); - const connectionParseService = $injector.get('connectionParseService'); - const connectionService = $injector.get('connectionService'); - const guacNotification = $injector.get('guacNotification'); - const permissionService = $injector.get('permissionService'); - const userService = $injector.get('userService'); - const userGroupService = $injector.get('userGroupService'); - - // Required types - const ConnectionImportConfig = $injector.get('ConnectionImportConfig'); - const DirectoryPatch = $injector.get('DirectoryPatch'); - const Error = $injector.get('Error'); - const ParseError = $injector.get('ParseError'); - const PermissionSet = $injector.get('PermissionSet'); - const User = $injector.get('User'); - const UserGroup = $injector.get('UserGroup'); - - /** - * The result of parsing the current upload, if successful. - * - * @type {ParseResult} - */ - $scope.parseResult = null; - - /** - * The failure associated with the current attempt to create connections - * through the API, if any. - * - * @type {Error} - */ - $scope.patchFailure = null;; - - /** - * True if the file is fully uploaded and ready to be processed, or false - * otherwise. - * - * @type {Boolean} - */ - $scope.dataReady = false; - - /** - * True if the file upload has been aborted mid-upload, or false otherwise. - */ - $scope.aborted = false; - - /** - * True if fully-uploaded data is being processed, or false otherwise. - */ - $scope.processing = false; - - /** - * The MIME type of the uploaded file, if any. - * - * @type {String} - */ - $scope.mimeType = null; - - /** - * The name of the file that's currently being uploaded, or has yet to - * be imported, if any. - * - * @type {String} - */ - $scope.fileName = null; - - /** - * The raw string contents of the uploaded file, if any. - * - * @type {String} - */ - $scope.fileData = null; - - /** - * The file reader currently being used to upload the file, if any. If - * null, no file upload is currently in progress. - * - * @type {FileReader} - */ - $scope.fileReader = null; - - /** - * The configuration options for this import, to be chosen by the user. - * - * @type {ConnectionImportConfig} - */ - $scope.importConfig = new ConnectionImportConfig(); - - /** - * Clear all file upload state. - */ - function resetUploadState() { - - $scope.aborted = false; - $scope.dataReady = false; - $scope.processing = false; - $scope.fileData = null; - $scope.mimeType = null; - $scope.fileReader = null; - $scope.parseResult = null; - $scope.patchFailure = null; - $scope.fileName = null; - - } - - // Indicate that data is currently being loaded / processed if the the file - // has been provided but not yet fully uploaded, or if the the file is - // fully loaded and is currently being processed. - $scope.isLoading = () => ( - ($scope.fileName && !$scope.dataReady && !$scope.patchFailure) - || $scope.processing); - - /** - * Create all users and user groups mentioned in the import file that don't - * already exist in the current data source. Return an object describing the - * result of the creation requests. - * - * @param {ParseResult} parseResult - * The result of parsing the user-supplied import file. - * - * @return {Promise.} - * A promise resolving to an object containing the results of the calls - * to create the users and groups. - */ - function createUsersAndGroups(parseResult) { - - const dataSource = $routeParams.dataSource; - - return $q.all({ - existingUsers : userService.getUsers(dataSource), - existingGroups : userGroupService.getUserGroups(dataSource) - }).then(({existingUsers, existingGroups}) => { - - const userPatches = Object.keys(parseResult.users) - - // Filter out any existing users - .filter(identifier => !existingUsers[identifier]) - - // A patch to create each new user - .map(username => new DirectoryPatch({ - op: 'add', - path: '/', - value: new User({ username }) - })); - - const groupPatches = Object.keys(parseResult.groups) - - // Filter out any existing groups - .filter(identifier => !existingGroups[identifier]) - - // A patch to create each new user group - .map(identifier => new DirectoryPatch({ - op: 'add', - path: '/', - value: new UserGroup({ identifier }) - })); - - // Create all the users and groups - return $q.all({ - userResponse: userService.patchUsers(dataSource, userPatches), - userGroupResponse: userGroupService.patchUserGroups( - dataSource, groupPatches) - }); - - }); - - } - - /** - * Grant read permissions for each user and group in the supplied parse - * result to each connection in their connection list. Note that there will - * be a seperate request for each user and group. - * - * @param {ParseResult} parseResult - * The result of successfully parsing a user-supplied import file. - * - * @param {Object} response - * The response from the PATCH API request. - * - * @returns {Promise.} - * A promise that will resolve with the result of every permission - * granting request. - */ - function grantConnectionPermissions(parseResult, response) { - - const dataSource = $routeParams.dataSource; - - // All connection grant requests, one per user/group - const userRequests = {}; - const groupRequests = {}; - - // Create a PermissionSet granting access to all connections at - // the provided indices within the provided parse result - const createPermissionSet = indices => - new PermissionSet({ connectionPermissions: indices.reduce( - (permissions, index) => { - const connectionId = response.patches[index].identifier; - permissions[connectionId] = [ - PermissionSet.ObjectPermissionType.READ]; - return permissions; - }, {}) }); - - // Now that we've created all the users, grant access to each - _.forEach(parseResult.users, (connectionIndices, identifier) => - - // Grant the permissions - note the group flag is `false` - userRequests[identifier] = permissionService.patchPermissions( - dataSource, identifier, - - // Create the permissions to these connections for this user - createPermissionSet(connectionIndices), - - // Do not remove any permissions - new PermissionSet(), - - // This call is not for a group - false)); - - // Now that we've created all the groups, grant access to each - _.forEach(parseResult.groups, (connectionIndices, identifier) => - - // Grant the permissions - note the group flag is `true` - groupRequests[identifier] = permissionService.patchPermissions( - dataSource, identifier, - - // Create the permissions to these connections for this user - createPermissionSet(connectionIndices), - - // Do not remove any permissions - new PermissionSet(), - - // This call is for a group - true)); - - // Return the result from all the permission granting calls - return $q.all({ ...userRequests, ...groupRequests }); - } - - /** - * Process a successfully parsed import file, creating any specified - * connections, creating and granting permissions to any specified users - * and user groups. If successful, the user will be shown a success message. - * If not, any errors will be displayed and any already-created entities - * will be rolled back. - * - * @param {ParseResult} parseResult - * The result of parsing the user-supplied import file. - */ - function handleParseSuccess(parseResult) { - - $scope.processing = false; - $scope.parseResult = parseResult; - - // If errors were encounted during file parsing, abort further - // processing - the user will have a chance to fix the errors and try - // again - if (parseResult.hasErrors) - return; - - const dataSource = $routeParams.dataSource; - - // First, attempt to create the connections - connectionService.patchConnections(dataSource, parseResult.patches) - .then(connectionResponse => - - // If connection creation is successful, create users and groups - createUsersAndGroups(parseResult).then(() => - - // Grant any new permissions to users and groups. NOTE: Any - // existing permissions for updated connections will NOT be - // removed - only new permissions will be added. - grantConnectionPermissions(parseResult, connectionResponse) - .then(() => { - - $scope.processing = false; - - // Display a success message if everything worked - guacNotification.showStatus({ - className : 'success', - title : 'IMPORT.DIALOG_HEADER_SUCCESS', - text : { - key: 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS', - variables: { NUMBER: parseResult.connectionCount } - }, - - // Add a button to acknowledge and redirect to - // the connection listing page - actions : [{ - name : 'IMPORT.ACTION_ACKNOWLEDGE', - callback : () => { - - // Close the notification - guacNotification.showStatus(false); - - // Redirect to connection list page - $location.url('/settings/' + dataSource + '/connections'); - } - }] - }); - })) - - // If an error occurs while trying to create users or groups, - // display the error to the user. - .catch(handleError) - ) - - // If an error occurred when the call to create the connections was made, - // skip any further processing - the user will have a chance to fix the - // problems and try again - .catch(patchFailure => { - $scope.processing = false; - $scope.patchFailure = patchFailure; - }); - } - - /** - * Display the provided error to the user in a dismissable dialog. - * - * @argument {ParseError|Error} error - * The error to display. - */ - const handleError = error => { - - // Any error indicates that processing of the file has failed, so clear - // all upload state to allow for a fresh retry - resetUploadState(); - - let text; - - // If it's a import file parsing error - if (error instanceof ParseError) - text = { - - // Use the translation key if available - key: error.key || error.message, - variables: error.variables - }; - - // If it's a generic REST error - else if (error instanceof Error) - text = error.translatableMessage; - - // If it's an unknown type, just use the message directly - else - text = { key: error }; - - guacNotification.showStatus({ - className : 'error', - title : 'IMPORT.DIALOG_HEADER_ERROR', - text, - - // Add a button to hide the error - actions : [{ - name : 'IMPORT.ACTION_ACKNOWLEDGE', - callback : () => guacNotification.showStatus(false) - }] - }); - - }; - - /** - * Process the uploaded import file, importing the connections, granting - * connection permissions, or displaying errors to the user if there are - * problems with the provided file. - * - * @param {String} mimeType - * The MIME type of the uploaded data file. - * - * @param {String} data - * The raw string contents of the import file. - */ - function processData(mimeType, data) { - - // Data processing has begun - $scope.processing = true; - - // The function that will process all the raw data and return a list of - // patches to be submitted to the API - let processDataCallback; - - // Choose the appropriate parse function based on the mimetype - if (mimeType === JSON_MIME_TYPE) - processDataCallback = connectionParseService.parseJSON; - - else if (mimeType === CSV_MIME_TYPE) - processDataCallback = connectionParseService.parseCSV; - - else if (YAML_MIME_TYPES.indexOf(mimeType) >= 0) - processDataCallback = connectionParseService.parseYAML; - - // The file type was validated before being uploaded - this should - // never happen - else - processDataCallback = () => { - throw new ParseError({ - message: "Unexpected invalid file type: " + mimeType - }); - }; - - // Make the call to process the data into a series of patches - processDataCallback($scope.importConfig, data) - - // Send the data off to be imported if parsing is successful - .then(handleParseSuccess) - - // Display any error found while parsing the file - .catch(handleError); - } - - /** - * Process the uploaded import data. Only usuable if the upload is fully - * complete. - */ - $scope.import = () => processData($scope.mimeType, $scope.fileData); - - /** - * Returns true if import should be disabled, or false if import should be - * allowed. - * - * @return {Boolean} - * True if import should be disabled, otherwise false. - */ - $scope.importDisabled = () => - - // Disable import if no data is ready - !$scope.dataReady || - - // Disable import if the file is currently being processed - $scope.processing; - - /** - * Cancel any in-progress upload, or clear any uploaded-but-errored-out - * batch. - */ - $scope.cancel = function() { - - // If the upload is in progress, stop it now; the FileReader will - // reset the upload state when it stops - if ($scope.fileReader) { - $scope.aborted = true; - $scope.fileReader.abort(); - } - - // Clear any upload state - there's no FileReader handler to do it - else - resetUploadState(); - - }; - - /** - * Returns true if cancellation should be disabled, or false if - * cancellation should be allowed. - * - * @return {Boolean} - * True if cancellation should be disabled, or false if cancellation - * should be allowed. - */ - $scope.cancelDisabled = () => - - // Disable cancellation if the import has already been cancelled - $scope.aborted || - - // Disable cancellation if the file is currently being processed - $scope.processing || - - // Disable cancellation if no data is ready or being uploaded - !($scope.fileReader || $scope.dataReady); - - /** - * Handle a provided File upload, reading all data onto the scope for - * import processing, should the user request an import. Note that this - * function is used as a callback for directives that invoke it with a file - * list, but directive-level checking should ensure that there is only ever - * one file provided at a time. - * - * @argument {File[]} files - * The files to upload onto the scope for further processing. There - * should only ever be a single file in the array. - */ - $scope.handleFiles = files => { - - // There should only ever be a single file in the array - const file = files[0]; - - // The name and MIME type of the file as provided by the browser - let fileName = file.name; - let mimeType = file.type; - - // If no MIME type was provided by the browser at all, use REGEXes as a - // fallback to try to determine the file type. NOTE: Windows 10/11 are - // known to do this with YAML files. - if (!_.trim(mimeType).length) { - - // If the file name matches what we'd expect for a CSV file, set the - // CSV MIME type and move on - if (CSV_FILENAME_REGEX.test(fileName)) - mimeType = CSV_MIME_TYPE; - - // If the file name matches what we'd expect for a JSON file, set - // the JSON MIME type and move on - else if (JSON_FILENAME_REGEX.test(fileName)) - mimeType = JSON_MIME_TYPE; - - // If the file name matches what we'd expect for a JSON file, set - // one of the allowed YAML MIME types and move on - else if (YAML_FILENAME_REGEX.test(fileName)) - mimeType = YAML_MIME_TYPES[0]; - - else { - - // If none of the REGEXes pass, there's nothing more to be tried - handleError(new ParseError({ - message: "Unknown type for file: " + fileName, - key: 'IMPORT.ERROR_DETECTED_INVALID_TYPE' - })); - return; - - } - - } - - // Check if the mimetype is one of the supported types, - // e.g. "application/json" or "text/csv" - else if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) { - - // If the provided file is not one of the supported types, - // display an error and abort processing - handleError(new ParseError({ - message: "Invalid file type: " + mimeType, - key: 'IMPORT.ERROR_INVALID_MIME_TYPE', - variables: { TYPE: mimeType } - })); - return; - - } - - // Save the name and type to the scope - $scope.fileName = fileName; - $scope.mimeType = mimeType; - - // Initialize upload state - $scope.aborted = false; - $scope.dataReady = false; - $scope.processing = false; - $scope.uploadStarted = true; - - // Save the file to the scope when ready - $scope.fileReader = new FileReader(); - $scope.fileReader.onloadend = (e => { - - // If the upload was explicitly aborted, clear any upload state and - // do not process the data - if ($scope.aborted) - resetUploadState(); - - else { - - const fileData = e.target.result; - - // Check if the file has a header of a known-bad type - if (_.some(ZIP_SIGNATURES, - signature => fileData.startsWith(signature))) { - - // Throw an error and abort processing - handleError(new ParseError({ - message: "Invalid file type detected", - key: 'IMPORT.ERROR_DETECTED_INVALID_TYPE' - })); - return; - - } - - // Save the uploaded data - $scope.fileData = fileData; - - // Mark the data as ready - $scope.dataReady = true; - - // Clear the file reader from the scope now that this file is - // fully uploaded - $scope.fileReader = null; - - } - }); - - // Read all the data into memory - $scope.fileReader.readAsText(file); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js b/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js deleted file mode 100644 index 17d2fef4bc..0000000000 --- a/guacamole/src/main/frontend/src/app/import/directives/connectionImportErrors.js +++ /dev/null @@ -1,319 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global _ */ - -/** - * A directive that displays errors that occurred during parsing of a connection - * import file, or errors that were returned from the API during the connection - * batch creation attempt. - */ -angular.module('import').directive('connectionImportErrors', [ - function connectionImportErrors() { - - const directive = { - restrict: 'E', - replace: true, - templateUrl: 'app/import/templates/connectionErrors.html', - scope: { - - /** - * The result of parsing the import file. Any errors in this file - * will be displayed to the user. - * - * @type ParseResult - */ - parseResult : '=', - - /** - * The error associated with an attempt to batch create the - * connections represented by the ParseResult, if the ParseResult - * had no errors. If the provided ParseResult has errors, no request - * should have been made, and any provided patch error will be - * ignored. - * - * @type Error - */ - patchFailure : '=', - - } - }; - - directive.controller = ['$scope', '$injector', - function connectionImportErrorsController($scope, $injector) { - - // Required types - const DirectoryPatch = $injector.get('DirectoryPatch'); - const DisplayErrorList = $injector.get('DisplayErrorList'); - const ImportConnectionError = $injector.get('ImportConnectionError'); - const ParseError = $injector.get('ParseError'); - const SortOrder = $injector.get('SortOrder'); - - // Required services - const $q = $injector.get('$q'); - const $translate = $injector.get('$translate'); - - // There are errors to display if the parse result generated errors, or - // if the patch request failed - $scope.hasErrors = () => - !!_.get($scope, 'parseResult.hasErrors') || !!$scope.patchFailure; - - /** - * All connections with their associated errors for display. These may - * be either parsing failures, or errors returned from the API. Both - * error types will be adapted to a common display format, though the - * error types will never be mixed, because no REST request should ever - * be made if there are client-side parse errors. - * - * @type {ImportConnectionError[]} - */ - $scope.connectionErrors = []; - - /** - * SortOrder instance which maintains the sort order of the visible - * connection errors. - * - * @type SortOrder - */ - $scope.errorOrder = new SortOrder([ - 'rowNumber', - 'name', - 'group', - 'protocol', - 'errors', - ]); - - /** - * Array of all connection error properties that are filterable. - * - * @type String[] - */ - $scope.filteredErrorProperties = [ - 'rowNumber', - 'name', - 'group', - 'protocol', - 'errors', - ]; - - /** - * Generate a ImportConnectionError representing any errors associated - * with the row at the given index within the given parse result. - * - * @param {ParseResult} parseResult - * The result of parsing the connection import file. - * - * @param {Integer} index - * The current row within the patches array, 0-indexed. - * - * @param {Integer} row - * The current row within the original connection, 0-indexed. - * If any REMOVE patches are present, this may be greater than - * the index. - * - * @returns {ImportConnectionError} - * The connection error object associated with the given row in the - * given parse result. - */ - const generateConnectionError = (parseResult, index, row) => { - - // Get the patch associated with the current row - const patch = parseResult.patches[index]; - - // The value of a patch is just the Connection object - const connection = patch.value; - - return new ImportConnectionError({ - - // Add 1 to the provided row to get the position in the file - rowNumber: row + 1, - - // Basic connection information - name, group, and protocol. - name: connection.name, - group: parseResult.groupPaths[index], - protocol: connection.protocol, - - // The human-readable error messages - errors: new DisplayErrorList( - [ ...(parseResult.errors[index] || []) ]) - }); - }; - - // If a new connection patch failure is seen, update the display list - $scope.$watch('patchFailure', function patchFailureChanged(patchFailure) { - - const { parseResult } = $scope; - - // Do not attempt to process anything before the data has loaded - if (!patchFailure || !parseResult) - return; - - // All promises from all translation requests. The scope will not be - // updated until all translations are ready. - const translationPromises = []; - - // Any error returned from the API specifically associated with the - // preceding REMOVE patch - let removeError = null; - - // Fetch the API error, if any, of the patch at the given index - const getAPIError = index => - _.get(patchFailure, ['patches', index, 'error']); - - // The row number for display. Unlike the index, this number will - // skip any REMOVE patches. In other words, this is the index of - // connections within the original import file. - let row = 0; - - // Set up the list of connection errors based on the existing parse - // result, with error messages fetched from the patch failure - const connectionErrors = parseResult.patches.reduce( - (errors, patch, index) => { - - // Do not process display REMOVE patches - they are always - // followed by ADD patches containing the actual content - // (and errors, if any) - if (patch.op === DirectoryPatch.Operation.REMOVE) { - - // Save the API error, if any, so it can be displayed - // alongside the connection information associated with the - // following ADD patch - removeError = getAPIError(index); - - // Do not add an entry for this remove patch - it should - // always be followed by a corresponding CREATE patch - // containing the relevant connection information - return errors; - - } - - // Generate a connection error for display - const connectionError = generateConnectionError( - parseResult, index, row++); - - // Add the error associated with the previous REMOVE patch, if - // any, to the error associated with the current patch, if any - const apiErrors = [ removeError, getAPIError(index) ]; - - // Clear the previous REMOVE patch error after consuming it - removeError = null; - - // Go through each potential API error - apiErrors.forEach(error => - - // If the API error exists, fetch the translation and - // update it when it's ready - error && translationPromises.push($translate( - error.key, error.variables) - .then(translatedError => - connectionError.errors.getArray().push(translatedError) - ))); - - errors.push(connectionError); - return errors; - - }, []); - - // Once all the translations have been completed, update the - // connectionErrors all in one go, to ensure no excessive reloading - $q.all(translationPromises).then(() => { - $scope.connectionErrors = connectionErrors; - }); - - }); - - // If a new parse result with errors is seen, update the display list - $scope.$watch('parseResult', function parseResultChanged(parseResult) { - - // Do not process if there are no errors in the provided result - if (!parseResult || !parseResult.hasErrors) - return; - - // All promises from all translation requests. The scope will not be - // updated until all translations are ready. - const translationPromises = []; - - // The parse result should only be updated on a fresh file import; - // therefore it should be safe to skip checking the patch errors - // entirely - if set, they will be from the previous file and no - // longer relevant. - - // The row number for display. Unlike the index, this number will - // skip any REMOVE patches. In other words, this is the index of - // connections within the original import file. - let row = 0; - - // Set up the list of connection errors based on the updated parse - // result - const connectionErrors = parseResult.patches.reduce( - (errors, patch, index) => { - - // Do not process display REMOVE patches - they are always - // followed by ADD patches containing the actual content - // (and errors, if any) - if (patch.op === DirectoryPatch.Operation.REMOVE) - return errors; - - // Generate a connection error for display - const connectionError = generateConnectionError( - parseResult, index, row++); - - // Go through the errors and check if any are translateable - connectionError.errors.getArray().forEach( - (error, errorIndex) => { - - // If this error is a ParseError, it can be translated. - // NOTE: Generally one would translate error messages in the - // template, but in this case, the connection errors need to - // be raw strings in order to enable sorting and filtering. - if (error instanceof ParseError) - - // Fetch the translation and update it when it's ready - translationPromises.push($translate( - error.key, error.variables) - .then(translatedError => { - connectionError.errors.getArray()[errorIndex] = translatedError; - })); - - // If the error is not a known translatable type, add the - // message directly to the error array - else - connectionError.errors.getArray()[errorIndex] = ( - error.message ? error.message : error); - - }); - - errors.push(connectionError); - return errors; - - }, []); - - // Once all the translations have been completed, update the - // connectionErrors all in one go, to ensure no excessive reloading - $q.all(translationPromises).then(() => { - $scope.connectionErrors = connectionErrors; - }); - - }); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/import/importModule.js b/guacamole/src/main/frontend/src/app/import/importModule.js deleted file mode 100644 index 45551e3238..0000000000 --- a/guacamole/src/main/frontend/src/app/import/importModule.js +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The module for code supporting importing user-supplied files. Currently, only - * connection import is supported. - */ -angular.module('import', ['element', 'list', 'notification', 'rest']); diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js b/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js deleted file mode 100644 index 375ce0a2a0..0000000000 --- a/guacamole/src/main/frontend/src/app/import/services/connectionCSVService.js +++ /dev/null @@ -1,461 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* global _ */ - -// A suffix that indicates that a particular header refers to a parameter -const PARAMETER_SUFFIX = ' (parameter)'; - -// A suffix that indicates that a particular header refers to an attribute -const ATTRIBUTE_SUFFIX = ' (attribute)'; - -/** - * A service for parsing user-provided CSV connection data for bulk import. - */ -angular.module('import').factory('connectionCSVService', - ['$injector', function connectionCSVService($injector) { - - // Required types - const ParseError = $injector.get('ParseError'); - const ImportConnection = $injector.get('ImportConnection'); - const TranslatableMessage = $injector.get('TranslatableMessage'); - - // Required services - const $q = $injector.get('$q'); - const $routeParams = $injector.get('$routeParams'); - const schemaService = $injector.get('schemaService'); - - const service = {}; - - /** - * Returns a promise that resolves to a object detailing the connection - * attributes for the current data source, as well as the connection - * paremeters for every protocol, for the current data source. - * - * The object that the promise will contain an "attributes" key that maps to - * a set of attribute names, and a "protocolParameters" key that maps to an - * object mapping protocol names to sets of parameter names for that protocol. - * - * The intended use case for this object is to determine if there is a - * connection parameter or attribute with a given name, by e.g. checking the - * path `.protocolParameters[protocolName]` to see if a protocol exists, - * checking the path `.protocolParameters[protocolName][fieldName]` to see - * if a parameter exists for a given protocol, or checking the path - * `.attributes[fieldName]` to check if a connection attribute exists. - * - * @returns {Promise.} - * A promise that resolves to a object detailing the connection - * attributes and parameters for every protocol, for the current data - * source. - */ - function getFieldLookups() { - - // The current data source - the one that the connections will be - // imported into - const dataSource = $routeParams.dataSource; - - // Fetch connection attributes and protocols for the current data source - return $q.all({ - attributes : schemaService.getConnectionAttributes(dataSource), - protocols : schemaService.getProtocols(dataSource) - }) - .then(function connectionStructureRetrieved({attributes, protocols}) { - - return { - - // Translate the forms and fields into a flat map of attribute - // name to `true` boolean value - attributes: attributes.reduce( - (attributeMap, form) => { - form.fields.forEach( - field => attributeMap[field.name] = true); - return attributeMap - }, {}), - - // Translate the protocol definitions into a map of protocol - // name to map of field name to `true` boolean value - protocolParameters: _.mapValues( - protocols, protocol => protocol.connectionForms.reduce( - (protocolFieldMap, form) => { - form.fields.forEach( - field => protocolFieldMap[field.name] = true); - return protocolFieldMap; - }, {})) - }; - }); - } - - /** - * Split a raw user-provided, semicolon-seperated list of identifiers into - * an array of identifiers. If identifiers contain semicolons, they can be - * escaped with backslashes, and backslashes can also be escaped using other - * backslashes. - * - * @param {String} rawIdentifiers - * The raw string value as fetched from the CSV. - * - * @returns {Array.} - * An array of identifier values. - */ - function splitIdentifiers(rawIdentifiers) { - - // Keep track of whether a backslash was seen - let escaped = false; - - return _.reduce(rawIdentifiers, (identifiers, ch) => { - - // The current identifier will be the last one in the final list - let identifier = identifiers[identifiers.length - 1]; - - // If a semicolon is seen, set the "escaped" flag and continue - // to the next character - if (!escaped && ch == '\\') { - escaped = true; - return identifiers; - } - - // End the current identifier and start a new one if there's an - // unescaped semicolon - else if (!escaped && ch == ';') { - identifiers.push(''); - return identifiers; - } - - // In all other cases, just append to the identifier - else { - identifier += ch; - escaped = false; - } - - // Save the updated identifier to the list - identifiers[identifiers.length - 1] = identifier; - - return identifiers; - - }, ['']) - - // Filter out any 0-length (empty) identifiers - .filter(identifier => identifier.length); - - } - - /** - * Given a CSV header row, create and return a promise that will resolve to - * a function that can take a CSV data row and return a ImportConnection - * object. If an error occurs while parsing a particular row, the resolved - * function will throw a ParseError describing the failure. - * - * The provided CSV must contain columns for name and protocol. Optionally, - * the parentIdentifier of the target parent connection group, or a connection - * name path e.g. "ROOT/parent/child" may be included. Additionallty, - * connection parameters or attributes can be included. - * - * The names of connection attributes and parameters are not guaranteed to - * be mutually exclusive, so the CSV import format supports a distinguishing - * suffix. A column may be explicitly declared to be a parameter using a - * " (parameter)" suffix, or an attribute using an " (attribute)" suffix. - * No suffix is required if the name is unique across connections and - * attributes. - * - * If a parameter or attribute name conflicts with the standard - * "name", "protocol", "group", or "parentIdentifier" fields, the suffix is - * required. - * - * If a failure occurs while attempting to create the transformer function, - * the promise will be rejected with a ParseError describing the failure. - * - * @returns {Promise.>} - * A promise that will resolve to a function that translates a CSV data - * row (array of strings) to a ImportConnection object. - */ - service.getCSVTransformer = function getCSVTransformer(headerRow) { - - // A promise that will be resolved with the transformer or rejected if - // an error occurs - const deferred = $q.defer(); - - getFieldLookups().then(({attributes, protocolParameters}) => { - - // All configuration required to generate a function that can - // transform a row of CSV into a connection object. - // NOTE: This is a single object instead of a collection of variables - // to ensure that no stale references are used - e.g. when one getter - // invokes another getter - const transformConfig = { - - // Callbacks for required fields - nameGetter: undefined, - protocolGetter: undefined, - - // Callbacks for a parent group ID or group path - groupGetter: undefined, - parentIdentifierGetter: undefined, - - // Callbacks for user and user group identifiers - usersGetter: () => [], - userGroupsGetter: () => [], - - // Callbacks that will generate either connection attributes or - // parameters. These callbacks will return a {type, name, value} - // object containing the type ("parameter" or "attribute"), - // the name of the attribute or parameter, and the corresponding - // value. - parameterOrAttributeGetters: [] - - }; - - // A set of all headers that have been seen so far. If any of these - // are duplicated, the CSV is invalid. - const headerSet = {}; - - // Iterate through the headers one by one - headerRow.forEach((rawHeader, index) => { - - // Trim to normalize all headers - const header = rawHeader.trim(); - - // Check if the header is duplicated - if (headerSet[header]) { - deferred.reject(new ParseError({ - message: 'Duplicate CSV Header: ' + header, - translatableMessage: new TranslatableMessage({ - key: 'IMPORT.ERROR_DUPLICATE_CSV_HEADER', - variables: { HEADER: header } - }) - })); - return; - } - - // Mark that this particular header has already been seen - headerSet[header] = true; - - // A callback that returns the field at the current index - const fetchFieldAtIndex = row => row[index]; - - // A callback that splits raw string identifier lists by - // semicolon characters into an array of identifiers - const identifierListCallback = row => - splitIdentifiers(fetchFieldAtIndex(row)); - - // Set up the name callback - if (header == 'name') - transformConfig.nameGetter = fetchFieldAtIndex; - - // Set up the protocol callback - else if (header == 'protocol') - transformConfig.protocolGetter = fetchFieldAtIndex; - - // Set up the group callback - else if (header == 'group') - transformConfig.groupGetter = fetchFieldAtIndex; - - // Set up the group parent ID callback - else if (header == 'parentIdentifier') - transformConfig.parentIdentifierGetter = fetchFieldAtIndex; - - // Set the user identifiers callback - else if (header == 'users') - transformConfig.usersGetter = ( - identifierListCallback); - - // Set the user group identifiers callback - else if (header == 'groups') - transformConfig.userGroupsGetter = ( - identifierListCallback); - - // At this point, any other header might refer to a connection - // parameter or to an attribute - - // A field may be explicitly specified as a parameter - else if (header.endsWith(PARAMETER_SUFFIX)) { - - // Push as an explicit parameter getter - const parameterName = header.replace(PARAMETER_SUFFIX, ''); - transformConfig.parameterOrAttributeGetters.push( - row => ({ - type: 'parameters', - name: parameterName, - value: fetchFieldAtIndex(row) - }) - ); - } - - // A field may be explicitly specified as a parameter - else if (header.endsWith(ATTRIBUTE_SUFFIX)) { - - // Push as an explicit attribute getter - const attributeName = header.replace(ATTRIBUTE_SUFFIX, ''); - transformConfig.parameterOrAttributeGetters.push( - row => ({ - type: 'attributes', - name: attributeName, - value: fetchFieldAtIndex(row) - }) - ); - } - - // The field is ambiguous, either an attribute or parameter, - // so the getter will have to determine this for every row - else - transformConfig.parameterOrAttributeGetters.push(row => { - - // The name is just the value of the current header - const name = header; - - // The value is at the index that matches the position - // of the header - const value = fetchFieldAtIndex(row); - - // If no value is provided, do not check the validity - // of the parameter/attribute. Doing so would prevent - // the import of a list of mixed protocol types, where - // fields are only populated for protocols for which - // they are valid parameters. If a value IS provided, - // it must be a valid parameter or attribute for the - // current protocol, which will be checked below. - if (!value) - return {}; - - // The protocol may determine whether a field is - // a parameter or an attribute (or both) - const protocol = transformConfig.protocolGetter(row); - - // Any errors encountered while processing this row - const errors = []; - - // Before checking whether it's an attribute or protocol, - // make sure this is a valid protocol to start - if (!protocolParameters[protocol]) - - // If the protocol is invalid, do not throw an error - // here - this will be handled further downstream - // by non-CSV-specific error handling - return {}; - - // Determine if the field refers to an attribute or a - // parameter (or both, which is an error) - const isAttribute = !!attributes[name]; - const isParameter = !!_.get( - protocolParameters, [protocol, name]); - - // If there is both an attribute and a protocol-specific - // parameter with the provided name, it's impossible to - // figure out which this should be - if (isAttribute && isParameter) - errors.push(new ParseError({ - message: 'Ambiguous CSV Header: ' + header, - key: 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', - variables: { HEADER: header } - })); - - // It's neither an attribute or a parameter - else if (!isAttribute && !isParameter) - errors.push(new ParseError({ - message: 'Invalid CSV Header: ' + header, - key: 'IMPORT.ERROR_INVALID_CSV_HEADER', - variables: { HEADER: header } - })); - - // Choose the appropriate type - const type = isAttribute ? 'attributes' : 'parameters'; - - return { type, name, value, errors }; - }); - }); - - const { - nameGetter, protocolGetter, - parentIdentifierGetter, groupGetter, - usersGetter, userGroupsGetter, - parameterOrAttributeGetters - } = transformConfig; - - // Fail if the name wasn't provided. Note that this is a file-level - // error, not specific to any connection. - if (!nameGetter) - deferred.reject(new ParseError({ - message: 'The connection name must be provided', - key: 'IMPORT.ERROR_REQUIRED_NAME_FILE' - })); - - // Fail if the protocol wasn't provided - if (!protocolGetter) - deferred.reject(new ParseError({ - message: 'The connection protocol must be provided', - key: 'IMPORT.ERROR_REQUIRED_PROTOCOL_FILE' - })); - - // The function to transform a CSV row into a connection object - deferred.resolve(function transformCSVRow(row) { - - // Get name and protocol - const name = nameGetter(row); - const protocol = protocolGetter(row); - - // Get any users or user groups who should be granted access - const users = usersGetter(row); - const groups = userGroupsGetter(row); - - // Get the parent group ID and/or group path - const group = groupGetter && groupGetter(row); - const parentIdentifier = ( - parentIdentifierGetter && parentIdentifierGetter(row)); - - return new ImportConnection({ - - // Fields that are not protocol-specific - name, - protocol, - parentIdentifier, - group, - users, - groups, - - // Fields that might potentially be either attributes or - // parameters, depending on the protocol - ...parameterOrAttributeGetters.reduce((values, getter) => { - - // Determine the type, name, and value - const { type, name, value, errors } = getter(row); - - // Set the value if available - if (type && name && value) - values[type][name] = value; - - // If there were errors - if (errors && errors.length) - values.errors = [...values.errors, ...errors]; - - // Continue on to the next attribute or parameter - return values; - - }, {parameters: {}, attributes: {}, errors: []}) - - }); - - }); - - }); - - return deferred.promise; - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html b/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html deleted file mode 100644 index f2d486cb08..0000000000 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionErrors.html +++ /dev/null @@ -1,49 +0,0 @@ -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - {{'IMPORT.TABLE_HEADER_ROW_NUMBER' | translate}} - - {{'IMPORT.TABLE_HEADER_NAME' | translate}} - - {{'IMPORT.TABLE_HEADER_GROUP' | translate}} - - {{'IMPORT.TABLE_HEADER_PROTOCOL' | translate}} - - {{'IMPORT.TABLE_HEADER_ERRORS' | translate}} -
    {{error.rowNumber}}{{error.name}}{{error.group}}{{error.protocol}} -
      -
    • - {{ message }} -
    • -
    -
    - - - -
    diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html deleted file mode 100644 index a6cc0fe034..0000000000 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImport.html +++ /dev/null @@ -1,82 +0,0 @@ -
    - -
    -

    {{'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | translate}}

    - -
    - -
    -
    {{fileName}}
    - -
    - -
    - -
    - {{'IMPORT.HELP_UPLOAD_FILE_TYPES' | translate}} - {{'IMPORT.ACTION_VIEW_FORMAT_HELP' | translate}} - -
    - -
    - -
    {{'IMPORT.HELP_UPLOAD_DROP_TITLE' | translate}}
    - - - - {{'IMPORT.ACTION_BROWSE' | translate}} - - -
    {{fileName}}
    - -
    - -
      -
    • - - - -
    • -
    • - - - -
    • -
    - -
    - -
    - - -
    - -
    - - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html b/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html deleted file mode 100644 index d3abda1db8..0000000000 --- a/guacamole/src/main/frontend/src/app/import/templates/connectionImportFileHelp.html +++ /dev/null @@ -1,29 +0,0 @@ -
    - -
    -

    {{'IMPORT.SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE' | translate}}

    - -
    - -

    {{'IMPORT.HELP_FILE_TYPE_HEADER' | translate}}

    -

    {{'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | translate}}

    - -

    {{'IMPORT.SECTION_HEADER_CSV' | translate}}

    -

    {{'IMPORT.HELP_CSV_DESCRIPTION' | translate}}

    -

    {{'IMPORT.HELP_CSV_MORE_DETAILS' | translate}}

    -
    {{'IMPORT.HELP_CSV_EXAMPLE' | translate }}
    - -

    {{'IMPORT.SECTION_HEADER_JSON' | translate}}

    -

    {{'IMPORT.HELP_JSON_DESCRIPTION' | translate}}

    -

    {{'IMPORT.HELP_JSON_MORE_DETAILS' | translate}}

    -
    {{'IMPORT.HELP_JSON_EXAMPLE' | translate }}
    - -

    {{'IMPORT.SECTION_HEADER_YAML' | translate}}

    -

    {{'IMPORT.HELP_YAML_DESCRIPTION' | translate}}

    -
    {{'IMPORT.HELP_YAML_EXAMPLE' | translate}}
    - -
      -
    1. {{'IMPORT.HELP_SEMICOLON_FOOTNOTE' | translate}}
    2. -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js deleted file mode 100644 index 47c13a8ebe..0000000000 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnection.js +++ /dev/null @@ -1,147 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ImportConnection class. - */ -angular.module('import').factory('ImportConnection', ['$injector', - function defineImportConnection($injector) { - - /** - * A representation of a connection to be imported, as parsed from an - * user-supplied import file. - * - * @constructor - * @param {ImportConnection|Object} [template={}] - * The object whose properties should be copied within the new - * Connection. - */ - const ImportConnection = function ImportConnection(template) { - - // Use empty object by default - template = template || {}; - - /** - * The unique identifier of the connection group that contains this - * connection. - * - * @type String - */ - this.parentIdentifier = template.parentIdentifier; - - /** - * The path to the connection group that contains this connection, - * written as e.g. "ROOT/parent/child/group". - * - * @type String - */ - this.group = template.group; - - /** - * The identifier of the connection being updated. Only meaningful if - * the replace operation is set. - */ - this.identifier = template.identifier; - - /** - * The human-readable name of this connection, which is not necessarily - * unique. - * - * @type String - */ - this.name = template.name; - - /** - * The name of the protocol associated with this connection, such as - * "vnc" or "rdp". - * - * @type String - */ - this.protocol = template.protocol; - - /** - * Connection configuration parameters, as dictated by the protocol in - * use, arranged as name/value pairs. - * - * @type Object. - */ - this.parameters = template.parameters || {}; - - /** - * Arbitrary name/value pairs which further describe this connection. - * The semantics and validity of these attributes are dictated by the - * extension which defines them. - * - * @type Object. - */ - this.attributes = template.attributes || {}; - - /** - * The identifiers of all users who should be granted read access to - * this connection. - * - * @type String[] - */ - this.users = template.users || []; - - /** - * The identifiers of all user groups who should be granted read access - * to this connection. - * - * @type String[] - */ - this.groups = template.groups || []; - - /** - * The mode import mode for this connection. If not otherwise specified, - * a brand new connection should be created. - */ - this.importMode = template.importMode || ImportConnection.ImportMode.CREATE; - - /** - * Any errors specific to this connection encountered while parsing. - * - * @type ParseError[] - */ - this.errors = template.errors || []; - - }; - - /** - * The possible import modes for a given connection. - */ - ImportConnection.ImportMode = { - - /** - * The connection should be created fresh. This mode is valid IFF there - * is no existing connection with the same name and parent group. - */ - CREATE : "CREATE", - - /** - * This connection will replace the existing connection with the same - * name and parent group. - */ - REPLACE : "REPLACE" - - }; - - return ImportConnection; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js b/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js deleted file mode 100644 index 9fabe23136..0000000000 --- a/guacamole/src/main/frontend/src/app/import/types/ImportConnectionError.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ImportConnectionError class. - */ -angular.module('import').factory('ImportConnectionError', ['$injector', - function defineImportConnectionError($injector) { - - // Required types - const DisplayErrorList = $injector.get('DisplayErrorList'); - - /** - * A representation of the errors associated with a connection to be - * imported, along with some basic information connection information to - * identify the connection having the error, as returned from a parsed - * user-supplied import file. - * - * @constructor - * @param {ImportConnectionError|Object} [template={}] - * The object whose properties should be copied within the new - * ImportConnectionError. - */ - const ImportConnectionError = function ImportConnectionError(template) { - - // Use empty object by default - template = template || {}; - - /** - * The row number within the original connection import file for this - * connection. This should be 1-indexed. - */ - this.rowNumber = template.rowNumber; - - /** - * The human-readable name of this connection, which is not necessarily - * unique. - * - * @type String - */ - this.name = template.name; - - /** - * The human-readable connection group path for this connection, of the - * form "ROOT/Parent/Child". - * - * @type String - */ - this.group = template.group; - - /** - * The name of the protocol associated with this connection, such as - * "vnc" or "rdp". - * - * @type String - */ - this.protocol = template.protocol; - - /** - * The error messages associated with this particular connection, if any. - * - * @type ImportConnectionError - */ - this.errors = template.errors || new DisplayErrorList(); - - }; - - return ImportConnectionError; - -}]); diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js b/guacamole/src/main/frontend/src/app/import/types/ParseResult.js deleted file mode 100644 index 3d15511cc6..0000000000 --- a/guacamole/src/main/frontend/src/app/import/types/ParseResult.js +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ParseResult class. - */ -angular.module('import').factory('ParseResult', [function defineParseResult() { - - /** - * The result of parsing a connection import file - containing a list of - * API patches ready to be submitted to the PATCH REST API for batch - * connection creation/replacement, a set of users and user groups to grant - * access to each connection, a group path for every connection, and any - * errors that may have occurred while parsing each connection. - * - * @constructor - * @param {ParseResult|Object} [template={}] - * The object whose properties should be copied within the new - * ParseResult. - */ - const ParseResult = function ParseResult(template) { - - // Use empty object by default - template = template || {}; - - /** - * An array of patches, ready to be submitted to the PATCH REST API for - * batch connection creation / replacement. Note that this array may - * contain more patches than connections from the original file - in the - * case that connections are being fully replaced, there will be a - * remove and a create patch for each replaced connection. - * - * @type {DirectoryPatch[]} - */ - this.patches = template.patches || []; - - /** - * An object whose keys are the user identifiers of users specified - * in the batch import, and whose values are an array of indices of - * connections within the patches array to which those users should be - * granted access. - * - * @type {Object.} - */ - this.users = template.users || {}; - - /** - * An object whose keys are the user group identifiers of every user - * group specified in the batch import. i.e. a set of all user group - * identifiers. - * - * @type {Object.} - */ - this.groups = template.users || {}; - - /** - * A map of connection index within the patch array, to connection group - * path for that connection, of the form "ROOT/Parent/Child". - * - * @type {Object.} - */ - this.groupPaths = template.groupPaths || {}; - - /** - * An array of errors encountered while parsing the corresponding - * connection (at the same array index in the patches array). Each - * connection should have an array of errors. If empty, no errors - * occurred for this connection. - * - * @type {ParseError[][]} - */ - this.errors = template.errors || []; - - /** - * True if any errors were encountered while parsing the connections - * represented by this ParseResult. This should always be true if there - * are a non-zero number of elements in the errors list for any - * connection, or false otherwise. - * - * @type {Boolean} - */ - this.hasErrors = template.hasErrors || false; - - /** - * The integer number of unique connections present in the parse result. - * This may be less than the length of the patches array, if any REMOVE - * patches are present. - * - * @Type {Number} - */ - this.connectionCount = template.connectionCount || 0; - - }; - - return ParseResult; - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js deleted file mode 100644 index d24df9179c..0000000000 --- a/guacamole/src/main/frontend/src/app/index/config/indexRouteConfig.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The config block for setting up all the url routing. - */ -angular.module('index').config(['$routeProvider', '$locationProvider', - function indexRouteConfig($routeProvider, $locationProvider) { - - // Disable HTML5 mode (use # for routing) - $locationProvider.html5Mode(false); - - // Clear hash prefix to keep /#/thing/bat URL style - $locationProvider.hashPrefix(''); - - /** - * Attempts to re-authenticate with the Guacamole server, sending any - * query parameters in the URL, along with the current auth token, and - * updating locally stored token if necessary. - * - * @param {Service} $injector - * The Angular $injector service. - * - * @returns {Promise} - * A promise which resolves successfully only after an attempt to - * re-authenticate has been made. If the authentication attempt fails, - * the promise will be rejected. - */ - var updateCurrentToken = ['$injector', function updateCurrentToken($injector) { - - // Required services - var $location = $injector.get('$location'); - var authenticationService = $injector.get('authenticationService'); - - // Re-authenticate including any parameters in URL - return authenticationService.updateCurrentToken($location.search()); - - }]; - - /** - * Redirects the user to their home page. This necessarily requires - * attempting to re-authenticate with the Guacamole server, as the user's - * credentials may have changed, and thus their most-appropriate home page - * may have changed as well. - * - * @param {Service} $injector - * The Angular $injector service. - * - * @returns {Promise} - * A promise which resolves successfully only after an attempt to - * re-authenticate and determine the user's proper home page has been - * made. - */ - var routeToUserHomePage = ['$injector', function routeToUserHomePage($injector) { - - // Required services - var $location = $injector.get('$location'); - var $q = $injector.get('$q'); - var userPageService = $injector.get('userPageService'); - - // Promise for routing attempt - var route = $q.defer(); - - // Re-authenticate including any parameters in URL - $injector.invoke(updateCurrentToken) - .then(function tokenUpdateComplete() { - - // Redirect to home page - userPageService.getHomePage() - .then(function homePageRetrieved(homePage) { - - // If home page is the requested location, allow through - if ($location.path() === homePage.url) - route.resolve(); - - // Otherwise, reject and reroute - else { - $location.path(homePage.url); - route.reject(); - } - - }) - - // If retrieval of home page fails, assume requested page is OK - ['catch'](function homePageFailed() { - route.resolve(); - }); - - }) - - ['catch'](function tokenUpdateFailed() { - route.reject(); - }); - - // Return promise that will resolve only if the requested page is the - // home page - return route.promise; - - }]; - - // Configure each possible route - $routeProvider - - // Home screen - .when('/', { - title : 'APP.NAME', - bodyClassName : 'home', - templateUrl : 'app/home/templates/home.html', - controller : 'homeController', - resolve : { routeToUserHomePage: routeToUserHomePage } - }) - - // Connection import page - .when('/import/:dataSource/connection', { - title : 'APP.NAME', - bodyClassName : 'settings', - templateUrl : 'app/import/templates/connectionImport.html', - controller : 'importConnectionsController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Connection import file format help page - .when('/import/connection/file-format-help', { - title : 'APP.NAME', - bodyClassName : 'settings', - templateUrl : 'app/import/templates/connectionImportFileHelp.html', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Management screen - .when('/settings/:dataSource?/:tab', { - title : 'APP.NAME', - bodyClassName : 'settings', - templateUrl : 'app/settings/templates/settings.html', - controller : 'settingsController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Connection editor - .when('/manage/:dataSource/connections/:id*?', { - title : 'APP.NAME', - bodyClassName : 'manage', - templateUrl : 'app/manage/templates/manageConnection.html', - controller : 'manageConnectionController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Sharing profile editor - .when('/manage/:dataSource/sharingProfiles/:id*?', { - title : 'APP.NAME', - bodyClassName : 'manage', - templateUrl : 'app/manage/templates/manageSharingProfile.html', - controller : 'manageSharingProfileController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Connection group editor - .when('/manage/:dataSource/connectionGroups/:id*?', { - title : 'APP.NAME', - bodyClassName : 'manage', - templateUrl : 'app/manage/templates/manageConnectionGroup.html', - controller : 'manageConnectionGroupController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // User editor - .when('/manage/:dataSource/users/:id*?', { - title : 'APP.NAME', - bodyClassName : 'manage', - templateUrl : 'app/manage/templates/manageUser.html', - controller : 'manageUserController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // User group editor - .when('/manage/:dataSource/userGroups/:id*?', { - title : 'APP.NAME', - bodyClassName : 'manage', - templateUrl : 'app/manage/templates/manageUserGroup.html', - controller : 'manageUserGroupController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Recording player - .when('/settings/:dataSource/recording/:identifier/:name', { - title : 'APP.NAME', - bodyClassName : 'settings', - templateUrl : 'app/settings/templates/settingsConnectionHistoryPlayer.html', - controller : 'connectionHistoryPlayerController', - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Client view - .when('/client/:id', { - bodyClassName : 'client', - templateUrl : 'app/client/templates/client.html', - controller : 'clientController', - reloadOnUrl : false, - resolve : { updateCurrentToken: updateCurrentToken } - }) - - // Redirect to home screen if page not found - .otherwise({ - resolve : { routeToUserHomePage: routeToUserHomePage } - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/config/indexTranslationConfig.js b/guacamole/src/main/frontend/src/app/index/config/indexTranslationConfig.js deleted file mode 100644 index 3e94203613..0000000000 --- a/guacamole/src/main/frontend/src/app/index/config/indexTranslationConfig.js +++ /dev/null @@ -1,44 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The configuration block for setting up everything having to do with i18n. - */ -angular.module('index').config(['$injector', function($injector) { - - // Required providers - var $translateProvider = $injector.get('$translateProvider'); - var preferenceServiceProvider = $injector.get('preferenceServiceProvider'); - - // Fallback to US English - $translateProvider.fallbackLanguage('en'); - - // Prefer chosen language - $translateProvider.preferredLanguage(preferenceServiceProvider.preferences.language); - - // Escape any HTML in translation strings - $translateProvider.useSanitizeValueStrategy('escape'); - - // Load translations via translationLoader service - $translateProvider.useLoader('translationLoader'); - - // Provide pluralization, etc. via messageformat.js - $translateProvider.useMessageFormatInterpolation(); - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/config/templateRequestDecorator.js b/guacamole/src/main/frontend/src/app/index/config/templateRequestDecorator.js deleted file mode 100644 index 2f832de874..0000000000 --- a/guacamole/src/main/frontend/src/app/index/config/templateRequestDecorator.js +++ /dev/null @@ -1,364 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Overrides $templateRequest such that HTML patches defined within Guacamole - * extensions are automatically applied to template contents. As the results of - * $templateRequest are cached internally, $templateCache is also overridden to - * update the internal cache as necessary. - */ -angular.module('index').config(['$provide', function($provide) { - - /** - * A map of previously-returned promises from past calls to - * $templateRequest(). Future calls to $templateRequest() will return - * new promises chained to the first promise returned for a given URL, - * rather than redo patch processing for every request. - * - * @type Object.> - */ - var promiseCache = {}; - - // Decorate $templateCache such that promiseCache is updated if a template - // is modified within $templateCache at runtime - $provide.decorator('$templateCache', ['$delegate', - function decorateTemplateCache($delegate) { - - // Create shallow copy of original $templateCache which we can safely - // override - var decoratedTemplateCache = angular.extend({}, $delegate); - - /** - * Overridden version of $templateCache.put() which automatically - * invalidates the cached $templateRequest result when used. Only the - * URL parameter is defined, as all other arguments, if any, will be - * passed through to the original $templateCache.put() when the actual - * put() operation is performed. - * - * @param {String} url - * The URL of the template whose entry is being updated. - * - * @return {Object} - * The value returned by $templateCache.put(), which is defined as - * being the value that was just stored at the provided URL. - */ - decoratedTemplateCache.put = function put(url) { - - // Evict old cached $templateRequest() result - delete promiseCache[url]; - - // Continue with originally-requested put() operation - return $delegate.put.apply(this, arguments); - - }; - - return decoratedTemplateCache; - - }]); - - // Decorate $templateRequest such that it automatically applies any HTML - // patches associated with installed Guacamole extensions - $provide.decorator('$templateRequest', ['$delegate', '$injector', - function decorateTemplateRequest($delegate, $injector) { - - // Required services - var $q = $injector.get('$q'); - var patchService = $injector.get('patchService'); - - /** - * Represents a single HTML patching operation which will be applied - * to the raw HTML of a template. The name of the patching operation - * MUST be one of the valid names defined within - * PatchOperation.Operations. - * - * @constructor - * @param {String} name - * The name of the patching operation that will be applied. Valid - * names are defined within PatchOperation.Operations. - * - * @param {String} selector - * The CSS selector which determines which elements within a - * template will be affected by the patch operation. - */ - var PatchOperation = function PatchOperation(name, selector) { - - /** - * Applies this patch operation to the template defined by the - * given root element, which must be a single element wrapped by - * JQuery. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template to which - * this patch operation should be applied. - * - * @param {Element[]} elements - * The elements which should be applied by the patch - * operation. For example, if the patch operation is inserting - * elements, these are the elements that will be inserted. - */ - this.apply = function apply(root, elements) { - PatchOperation.Operations[name](root, selector, elements); - }; - - }; - - /** - * Mapping of all valid patch operation names to their corresponding - * implementations. Each implementation accepts the same three - * parameters: the root element of the template being patched, the CSS - * selector determining which elements within the template are patched, - * and an array of elements which make up the body of the patch. - * - * @type Object. - */ - PatchOperation.Operations = { - - /** - * Inserts the given elements before the elements matched by the - * provided CSS selector. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'before' : function before(root, selector, elements) { - root.find(selector).before(elements); - }, - - /** - * Inserts the given elements after the elements matched by the - * provided CSS selector. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'after' : function after(root, selector, elements) { - root.find(selector).after(elements); - }, - - /** - * Replaces the elements matched by the provided CSS selector with - * the given elements. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'replace' : function replace(root, selector, elements) { - root.find(selector).replaceWith(elements); - }, - - /** - * Inserts the given elements within the elements matched by the - * provided CSS selector, before any existing children. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'before-children' : function beforeChildren(root, selector, elements) { - root.find(selector).prepend(elements); - }, - - /** - * Inserts the given elements within the elements matched by the - * provided CSS selector, after any existing children. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'after-children' : function afterChildren(root, selector, elements) { - root.find(selector).append(elements); - }, - - /** - * Inserts the given elements within the elements matched by the - * provided CSS selector, replacing any existing children. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String} selector - * The CSS selector which determines where this patch operation - * should be applied within the template defined by root. - * - * @param {Element[]} elements - * The contents of the patch which should be applied to the - * template defined by root at the locations selected by the - * given CSS selector. - */ - 'replace-children' : function replaceChildren(root, selector, elements) { - root.find(selector).empty().append(elements); - } - - }; - - /** - * Applies each of the given HTML patches to the given template. - * - * @param {Element[]} root - * The JQuery-wrapped root element of the template being - * patched. - * - * @param {String[]} patches - * An array of all HTML patches to be applied to the given - * template. - */ - var applyPatches = function applyPatches(root, patches) { - - // Apply all defined patches - angular.forEach(patches, function applyPatch(patch) { - - var elements = $(patch); - - // Filter out and parse all applicable meta tags - var operations = []; - elements = elements.filter(function filterMetaTags(index, element) { - - // Leave non-meta tags untouched - if (element.tagName !== 'META') - return true; - - // Only meta tags having a valid "name" attribute need - // to be filtered - var name = element.getAttribute('name'); - if (!name || !(name in PatchOperation.Operations)) - return true; - - // The "content" attribute must be present for any - // valid "name" meta tag - var content = element.getAttribute('content'); - if (!content) - return true; - - // Filter out and parse meta tag - operations.push(new PatchOperation(name, content)); - return false; - - }); - - // Apply each operation implied by the meta tags - angular.forEach(operations, function applyOperation(operation) { - operation.apply(root, elements); - }); - - }); - - }; - - /** - * Invokes $templateRequest() with all arguments exactly as provided, - * applying all HTML patches from any installed Guacamole extensions - * to the HTML of the requested template. - * - * @param {String} url - * The URL of the template being requested. - * - * @returns {Promise.} - * A Promise which resolves with the patched HTML contents of the - * requested template if retrieval of the template is successful. - */ - var decoratedTemplateRequest = function decoratedTemplateRequest(url) { - - var deferred = $q.defer(); - - // Chain to cached promise if it already exists - var cachedPromise = promiseCache[url]; - if (cachedPromise) { - cachedPromise.then(deferred.resolve, deferred.reject); - return deferred.promise; - } - - // Resolve promise with patched template HTML - $delegate.apply(this, arguments).then(function patchTemplate(data) { - - // Retrieve and apply all patches - patchService.getPatches().then(function applyRetrievedPatches(patches) { - - // Parse HTML into DOM tree - var root = $('
    ').html(data); - - // Apply all HTML patches to the parsed DOM - applyPatches(root, patches); - - // Transform back into HTML - deferred.resolve.call(this, root.html()); - - }, deferred.reject); - - }, deferred.reject); - - // Cache this promise for future results - promiseCache[url] = deferred.promise; - return deferred.promise; - - }; - - return decoratedTemplateRequest; - - }]); -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js b/guacamole/src/main/frontend/src/app/index/controllers/indexController.js deleted file mode 100644 index 2f472a6e24..0000000000 --- a/guacamole/src/main/frontend/src/app/index/controllers/indexController.js +++ /dev/null @@ -1,400 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for the root of the application. - */ -angular.module('index').controller('indexController', ['$scope', '$injector', - function indexController($scope, $injector) { - - /** - * The number of milliseconds that should elapse between client-side - * session checks. This DOES NOT impact whether a session expires at all; - * such checks will always be server-side. This only affects how quickly - * the client-side view can recognize that a user's session has expired - * absent any action taken by the user. - * - * @type {!number} - */ - const SESSION_VALIDITY_RECHECK_INTERVAL = 15000; - - // Required types - const Error = $injector.get('Error'); - const ManagedClientState = $injector.get('ManagedClientState'); - - // Required services - const $document = $injector.get('$document'); - const $interval = $injector.get('$interval'); - const $location = $injector.get('$location'); - const $route = $injector.get('$route'); - const $window = $injector.get('$window'); - const authenticationService = $injector.get('authenticationService'); - const clipboardService = $injector.get('clipboardService'); - const guacNotification = $injector.get('guacNotification'); - const guacClientManager = $injector.get('guacClientManager'); - - /** - * The error that prevents the current page from rendering at all. If no - * such error has occurred, this will be null. - * - * @type Error - */ - $scope.fatalError = null; - - /** - * The notification service. - */ - $scope.guacNotification = guacNotification; - - /** - * All currently-active connections, grouped into their corresponding - * tiled views. - * - * @type ManagedClientGroup[] - */ - $scope.getManagedClientGroups = guacClientManager.getManagedClientGroups; - - /** - * The message to display to the user as instructions for the login - * process. - * - * @type TranslatableMessage - */ - $scope.loginHelpText = null; - - /** - * Whether the user has selected to log back in after having logged out. - * - * @type boolean - */ - $scope.reAuthenticating = false; - - /** - * The credentials that the authentication service is has already accepted, - * pending additional credentials, if any. If the user is logged in, or no - * credentials have been accepted, this will be null. If credentials have - * been accepted, this will be a map of name/value pairs corresponding to - * the parameters submitted in a previous authentication attempt. - * - * @type Object. - */ - $scope.acceptedCredentials = null; - - /** - * The credentials that the authentication service is currently expecting, - * if any. If the user is logged in, this will be null. - * - * @type Field[] - */ - $scope.expectedCredentials = null; - - /** - * Possible overall states of the client side of the web application. - * - * @enum {string} - */ - var ApplicationState = { - - /** - * A non-interactive authentication attempt failed. - */ - AUTOMATIC_LOGIN_REJECTED : 'automaticLoginRejected', - - /** - * The application has fully loaded but is awaiting credentials from - * the user before proceeding. - */ - AWAITING_CREDENTIALS : 'awaitingCredentials', - - /** - * A fatal error has occurred that will prevent the client side of the - * application from functioning properly. - */ - FATAL_ERROR : 'fatalError', - - /** - * The application has just started within the user's browser and has - * not yet settled into any specific state. - */ - LOADING : 'loading', - - /** - * The user has manually logged out. - */ - LOGGED_OUT : 'loggedOut', - - /** - * The application has fully loaded and the user has logged in - */ - READY : 'ready' - - }; - - /** - * The current overall state of the client side of the application. - * Possible values are defined by {@link ApplicationState}. - * - * @type string - */ - $scope.applicationState = ApplicationState.LOADING; - - /** - * Basic page-level information. - */ - $scope.page = { - - /** - * The title of the page. - * - * @type String - */ - title: '', - - /** - * The name of the CSS class to apply to the page body, if any. - * - * @type String - */ - bodyClassName: '' - - }; - - // Add default destination for input events - var sink = new Guacamole.InputSink(); - $document[0].body.appendChild(sink.getElement()); - - // Create event listeners at the global level - var keyboard = new Guacamole.Keyboard($document[0]); - keyboard.listenTo(sink.getElement()); - - // Broadcast keydown events - keyboard.onkeydown = function onkeydown(keysym) { - - // Do not handle key events if not logged in - if ($scope.applicationState !== ApplicationState.READY) - return true; - - // Warn of pending keydown - var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeydown', keysym, keyboard); - if (guacBeforeKeydownEvent.defaultPrevented) - return true; - - // If not prevented via guacBeforeKeydown, fire corresponding keydown event - var guacKeydownEvent = $scope.$broadcast('guacKeydown', keysym, keyboard); - return !guacKeydownEvent.defaultPrevented; - - }; - - // Broadcast keyup events - keyboard.onkeyup = function onkeyup(keysym) { - - // Do not handle key events if not logged in or if a notification is - // shown - if ($scope.applicationState !== ApplicationState.READY) - return; - - // Warn of pending keyup - var guacBeforeKeydownEvent = $scope.$broadcast('guacBeforeKeyup', keysym, keyboard); - if (guacBeforeKeydownEvent.defaultPrevented) - return; - - // If not prevented via guacBeforeKeyup, fire corresponding keydown event - $scope.$broadcast('guacKeyup', keysym, keyboard); - - }; - - // Release all keys when window loses focus - $window.onblur = function () { - keyboard.reset(); - }; - - /** - * Returns whether the current user has at least one active connection - * running within the current tab. - * - * @returns {!boolean} - * true if the current user has at least one active connection running - * in the current browser tab, false otherwise. - */ - var hasActiveTunnel = function hasActiveTunnel() { - - var clients = guacClientManager.getManagedClients(); - for (var id in clients) { - - switch (clients[id].clientState.connectionState) { - case ManagedClientState.ConnectionState.CONNECTING: - case ManagedClientState.ConnectionState.WAITING: - case ManagedClientState.ConnectionState.CONNECTED: - return true; - } - - } - - return false; - - }; - - // If we're logged in and not connected to anything, periodically check - // whether the current session is still valid. If the session has expired, - // refresh the auth state to reshow the login screen (rather than wait for - // the user to take some action and discover that they are not logged in - // after all). There is no need to do this if a connection is active as - // that connection activity will already automatically check session - // validity. - $interval(function cleanUpViewIfSessionInvalid() { - if (!!authenticationService.getCurrentToken() && !hasActiveTunnel()) { - authenticationService.getValidity().then(function validityDetermined(valid) { - if (!valid) - $scope.reAuthenticate(); - }); - } - }, SESSION_VALIDITY_RECHECK_INTERVAL); - - // Release all keys upon form submission (there may not be corresponding - // keyup events for key presses involved in submitting a form) - $document.on('submit', function formSubmitted() { - keyboard.reset(); - }); - - // Attempt to read the clipboard if it may have changed - $window.addEventListener('load', clipboardService.resyncClipboard, true); - $window.addEventListener('copy', clipboardService.resyncClipboard); - $window.addEventListener('cut', clipboardService.resyncClipboard); - $window.addEventListener('focus', function focusGained(e) { - - // Only recheck clipboard if it's the window itself that gained focus - if (e.target === $window) - clipboardService.resyncClipboard(); - - }, true); - - /** - * Sets the current overall state of the client side of the - * application to the given value. Possible values are defined by - * {@link ApplicationState}. The title and class associated with the - * current page are automatically reset to the standard values applicable - * to the application as a whole (rather than any specific page). - * - * @param {!string} state - * The state to assign, as defined by {@link ApplicationState}. - */ - const setApplicationState = function setApplicationState(state) { - $scope.applicationState = state; - $scope.page.title = 'APP.NAME'; - $scope.page.bodyClassName = ''; - }; - - /** - * Navigates the user back to the root of the application (or reloads the - * current route and controller if the user is already there), effectively - * forcing reauthentication. If the user is not logged in, this will result - * in the login screen appearing. - */ - $scope.reAuthenticate = function reAuthenticate() { - - $scope.reAuthenticating = true; - - // Clear out URL state to conveniently bring user back to home screen - // upon relogin - if ($location.path() !== '/') - $location.url('/'); - else - $route.reload(); - - }; - - // Display login screen if a whole new set of credentials is needed - $scope.$on('guacInvalidCredentials', function loginInvalid(event, parameters, error) { - - setApplicationState(ApplicationState.AWAITING_CREDENTIALS); - - $scope.loginHelpText = null; - $scope.acceptedCredentials = {}; - $scope.expectedCredentials = error.expected; - - }); - - // Prompt for remaining credentials if provided credentials were not enough - $scope.$on('guacInsufficientCredentials', function loginInsufficient(event, parameters, error) { - - setApplicationState(ApplicationState.AWAITING_CREDENTIALS); - - $scope.loginHelpText = error.translatableMessage; - $scope.acceptedCredentials = parameters; - $scope.expectedCredentials = error.expected; - - }); - - // Alert user to authentication errors that occur in the absence of an - // interactive login form - $scope.$on('guacLoginFailed', function loginFailed(event, parameters, error) { - - // All errors related to an interactive login form are handled elsewhere - if ($scope.applicationState === ApplicationState.AWAITING_CREDENTIALS - || error.type === Error.Type.INSUFFICIENT_CREDENTIALS - || error.type === Error.Type.INVALID_CREDENTIALS) - return; - - setApplicationState(ApplicationState.AUTOMATIC_LOGIN_REJECTED); - $scope.reAuthenticating = false; - $scope.fatalError = error; - - }); - - // Replace absolutely all content with an error message if the page itself - // cannot be displayed due to an error - $scope.$on('guacFatalPageError', function fatalPageError(error) { - setApplicationState(ApplicationState.FATAL_ERROR); - $scope.fatalError = error; - }); - - // Replace the overall user interface with an informational message if the - // user has manually logged out - $scope.$on('guacLogout', function loggedOut() { - $scope.applicationState = ApplicationState.LOGGED_OUT; - $scope.reAuthenticating = false; - }); - - // Ensure new pages always start with clear keyboard state - $scope.$on('$routeChangeStart', function routeChanging() { - keyboard.reset(); - }); - - // Update title and CSS class upon navigation - $scope.$on('$routeChangeSuccess', function(event, current, previous) { - - // If the current route is available - if (current.$$route) { - - // Clear login screen if route change was successful (and thus - // login was either successful or not required) - $scope.applicationState = ApplicationState.READY; - - // Set title - var title = current.$$route.title; - if (title) - $scope.page.title = title; - - // Set body CSS class - $scope.page.bodyClassName = current.$$route.bodyClassName || ''; - } - - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/filters/arrayFilter.js b/guacamole/src/main/frontend/src/app/index/filters/arrayFilter.js deleted file mode 100644 index 861295336e..0000000000 --- a/guacamole/src/main/frontend/src/app/index/filters/arrayFilter.js +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A filter for transforming an object into an array of all non-inherited - * property key/value pairs. The resulting array contains one object for each - * property in the original object, where the "key" property contains the - * original property key and the "value" property contains the original - * property value. - */ -angular.module('index').filter('toArray', [function toArrayFactory() { - - /** - * The name of the property to use to store the cached result of converting - * an object to an array. This property is added to each object converted, - * such that the same array is returned each time unless the original - * object has changed. - * - * @type String - */ - var CACHE_KEY = '_guac_toArray'; - - return function toArrayFilter(input) { - - // If no object is available, just return an empty array - if (!input) { - return []; - } - - // Translate object into array of key/value pairs - var array = []; - angular.forEach(input, function fetchValueByKey(value, key) { - array.push({ - key : key, - value : value - }); - }); - - // Sort consistently by key - array.sort(function compareKeys(a, b) { - if (a.key < b.key) return -1; - if (a.key > b.key) return 1; - return 0; - }); - - // Define non-enumerable property for holding cached array - if (!input[CACHE_KEY]) { - Object.defineProperty(input, CACHE_KEY, { - value : [], - enumerable : false, - configurable : true, - writable : true - }); - } - - // Update cache if resulting array is different - if (!angular.equals(input[CACHE_KEY], array)) - input[CACHE_KEY] = array; - - return input[CACHE_KEY]; - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js b/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js deleted file mode 100644 index a774d28fb5..0000000000 --- a/guacamole/src/main/frontend/src/app/index/filters/resolveFilter.js +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Filter which accepts a promise as input, returning the resolved value of - * that promise if/when the promise is resolved. While the promise is not - * resolved, null is returned. - */ -angular.module('index').filter('resolve', [function resolveFilter() { - - /** - * The name of the property to use to store the resolved promise value. - * - * @type {!string} - */ - const RESOLVED_VALUE_KEY = '_guac_resolveFilter_resolvedValue'; - - return function resolveFilter(promise) { - - if (!promise) - return null; - - // Assign value to RESOLVED_VALUE_KEY automatically upon resolution of - // the received promise - if (!(RESOLVED_VALUE_KEY in promise)) { - promise[RESOLVED_VALUE_KEY] = null; - promise.then((value) => { - return promise[RESOLVED_VALUE_KEY] = value; - }); - } - - // Always return cached value - return promise[RESOLVED_VALUE_KEY]; - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/index/indexModule.js b/guacamole/src/main/frontend/src/app/index/indexModule.js deleted file mode 100644 index 2bcc945274..0000000000 --- a/guacamole/src/main/frontend/src/app/index/indexModule.js +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -require('angular-module-shim.js'); -require('relocateParameters.js'); - -require('angular-translate-interpolation-messageformat'); -require('angular-translate-loader-static-files'); - -/** - * The module for the root of the application. - */ -angular.module('index', [ - - require('angular-route'), - require('angular-translate'), - - 'auth', - 'client', - 'clipboard', - 'home', - 'import', - 'login', - 'manage', - 'navigation', - 'notification', - 'rest', - 'settings', - - 'templates-main' - -]); - -// Recursively pull in all other JavaScript and CSS files as requirements (just -// like old minify-maven-plugin build) -const context = require.context('../', true, /.*\.(css|js)$/); -context.keys().forEach(key => context(key)); - diff --git a/guacamole/src/main/frontend/src/app/list/directives/guacFilter.js b/guacamole/src/main/frontend/src/app/list/directives/guacFilter.js deleted file mode 100644 index 5a284d6fba..0000000000 --- a/guacamole/src/main/frontend/src/app/list/directives/guacFilter.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides a filtering text input field which automatically - * produces a filtered subset of the elements of some given array. - */ -angular.module('list').directive('guacFilter', [function guacFilter() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The property to which a subset of the provided array will be - * assigned. - * - * @type Array - */ - filteredItems : '=', - - /** - * The placeholder text to display within the filter input field - * when no filter has been provided. - * - * @type String - */ - placeholder : '&', - - /** - * An array of objects to filter. A subset of this array will be - * exposed as filteredItems. - * - * @type Array - */ - items : '&', - - /** - * An array of expressions to filter against for each object in the - * items array. These expressions must be Angular expressions - * which resolve to properties on the objects in the items array. - * - * @type String[] - */ - properties : '&' - - }, - - templateUrl: 'app/list/templates/guacFilter.html', - controller: ['$scope', '$injector', function guacFilterController($scope, $injector) { - - // Required types - var FilterPattern = $injector.get('FilterPattern'); - - /** - * The pattern object to use when filtering items. - * - * @type FilterPattern - */ - var filterPattern = new FilterPattern($scope.properties()); - - /** - * The filter search string to use to restrict the displayed items. - * - * @type String - */ - $scope.searchString = null; - - /** - * Applies the current filter predicate, filtering all provided - * items and storing the result in filteredItems. - */ - var updateFilteredItems = function updateFilteredItems() { - - var items = $scope.items(); - if (items) - $scope.filteredItems = items.filter(filterPattern.predicate); - else - $scope.filteredItems = []; - - }; - - // Recompile and refilter when pattern is changed - $scope.$watch('searchString', function searchStringChanged(searchString) { - filterPattern.compile(searchString); - updateFilteredItems(); - }); - - // Refilter when items change - $scope.$watchCollection($scope.items, function itemsChanged() { - updateFilteredItems(); - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/list/directives/guacPager.js b/guacamole/src/main/frontend/src/app/list/directives/guacPager.js deleted file mode 100644 index 956abc3b53..0000000000 --- a/guacamole/src/main/frontend/src/app/list/directives/guacPager.js +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides pagination controls, along with a paginated - * subset of the elements of some given array. - */ -angular.module('list').directive('guacPager', [function guacPager() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The property to which a subset of the provided array will be - * assigned. - * - * @type Array - */ - page : '=', - - /** - * The maximum number of items per page. - * - * @type Number - */ - pageSize : '&', - - /** - * The maximum number of page choices to provide, regardless of the - * total number of pages. - * - * @type Number - */ - pageCount : '&', - - /** - * An array objects to paginate. Subsets of this array will be - * exposed as pages. - * - * @type Array - */ - items : '&' - - }, - - templateUrl: 'app/list/templates/guacPager.html', - controller: ['$scope', function guacPagerController($scope) { - - /** - * The default size of a page, if not provided via the pageSize - * attribute. - * - * @type Number - */ - var DEFAULT_PAGE_SIZE = 10; - - /** - * The default maximum number of page choices to provide, if a - * value is not providede via the pageCount attribute. - * - * @type Number - */ - var DEFAULT_PAGE_COUNT = 11; - - /** - * An array of arrays, where the Nth array contains the contents of - * the Nth page. - * - * @type Array[] - */ - var pages = []; - - /** - * The number of the first selectable page. - * - * @type Number; - */ - $scope.firstPage = 1; - - /** - * The number of the page immediately before the currently-selected - * page. - * - * @type Number; - */ - $scope.previousPage = 1; - - /** - * The number of the currently-selected page. - * - * @type Number; - */ - $scope.currentPage = 1; - - /** - * The number of the page immediately after the currently-selected - * page. - * - * @type Number; - */ - $scope.nextPage = 1; - - /** - * The number of the last selectable page. - * - * @type Number; - */ - $scope.lastPage = 1; - - /** - * An array of relevant page numbers that the user may want to jump - * to directly. - * - * @type Number[] - */ - $scope.pageNumbers = []; - - /** - * Updates the displayed page number choices. - */ - var updatePageNumbers = function updatePageNumbers() { - - // Get page count - var pageCount = $scope.pageCount() || DEFAULT_PAGE_COUNT; - - // Determine start/end of page window - var windowStart = $scope.currentPage - (pageCount - 1) / 2; - var windowEnd = windowStart + pageCount - 1; - - // Shift window as necessary if it extends beyond the first page - if (windowStart < $scope.firstPage) { - windowEnd = Math.min($scope.lastPage, windowEnd - windowStart + $scope.firstPage); - windowStart = $scope.firstPage; - } - - // Shift window as necessary if it extends beyond the last page - else if (windowEnd > $scope.lastPage) { - windowStart = Math.max(1, windowStart - windowEnd + $scope.lastPage); - windowEnd = $scope.lastPage; - } - - // Generate list of relevant page numbers - $scope.pageNumbers = []; - for (var pageNumber = windowStart; pageNumber <= windowEnd; pageNumber++) - $scope.pageNumbers.push(pageNumber); - - }; - - /** - * Iterates through the bound items array, splitting it into pages - * based on the current page size. - */ - var updatePages = function updatePages() { - - // Get current items and page size - var items = $scope.items(); - var pageSize = $scope.pageSize() || DEFAULT_PAGE_SIZE; - - // Clear current pages - pages = []; - - // Only split into pages if items actually exist - if (items) { - - // Split into pages of pageSize items each - for (var i = 0; i < items.length; i += pageSize) - pages.push(items.slice(i, i + pageSize)); - - } - - // Update minimum and maximum values - $scope.firstPage = 1; - $scope.lastPage = pages.length; - - // Select an appropriate page - var adjustedCurrentPage = Math.min($scope.lastPage, Math.max($scope.firstPage, $scope.currentPage)); - $scope.selectPage(adjustedCurrentPage); - - }; - - /** - * Selects the page having the given number, assigning that page to - * the property bound to the page attribute. If no such page - * exists, the property will be set to undefined instead. Valid - * page numbers begin at 1. - * - * @param {Number} page - * The number of the page to select. Valid page numbers begin - * at 1. - */ - $scope.selectPage = function selectPage(page) { - - // Select the chosen page - $scope.currentPage = page; - $scope.page = pages[page-1]; - - // Update next/previous page numbers - $scope.nextPage = Math.min($scope.lastPage, $scope.currentPage + 1); - $scope.previousPage = Math.max($scope.firstPage, $scope.currentPage - 1); - - // Update which page numbers are shown - updatePageNumbers(); - - }; - - /** - * Returns whether the given page number can be legally selected - * via selectPage(), resulting in a different page being shown. - * - * @param {Number} page - * The page number to check. - * - * @returns {Boolean} - * true if the page having the given number can be selected, - * false otherwise. - */ - $scope.canSelectPage = function canSelectPage(page) { - return page !== $scope.currentPage - && page >= $scope.firstPage - && page <= $scope.lastPage; - }; - - /** - * Returns whether the page having the given number is currently - * selected. - * - * @param {Number} page - * The page number to check. - * - * @returns {Boolean} - * true if the page having the given number is currently - * selected, false otherwise. - */ - $scope.isSelected = function isSelected(page) { - return page === $scope.currentPage; - }; - - /** - * Returns whether pages exist before the first page listed in the - * pageNumbers array. - * - * @returns {Boolean} - * true if pages exist before the first page listed in the - * pageNumbers array, false otherwise. - */ - $scope.hasMorePagesBefore = function hasMorePagesBefore() { - var firstPageNumber = $scope.pageNumbers[0]; - return firstPageNumber !== $scope.firstPage; - }; - - /** - * Returns whether pages exist after the last page listed in the - * pageNumbers array. - * - * @returns {Boolean} - * true if pages exist after the last page listed in the - * pageNumbers array, false otherwise. - */ - $scope.hasMorePagesAfter = function hasMorePagesAfter() { - var lastPageNumber = $scope.pageNumbers[$scope.pageNumbers.length - 1]; - return lastPageNumber !== $scope.lastPage; - }; - - // Update available pages when available items are changed - $scope.$watchCollection($scope.items, function itemsChanged() { - updatePages(); - }); - - // Update available pages when page size is changed - $scope.$watch($scope.pageSize, function pageSizeChanged() { - updatePages(); - }); - - // Update available page numbers when page count is changed - $scope.$watch($scope.pageCount, function pageCountChanged() { - updatePageNumbers(); - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/list/directives/guacSortOrder.js b/guacamole/src/main/frontend/src/app/list/directives/guacSortOrder.js deleted file mode 100644 index 09ed787c34..0000000000 --- a/guacamole/src/main/frontend/src/app/list/directives/guacSortOrder.js +++ /dev/null @@ -1,101 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Updates the priority of the sorting property given by "guac-sort-property" - * within the SortOrder object given by "guac-sort-order". The CSS classes - * "sort-primary" and "sort-descending" will be applied to the associated - * element depending on the priority and sort direction of the given property. - * - * The associated element will automatically be assigned the "sortable" CSS - * class. - */ -angular.module('list').directive('guacSortOrder', [function guacFocus() { - - return { - restrict: 'A', - - link: function linkGuacSortOrder($scope, $element, $attrs) { - - /** - * The object defining the sorting order. - * - * @type SortOrder - */ - var sortOrder = $scope.$eval($attrs.guacSortOrder); - - /** - * The name of the property whose priority within the sort order - * is controlled by this directive. - * - * @type String - */ - var sortProperty = $scope.$eval($attrs.guacSortProperty); - - /** - * Returns whether the sort property defined via the - * "guac-sort-property" attribute is the primary sort property of - * the associated sort order. - * - * @returns {Boolean} - * true if the sort property defined via the - * "guac-sort-property" attribute is the primary sort property, - * false otherwise. - */ - var isPrimary = function isPrimary() { - return sortOrder.primary === sortProperty; - }; - - /** - * Returns whether the primary property of the sort order is - * sorted in descending order. - * - * @returns {Boolean} - * true if the primary property of the sort order is sorted in - * descending order, false otherwise. - */ - var isDescending = function isDescending() { - return sortOrder.descending; - }; - - // Assign "sortable" class to associated element - $element.addClass('sortable'); - - // Add/remove "sort-primary" class depending on sort order - $scope.$watch(isPrimary, function primaryChanged(primary) { - $element.toggleClass('sort-primary', primary); - }); - - // Add/remove "sort-descending" class depending on sort order - $scope.$watch(isDescending, function descendingChanged(descending) { - $element.toggleClass('sort-descending', descending); - }); - - // Update sort order when clicked - $element[0].addEventListener('click', function clicked() { - $scope.$evalAsync(function updateSortOrder() { - sortOrder.togglePrimary(sortProperty); - }); - }); - - } // end guacSortOrder link function - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/list/directives/guacUserItem.js b/guacamole/src/main/frontend/src/app/list/directives/guacUserItem.js deleted file mode 100644 index 6d13cec90a..0000000000 --- a/guacamole/src/main/frontend/src/app/list/directives/guacUserItem.js +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which graphically represents an individual user. - */ -angular.module('list').directive('guacUserItem', [function guacUserItem() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The username of the user represented by this guacUserItem. - * - * @type String - */ - username : '=' - - }, - - templateUrl: 'app/list/templates/guacUserItem.html', - controller: ['$scope', '$injector', - function guacUserItemController($scope, $injector) { - - // Required types - var AuthenticationResult = $injector.get('AuthenticationResult'); - - // Required services - var $translate = $injector.get('$translate'); - - /** - * The string to display when listing the user having the provided - * username. Generally, this will be the username itself, but can - * also be an arbitrary human-readable representation of the user, - * or null if the display name is not yet determined. - * - * @type String - */ - $scope.displayName = null; - - /** - * Returns whether the username provided to this directive denotes - * a user that authenticated anonymously. - * - * @returns {Boolean} - * true if the username provided represents an anonymous user, - * false otherwise. - */ - $scope.isAnonymous = function isAnonymous() { - return $scope.username === AuthenticationResult.ANONYMOUS_USERNAME; - }; - - // Update display name whenever provided username changes - $scope.$watch('username', function updateDisplayName(username) { - - // If the user is anonymous, pull the display name for anonymous - // users from the translation service - if ($scope.isAnonymous()) { - $translate('LIST.TEXT_ANONYMOUS_USER') - .then(function retrieveAnonymousDisplayName(anonymousDisplayName) { - $scope.displayName = anonymousDisplayName; - }, angular.noop); - } - - // For all other users, use the username verbatim - else - $scope.displayName = username; - - }); - - }] // end controller - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/list/templates/guacFilter.html b/guacamole/src/main/frontend/src/app/list/templates/guacFilter.html deleted file mode 100644 index d3025be22a..0000000000 --- a/guacamole/src/main/frontend/src/app/list/templates/guacFilter.html +++ /dev/null @@ -1,6 +0,0 @@ -
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/list/templates/guacPager.html b/guacamole/src/main/frontend/src/app/list/templates/guacPager.html deleted file mode 100644 index eac2fe8533..0000000000 --- a/guacamole/src/main/frontend/src/app/list/templates/guacPager.html +++ /dev/null @@ -1,25 +0,0 @@ -
    - - -
    -
    - - -
    ...
    - - -
      -
    • {{pageNumber}}
    • -
    - - -
    ...
    - - -
    -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/list/templates/guacUserItem.html b/guacamole/src/main/frontend/src/app/list/templates/guacUserItem.html deleted file mode 100644 index e6ea670a63..0000000000 --- a/guacamole/src/main/frontend/src/app/list/templates/guacUserItem.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - {{displayName}} -
    diff --git a/guacamole/src/main/frontend/src/app/list/types/FilterPattern.js b/guacamole/src/main/frontend/src/app/list/types/FilterPattern.js deleted file mode 100644 index f67b299025..0000000000 --- a/guacamole/src/main/frontend/src/app/list/types/FilterPattern.js +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the FilterPattern class. - */ -angular.module('list').factory('FilterPattern', ['$injector', - function defineFilterPattern($injector) { - - // Required types - var FilterToken = $injector.get('FilterToken'); - var IPv4Network = $injector.get('IPv4Network'); - var IPv6Network = $injector.get('IPv6Network'); - - // Required services - var $parse = $injector.get('$parse'); - - /** - * Object which handles compilation of filtering predicates as used by - * the Angular "filter" filter. Predicates are compiled from a user- - * specified search string. - * - * @constructor - * @param {String[]} expressions - * The Angular expressions whose values are to be filtered. - */ - var FilterPattern = function FilterPattern(expressions) { - - /** - * Reference to this instance. - * - * @type FilterPattern - */ - var filterPattern = this; - - /** - * Filter predicate which simply matches everything. This function - * always returns true. - * - * @returns {Boolean} - * true. - */ - var nullPredicate = function nullPredicate() { - return true; - }; - - /** - * Array of getters corresponding to the Angular expressions provided - * to the constructor of this class. The functions returns are those - * produced by the $parse service. - * - * @type Function[] - */ - var getters = []; - - // Parse all expressions - angular.forEach(expressions, function parseExpression(expression) { - getters.push($parse(expression)); - }); - - /** - * Determines whether the given object contains properties that match - * the given string, according to the provided getters. - * - * @param {Object} object - * The object to match against. - * - * @param {String} str - * The string to match. - * - * @returns {Boolean} - * true if the object matches the given string, false otherwise. - */ - var matchesString = function matchesString(object, str) { - - // For each defined getter - for (var i=0; i < getters.length; i++) { - - // Retrieve value of current getter - var value = getters[i](object); - - // If the value matches the pattern, the whole object matches - if (String(value).toLowerCase().indexOf(str) !== -1) - return true; - - } - - // No matches found - return false; - - }; - - /** - * Determines whether the given object contains properties that match - * the given IPv4 network, according to the provided getters. - * - * @param {Object} object - * The object to match against. - * - * @param {IPv4Network} network - * The IPv4 network to match. - * - * @returns {Boolean} - * true if the object matches the given network, false otherwise. - */ - var matchesIPv4 = function matchesIPv4(object, network) { - - // For each defined getter - for (var i=0; i < getters.length; i++) { - - // Test each possible IPv4 address within the string against - // the given IPv4 network - var addresses = String(getters[i](object)).split(/[^0-9.]+/); - for (var j=0; j < addresses.length; j++) { - var value = IPv4Network.parse(addresses[j]); - if (value && network.contains(value)) - return true; - } - - } - - // No matches found - return false; - - }; - - /** - * Determines whether the given object contains properties that match - * the given IPv6 network, according to the provided getters. - * - * @param {Object} object - * The object to match against. - * - * @param {IPv6Network} network - * The IPv6 network to match. - * - * @returns {Boolean} - * true if the object matches the given network, false otherwise. - */ - var matchesIPv6 = function matchesIPv6(object, network) { - - // For each defined getter - for (var i=0; i < getters.length; i++) { - - // Test each possible IPv6 address within the string against - // the given IPv6 network - var addresses = String(getters[i](object)).split(/[^0-9A-Fa-f:]+/); - for (var j=0; j < addresses.length; j++) { - var value = IPv6Network.parse(addresses[j]); - if (value && network.contains(value)) - return true; - } - - } - - // No matches found - return false; - - }; - - - /** - * Determines whether the given object matches the given filter pattern - * token. - * - * @param {Object} object - * The object to match the token against. - * - * @param {FilterToken} token - * The token from the tokenized filter pattern to match aginst the - * given object. - * - * @returns {Boolean} - * true if the object matches the token, false otherwise. - */ - var matchesToken = function matchesToken(object, token) { - - // Match depending on token type - switch (token.type) { - - // Simple string literal - case 'LITERAL': - return matchesString(object, token.value); - - // IPv4 network address / subnet - case 'IPV4_NETWORK': - return matchesIPv4(object, token.value); - - // IPv6 network address / subnet - case 'IPV6_NETWORK': - return matchesIPv6(object, token.value); - - // Unsupported token type - default: - return false; - - } - - }; - - /** - * The current filtering predicate. - * - * @type Function - */ - this.predicate = nullPredicate; - - /** - * Compiles the given pattern string, assigning the resulting filter - * predicate. The resulting predicate will accept only objects that - * match the given pattern. - * - * @param {String} pattern - * The pattern to compile. - */ - this.compile = function compile(pattern) { - - // If no pattern provided, everything matches - if (!pattern) { - filterPattern.predicate = nullPredicate; - return; - } - - // Tokenize pattern, converting to lower case for case-insensitive matching - var tokens = FilterToken.tokenize(pattern.toLowerCase()); - - // Return predicate which matches against the value of any getter in the getters array - filterPattern.predicate = function matchesAllTokens(object) { - - // False if any token does not match - for (var i=0; i < tokens.length; i++) { - if (!matchesToken(object, tokens[i])) - return false; - } - - // True if all tokens matched - return true; - - }; - - }; - - }; - - return FilterPattern; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/list/types/IPv4Network.js b/guacamole/src/main/frontend/src/app/list/types/IPv4Network.js deleted file mode 100644 index 564eb7f0cb..0000000000 --- a/guacamole/src/main/frontend/src/app/list/types/IPv4Network.js +++ /dev/null @@ -1,126 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the IPv4Network class. - */ -angular.module('list').factory('IPv4Network', [ - function defineIPv4Network() { - - /** - * Represents an IPv4 network as a pairing of base address and netmask, - * both of which are in binary form. To obtain an IPv4Network from - * standard CIDR or dot-decimal notation, use IPv4Network.parse(). - * - * @constructor - * @param {Number} address - * The IPv4 address of the network in binary form. - * - * @param {Number} netmask - * The IPv4 netmask of the network in binary form. - */ - var IPv4Network = function IPv4Network(address, netmask) { - - /** - * Reference to this IPv4Network. - * - * @type IPv4Network - */ - var network = this; - - /** - * The binary address of this network. This will be a 32-bit quantity. - * - * @type Number - */ - this.address = address; - - /** - * The binary netmask of this network. This will be a 32-bit quantity. - * - * @type Number - */ - this.netmask = netmask; - - /** - * Tests whether the given network is entirely within this network, - * taking into account the base addresses and netmasks of both. - * - * @param {IPv4Network} other - * The network to test. - * - * @returns {Boolean} - * true if the other network is entirely within this network, false - * otherwise. - */ - this.contains = function contains(other) { - return network.address === (other.address & other.netmask & network.netmask); - }; - - }; - - /** - * Parses the given string as an IPv4 address or subnet, returning an - * IPv4Network object which describes that address or subnet. - * - * @param {String} str - * The string to parse. - * - * @returns {IPv4Network} - * The parsed network, or null if the given string is not valid. - */ - IPv4Network.parse = function parse(str) { - - // Regex which matches the general form of IPv4 addresses - var pattern = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(?:\/([0-9]{1,2}))?$/; - - // Parse IPv4 address via regex - var match = pattern.exec(str); - if (!match) - return null; - - // Parse netmask, if given - var netmask = 0xFFFFFFFF; - if (match[5]) { - var bits = parseInt(match[5]); - if (bits > 0 && bits <= 32) - netmask = 0xFFFFFFFF << (32 - bits); - } - - // Read each octet onto address - var address = 0; - for (var i=1; i <= 4; i++) { - - // Validate octet range - var octet = parseInt(match[i]); - if (octet > 255) - return null; - - // Shift on octet - address = (address << 8) | octet; - - } - - return new IPv4Network(address, netmask); - - }; - - return IPv4Network; - -}]); diff --git a/guacamole/src/main/frontend/src/app/list/types/SortOrder.js b/guacamole/src/main/frontend/src/app/list/types/SortOrder.js deleted file mode 100644 index ad3bc56dac..0000000000 --- a/guacamole/src/main/frontend/src/app/list/types/SortOrder.js +++ /dev/null @@ -1,146 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the SortOrder class. - */ -angular.module('list').factory('SortOrder', [ - function defineSortOrder() { - - /** - * Maintains a sorting predicate as required by the Angular orderBy filter. - * The order of properties sorted by the predicate can be altered while - * otherwise maintaining the sort order. - * - * @constructor - * @param {String[]} predicate - * The properties to sort by, in order of precidence. - */ - var SortOrder = function SortOrder(predicate) { - - /** - * Reference to this instance. - * - * @type SortOrder - */ - var sortOrder = this; - - /** - * The current sorting predicate. - * - * @type String[] - */ - this.predicate = predicate; - - /** - * The name of the highest-precedence sorting property. - * - * @type String - */ - this.primary = predicate[0]; - - /** - * Whether the highest-precedence sorting property is sorted in - * descending order. - * - * @type Boolean - */ - this.descending = false; - - // Handle initially-descending primary properties - if (this.primary.charAt(0) === '-') { - this.primary = this.primary.substring(1); - this.descending = true; - } - - /** - * Reorders the currently-defined predicate such that the named - * property takes precidence over all others. The property will be - * sorted in ascending order unless otherwise specified. - * - * @param {String} name - * The name of the property to reorder by. - * - * @param {Boolean} [descending=false] - * Whether the property should be sorted in descending order. By - * default, all properties are sorted in ascending order. - */ - this.reorder = function reorder(name, descending) { - - // Build ascending and descending predicate components - var ascendingName = name; - var descendingName = '-' + name; - - // Remove requested property from current predicate - sortOrder.predicate = sortOrder.predicate.filter(function notRequestedProperty(current) { - return current !== ascendingName - && current !== descendingName; - }); - - // Add property to beginning of predicate - if (descending) - sortOrder.predicate.unshift(descendingName); - else - sortOrder.predicate.unshift(ascendingName); - - // Update sorted state - sortOrder.primary = name; - sortOrder.descending = !!descending; - - }; - - /** - * Returns whether the sort order is primarily determined by the given - * property. - * - * @param {String} property - * The name of the property to check. - * - * @returns {Boolean} - * true if the sort order is primarily determined by the given - * property, false otherwise. - */ - this.isSortedBy = function isSortedBy(property) { - return sortOrder.primary === property; - }; - - /** - * Sets the primary sorting property to the given property, if not already - * set. If already set, the ascending/descending sort order is toggled. - * - * @param {String} property - * The name of the property to assign as the primary sorting property. - */ - this.togglePrimary = function togglePrimary(property) { - - // Sort in ascending order by new property, if different - if (!sortOrder.isSortedBy(property)) - sortOrder.reorder(property, false); - - // Otherwise, toggle sort order - else - sortOrder.reorder(property, !sortOrder.descending); - - }; - - }; - - return SortOrder; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/locale/localeModule.js b/guacamole/src/main/frontend/src/app/locale/localeModule.js deleted file mode 100644 index 765eb4d449..0000000000 --- a/guacamole/src/main/frontend/src/app/locale/localeModule.js +++ /dev/null @@ -1,23 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Module for handling common localization-related tasks. - */ -angular.module('locale', []); diff --git a/guacamole/src/main/frontend/src/app/locale/services/translationLoader.js b/guacamole/src/main/frontend/src/app/locale/services/translationLoader.js deleted file mode 100644 index 2be5e6a641..0000000000 --- a/guacamole/src/main/frontend/src/app/locale/services/translationLoader.js +++ /dev/null @@ -1,156 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for loading translation definition files, conforming to the - * angular-translate documentation for custom translation loaders: - * - * https://github.com/angular-translate/angular-translate/wiki/Asynchronous-loading#using-custom-loader-service - */ -angular.module('locale').factory('translationLoader', ['$injector', function translationLoader($injector) { - - // Required services - var $http = $injector.get('$http'); - var $q = $injector.get('$q'); - var cacheService = $injector.get('cacheService'); - var languageService = $injector.get('languageService'); - - /** - * Satisfies a translation request for the given key by searching for the - * translation files for each key in the given array, in order. The request - * fails only if none of the files can be found. - * - * @param {Deferred} deferred - * The Deferred object to resolve or reject depending on whether at - * least one translation file can be successfully loaded. - * - * @param {String} requestedKey - * The originally-requested language key. - * - * @param {String[]} remainingKeys - * The keys of the languages to attempt to load, in order, where the - * first key in this array is the language to try within this function - * call. The first key in the array is not necessarily the originally- - * requested language key. - */ - var satisfyTranslation = function satisfyTranslation(deferred, requestedKey, remainingKeys) { - - // Get current language key - var currentKey = remainingKeys.shift(); - - // If no languages to try, "succeed" with an empty translation (force fallback) - if (!currentKey) { - deferred.resolve('{}'); - return; - } - - /** - * Continues trying possible translation files until no possibilities - * exist. - * - * @private - */ - var tryNextTranslation = function tryNextTranslation() { - satisfyTranslation(deferred, requestedKey, remainingKeys); - }; - - // Retrieve list of supported languages - languageService.getLanguages() - - // Attempt to retrieve translation if language is supported - .then(function retrievedLanguages(languages) { - - // Skip retrieval if language is not supported - if (!(currentKey in languages)) { - tryNextTranslation(); - return; - } - - // Attempt to retrieve language - $http({ - cache : cacheService.languages, - method : 'GET', - url : 'translations/' + encodeURIComponent(currentKey) + '.json' - }) - - // Resolve promise if translation retrieved successfully - .then(function translationFileRetrieved(request) { - deferred.resolve(request.data); - }, - - // Retry with remaining languages if translation file could not be - // retrieved - tryNextTranslation); - }, - - // Retry with remaining languages if translation does not exist - tryNextTranslation); - - }; - - /** - * Given a valid language key, returns all possible legal variations of - * that key. Currently, this will be the given key and the given key - * without the country code. If the key has no country code, only the - * given key will be included in the returned array. - * - * @param {String} key - * The language key to generate variations of. - * - * @returns {String[]} - * All possible variations of the given language key. - */ - var getKeyVariations = function getKeyVariations(key) { - - var underscore = key.indexOf('_'); - - // If no underscore, only one possibility - if (underscore === -1) - return [key]; - - // Otherwise, include the lack of country code as an option - return [key, key.substr(0, underscore)]; - - }; - - /** - * Custom loader function for angular-translate which loads the desired - * language file dynamically via HTTP. If the language file cannot be - * found, the fallback language is used instead. - * - * @param {Object} options - * Arbitrary options, containing at least a "key" property which - * contains the requested language key. - * - * @returns {Promise.} - * A promise which resolves to the requested translation string object. - */ - return function loadTranslationFile(options) { - - var translation = $q.defer(); - - // Satisfy the translation request using possible variations of the given key - satisfyTranslation(translation, options.key, getKeyVariations(options.key)); - - // Return promise which is resolved only after the translation file is loaded - return translation.promise; - - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/locale/services/translationStringService.js b/guacamole/src/main/frontend/src/app/locale/services/translationStringService.js deleted file mode 100644 index 6cd09f2b16..0000000000 --- a/guacamole/src/main/frontend/src/app/locale/services/translationStringService.js +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for manipulating translation strings and translation table - * identifiers. - */ -angular.module('locale').factory('translationStringService', [function translationStringService() { - - var service = {}; - - /** - * Given an arbitrary identifier, returns the corresponding translation - * table identifier. Translation table identifiers are uppercase strings, - * word components separated by single underscores. For example, the - * string "Swap red/blue" would become "SWAP_RED_BLUE". - * - * @param {String} identifier - * The identifier to transform into a translation table identifier. - * - * @returns {String} - * The translation table identifier. - */ - service.canonicalize = function canonicalize(identifier) { - return identifier.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase(); - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/login/directives/login.js b/guacamole/src/main/frontend/src/app/login/directives/login.js deleted file mode 100644 index 60272086a3..0000000000 --- a/guacamole/src/main/frontend/src/app/login/directives/login.js +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for displaying an arbitrary login form. - */ -angular.module('login').directive('guacLogin', [function guacLogin() { - - // Login directive - var directive = { - restrict : 'E', - replace : true, - templateUrl : 'app/login/templates/login.html' - }; - - // Login directive scope - directive.scope = { - - /** - * An optional instructional message to display within the login - * dialog. - * - * @type TranslatableMessage - */ - helpText : '=', - - /** - * The login form or set of fields. This will be displayed to the user - * to capture their credentials. - * - * @type Field[] - */ - form : '=', - - /** - * A map of all field name/value pairs that have already been provided. - * If not null, the user will be prompted to continue their login - * attempt using only the fields which remain. - */ - values : '=' - - }; - - // Controller for login directive - directive.controller = ['$scope', '$injector', - function loginController($scope, $injector) { - - // Required types - var Error = $injector.get('Error'); - var Field = $injector.get('Field'); - - // Required services - var $location = $injector.get('$location'); - var $rootScope = $injector.get('$rootScope'); - var $route = $injector.get('$route'); - var authenticationService = $injector.get('authenticationService'); - var requestService = $injector.get('requestService'); - - /** - * The initial value for all login fields. Note that this value must - * not be null. If null, empty fields may not be submitted back to the - * server at all, causing the request to misrepresent true login state. - * - * For example, if a user receives an insufficient credentials error - * due to their password expiring, failing to provide that new password - * should result in the user submitting their username, original - * password, and empty new password. If only the username and original - * password are sent, the invalid password reset request will be - * indistinguishable from a normal login attempt. - * - * @constant - * @type String - */ - var DEFAULT_FIELD_VALUE = ''; - - /** - * A description of the error that occurred during login, if any. - * - * @type TranslatableMessage - */ - $scope.loginError = null; - - /** - * All form values entered by the user, as parameter name/value pairs. - * - * @type Object. - */ - $scope.enteredValues = {}; - - /** - * All form fields which have not yet been filled by the user. - * - * @type Field[] - */ - $scope.remainingFields = []; - - /** - * Whether an authentication attempt has been submitted. This will be - * set to true once credentials have been submitted and will only be - * reset to false once the attempt has been fully processed, including - * rerouting the user to the requested page if the attempt succeeded. - * - * @type Boolean - */ - $scope.submitted = false; - - /** - * The field that is most relevant to the user. - * - * @type Field - */ - $scope.relevantField = null; - - /** - * Returns whether a previous login attempt is continuing. - * - * @return {Boolean} - * true if a previous login attempt is continuing, false otherwise. - */ - $scope.isContinuation = function isContinuation() { - - // The login is continuing if any parameter values are provided - for (var name in $scope.values) - return true; - - return false; - - }; - - // Ensure provided values are included within entered values, even if - // they have no corresponding input fields - $scope.$watch('values', function resetEnteredValues(values) { - angular.extend($scope.enteredValues, values || {}); - }); - - // Update field information when form is changed - $scope.$watch('form', function resetRemainingFields(fields) { - - // If no fields are provided, then no fields remain - if (!fields) { - $scope.remainingFields = []; - return; - } - - // Filter provided fields against provided values - $scope.remainingFields = fields.filter(function isRemaining(field) { - return !(field.name in $scope.values); - }); - - // Set default values for all unset fields - angular.forEach($scope.remainingFields, function setDefault(field) { - if (!$scope.enteredValues[field.name]) - $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; - }); - - $scope.relevantField = getRelevantField(); - - }); - - /** - * Submits the currently-specified fields to the authentication service, - * as well as any URL parameters set for the current page, preferring - * the values from the fields, and redirecting to the main view if - * successful. - */ - $scope.login = function login() { - - // Any values from URL paramters - const urlValues = $location.search(); - - // Values from the fields - const fieldValues = $scope.enteredValues; - - // All the values to be submitted in the auth attempt, preferring - // any values from fields over those in the URL - const authParams = {...urlValues, ...fieldValues}; - - authenticationService.authenticate(authParams)['catch'](requestService.IGNORE); - }; - - /** - * Returns the field most relevant to the user given the current state - * of the login process. This will normally be the first empty field. - * - * @return {Field} - * The field most relevant, null if there is no single most relevant - * field. - */ - var getRelevantField = function getRelevantField() { - - for (var i = 0; i < $scope.remainingFields.length; i++) { - var field = $scope.remainingFields[i]; - if (!$scope.enteredValues[field.name]) - return field; - } - - return null; - - }; - - // Update UI to reflect in-progress auth status (clear any previous - // errors, flag as pending) - $rootScope.$on('guacLoginPending', function loginSuccessful() { - $scope.submitted = true; - $scope.loginError = null; - }); - - // Retry route upon success (entered values will be cleared only - // after route change has succeeded as this can take time) - $rootScope.$on('guacLogin', function loginSuccessful() { - $route.reload(); - }); - - // Reset upon failure - $rootScope.$on('guacLoginFailed', function loginFailed(event, parameters, error) { - - // Initial submission is complete and has failed - $scope.submitted = false; - - // Clear out passwords if the credentials were rejected for any reason - if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { - - // Flag generic error for invalid login - if (error.type === Error.Type.INVALID_CREDENTIALS) - $scope.loginError = { - 'key' : 'LOGIN.ERROR_INVALID_LOGIN' - }; - - // Display error if anything else goes wrong - else - $scope.loginError = error.translatableMessage; - - // Reset all remaining fields to default values, but - // preserve any usernames - angular.forEach($scope.remainingFields, function clearEnteredValueIfPassword(field) { - if (field.type !== Field.Type.USERNAME && field.name in $scope.enteredValues) - $scope.enteredValues[field.name] = DEFAULT_FIELD_VALUE; - }); - } - - }); - - // Reset state after authentication and routing have succeeded - $rootScope.$on('$routeChangeSuccess', function routeChanged() { - $scope.enteredValues = {}; - $scope.submitted = false; - }); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/login/templates/login.html b/guacamole/src/main/frontend/src/app/login/templates/login.html deleted file mode 100644 index 9a354163bb..0000000000 --- a/guacamole/src/main/frontend/src/app/login/templates/login.html +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionController.js deleted file mode 100644 index 5635def0df..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionController.js +++ /dev/null @@ -1,355 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for editing or creating connections. - */ -angular.module('manage').controller('manageConnectionController', ['$scope', '$injector', - function manageConnectionController($scope, $injector) { - - // Required types - var Connection = $injector.get('Connection'); - var ConnectionGroup = $injector.get('ConnectionGroup'); - var HistoryEntryWrapper = $injector.get('HistoryEntryWrapper'); - var ManagementPermissions = $injector.get('ManagementPermissions'); - var PermissionSet = $injector.get('PermissionSet'); - var Protocol = $injector.get('Protocol'); - - // Required services - var $location = $injector.get('$location'); - var $q = $injector.get('$q'); - var $routeParams = $injector.get('$routeParams'); - var $translate = $injector.get('$translate'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var connectionService = $injector.get('connectionService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); - - /** - * The unique identifier of the data source containing the connection being - * edited. - * - * @type String - */ - $scope.selectedDataSource = $routeParams.dataSource; - - /** - * The identifier of the original connection from which this connection is - * being cloned. Only valid if this is a new connection. - * - * @type String - */ - var cloneSourceIdentifier = $location.search().clone; - - /** - * The identifier of the connection being edited. If a new connection is - * being created, this will not be defined. - * - * @type String - */ - var identifier = $routeParams.id; - - /** - * All known protocols. - * - * @type Object. - */ - $scope.protocols = null; - - /** - * The root connection group of the connection group hierarchy. - * - * @type ConnectionGroup - */ - $scope.rootGroup = null; - - /** - * The connection being modified. - * - * @type Connection - */ - $scope.connection = null; - - /** - * The parameter name/value pairs associated with the connection being - * modified. - * - * @type Object. - */ - $scope.parameters = null; - - /** - * The date format for use within the connection history. - * - * @type String - */ - $scope.historyDateFormat = null; - - /** - * The usage history of the connection being modified. - * - * @type HistoryEntryWrapper[] - */ - $scope.historyEntryWrappers = null; - - /** - * The management-related actions that the current user may perform on the - * connection currently being created/modified, or null if the current - * user's permissions have not yet been loaded. - * - * @type ManagementPermissions - */ - $scope.managementPermissions = null; - - /** - * All available connection attributes. This is only the set of attribute - * definitions, organized as logical groupings of attributes, not attribute - * values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.protocols !== null - && $scope.rootGroup !== null - && $scope.connection !== null - && $scope.parameters !== null - && $scope.historyDateFormat !== null - && $scope.historyEntryWrappers !== null - && $scope.managementPermissions !== null - && $scope.attributes !== null; - - }; - - /** - * Loads the data associated with the connection having the given - * identifier, preparing the interface for making modifications to that - * existing connection. - * - * @param {String} dataSource - * The unique identifier of the data source containing the connection to - * load. - * - * @param {String} identifier - * The identifier of the connection to load. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * editing the given connection. - */ - var loadExistingConnection = function loadExistingConnection(dataSource, identifier) { - return $q.all({ - connection : connectionService.getConnection(dataSource, identifier), - historyEntries : connectionService.getConnectionHistory(dataSource, identifier), - parameters : connectionService.getConnectionParameters(dataSource, identifier) - }) - .then(function connectionDataRetrieved(values) { - - $scope.connection = values.connection; - $scope.parameters = values.parameters; - - // Wrap all history entries for sake of display - $scope.historyEntryWrappers = []; - angular.forEach(values.historyEntries, function wrapHistoryEntry(historyEntry) { - $scope.historyEntryWrappers.push(new HistoryEntryWrapper(historyEntry)); - }); - - }); - }; - - /** - * Loads the data associated with the connection having the given - * identifier, preparing the interface for cloning that existing - * connection. - * - * @param {String} dataSource - * The unique identifier of the data source containing the connection - * to be cloned. - * - * @param {String} identifier - * The identifier of the connection being cloned. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * cloning the given connection. - */ - var loadClonedConnection = function loadClonedConnection(dataSource, identifier) { - return $q.all({ - connection : connectionService.getConnection(dataSource, identifier), - parameters : connectionService.getConnectionParameters(dataSource, identifier) - }) - .then(function connectionDataRetrieved(values) { - - $scope.connection = values.connection; - $scope.parameters = values.parameters; - - // Clear the identifier field because this connection is new - delete $scope.connection.identifier; - - // Cloned connections have no history - $scope.historyEntryWrappers = []; - - }); - }; - - /** - * Loads skeleton connection data, preparing the interface for creating a - * new connection. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * creating a new connection. - */ - var loadSkeletonConnection = function loadSkeletonConnection() { - - // Use skeleton connection object with no associated permissions, - // history, or parameters - $scope.connection = new Connection({ - protocol : 'vnc', - parentIdentifier : $location.search().parent - }); - $scope.historyEntryWrappers = []; - $scope.parameters = {}; - - return $q.resolve(); - - }; - - /** - * Loads the data required for performing the management task requested - * through the route parameters given at load time, automatically preparing - * the interface for editing an existing connection, cloning an existing - * connection, or creating an entirely new connection. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared - * for performing the requested management task. - */ - var loadRequestedConnection = function loadRequestedConnection() { - - // If we are editing an existing connection, pull its data - if (identifier) - return loadExistingConnection($scope.selectedDataSource, identifier); - - // If we are cloning an existing connection, pull its data instead - if (cloneSourceIdentifier) - return loadClonedConnection($scope.selectedDataSource, cloneSourceIdentifier); - - // If we are creating a new connection, populate skeleton connection data - return loadSkeletonConnection(); - - }; - - // Populate interface with requested data - $q.all({ - connectionData : loadRequestedConnection(), - attributes : schemaService.getConnectionAttributes($scope.selectedDataSource), - permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()), - protocols : schemaService.getProtocols($scope.selectedDataSource), - rootGroup : connectionGroupService.getConnectionGroupTree($scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) - }) - .then(function dataRetrieved(values) { - - $scope.attributes = values.attributes; - $scope.protocols = values.protocols; - $scope.rootGroup = values.rootGroup; - - $scope.managementPermissions = ManagementPermissions.fromPermissionSet( - values.permissions, - PermissionSet.SystemPermissionType.CREATE_CONNECTION, - PermissionSet.hasConnectionPermission, - identifier); - - }, requestService.DIE); - - // Get history date format - $translate('MANAGE_CONNECTION.FORMAT_HISTORY_START').then(function historyDateFormatReceived(historyDateFormat) { - $scope.historyDateFormat = historyDateFormat; - }, angular.noop); - - /** - * @borrows Protocol.getNamespace - */ - $scope.getNamespace = Protocol.getNamespace; - - /** - * @borrows Protocol.getName - */ - $scope.getProtocolName = Protocol.getName; - - /** - * Cancels all pending edits, returning to the main list of connections - * within the selected data source. - */ - $scope.returnToConnectionList = function returnToConnectionList() { - $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }; - - /** - * Cancels all pending edits, opening an edit page for a new connection - * which is prepopulated with the data from the connection currently being edited. - */ - $scope.cloneConnection = function cloneConnection() { - $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/connections').search('clone', identifier); - $window.scrollTo(0,0); - }; - - /** - * Saves the current connection, creating a new connection or updating the - * existing connection, returning a promise which is resolved if the save - * operation succeeds and rejected if the save operation fails. - * - * @returns {Promise} - * A promise which is resolved if the save operation succeeds and is - * rejected with an {@link Error} if the save operation fails. - */ - $scope.saveConnection = function saveConnection() { - - $scope.connection.parameters = $scope.parameters; - - // Save the connection - return connectionService.saveConnection($scope.selectedDataSource, $scope.connection); - - }; - - /** - * Deletes the current connection, returning a promise which is resolved if - * the delete operation succeeds and rejected if the delete operation fails. - * - * @returns {Promise} - * A promise which is resolved if the delete operation succeeds and is - * rejected with an {@link Error} if the delete operation fails. - */ - $scope.deleteConnection = function deleteConnection() { - return connectionService.deleteConnection($scope.selectedDataSource, $scope.connection); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionGroupController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionGroupController.js deleted file mode 100644 index e0029cb157..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageConnectionGroupController.js +++ /dev/null @@ -1,296 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for editing or creating connection groups. - */ -angular.module('manage').controller('manageConnectionGroupController', ['$scope', '$injector', - function manageConnectionGroupController($scope, $injector) { - - // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var ManagementPermissions = $injector.get('ManagementPermissions'); - var PermissionSet = $injector.get('PermissionSet'); - - // Required services - var $location = $injector.get('$location'); - var $q = $injector.get('$q'); - var $routeParams = $injector.get('$routeParams'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); - - /** - * The unique identifier of the data source containing the connection group - * being edited. - * - * @type String - */ - $scope.selectedDataSource = $routeParams.dataSource; - - /** - * The identifier of the original connection group from which this - * connection group is being cloned. Only valid if this is a new - * connection group. - * - * @type String - */ - var cloneSourceIdentifier = $location.search().clone; - - /** - * The identifier of the connection group being edited. If a new connection - * group is being created, this will not be defined. - * - * @type String - */ - var identifier = $routeParams.id; - - /** - * Available connection group types, as translation string / internal value - * pairs. - * - * @type Object[] - */ - $scope.types = [ - { - label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_ORGANIZATIONAL", - value: ConnectionGroup.Type.ORGANIZATIONAL - }, - { - label: "MANAGE_CONNECTION_GROUP.NAME_TYPE_BALANCING", - value : ConnectionGroup.Type.BALANCING - } - ]; - - /** - * The root connection group of the connection group hierarchy. - * - * @type ConnectionGroup - */ - $scope.rootGroup = null; - - /** - * The connection group being modified. - * - * @type ConnectionGroup - */ - $scope.connectionGroup = null; - - /** - * The management-related actions that the current user may perform on the - * connection group currently being created/modified, or null if the current - * user's permissions have not yet been loaded. - * - * @type ManagementPermissions - */ - $scope.managementPermissions = null; - - /** - * All available connection group attributes. This is only the set of - * attribute definitions, organized as logical groupings of attributes, not - * attribute values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.rootGroup !== null - && $scope.connectionGroup !== null - && $scope.managementPermissions !== null - && $scope.attributes !== null; - - }; - - /** - * Loads the data associated with the connection group having the given - * identifier, preparing the interface for making modifications to that - * existing connection group. - * - * @param {String} dataSource - * The unique identifier of the data source containing the connection - * group to load. - * - * @param {String} identifier - * The identifier of the connection group to load. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * editing the given connection group. - */ - var loadExistingConnectionGroup = function loadExistingConnectionGroup(dataSource, identifier) { - return connectionGroupService.getConnectionGroup( - dataSource, - identifier - ) - .then(function connectionGroupReceived(connectionGroup) { - $scope.connectionGroup = connectionGroup; - }); - }; - - /** - * Loads the data associated with the connection group having the given - * identifier, preparing the interface for cloning that existing - * connection group. - * - * @param {String} dataSource - * The unique identifier of the data source containing the connection - * group to be cloned. - * - * @param {String} identifier - * The identifier of the connection group being cloned. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * cloning the given connection group. - */ - var loadClonedConnectionGroup = function loadClonedConnectionGroup(dataSource, identifier) { - return connectionGroupService.getConnectionGroup( - dataSource, - identifier - ) - .then(function connectionGroupReceived(connectionGroup) { - $scope.connectionGroup = connectionGroup; - delete $scope.connectionGroup.identifier; - }); - }; - - /** - * Loads skeleton connection group data, preparing the interface for - * creating a new connection group. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * creating a new connection group. - */ - var loadSkeletonConnectionGroup = function loadSkeletonConnectionGroup() { - - // Use skeleton connection group object with specified parent - $scope.connectionGroup = new ConnectionGroup({ - parentIdentifier : $location.search().parent - }); - - return $q.resolve(); - - }; - - /** - * Loads the data required for performing the management task requested - * through the route parameters given at load time, automatically preparing - * the interface for editing an existing connection group, cloning an - * existing connection group, or creating an entirely new connection group. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared - * for performing the requested management task. - */ - var loadRequestedConnectionGroup = function loadRequestedConnectionGroup() { - - // If we are editing an existing connection group, pull its data - if (identifier) - return loadExistingConnectionGroup($scope.selectedDataSource, identifier); - - // If we are cloning an existing connection group, pull its data - // instead - if (cloneSourceIdentifier) - return loadClonedConnectionGroup($scope.selectedDataSource, cloneSourceIdentifier); - - // If we are creating a new connection group, populate skeleton - // connection group data - return loadSkeletonConnectionGroup(); - - }; - - // Query the user's permissions for the current connection group - $q.all({ - connectionGroupData : loadRequestedConnectionGroup(), - attributes : schemaService.getConnectionGroupAttributes($scope.selectedDataSource), - permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()), - rootGroup : connectionGroupService.getConnectionGroupTree($scope.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) - }) - .then(function connectionGroupDataRetrieved(values) { - - $scope.attributes = values.attributes; - $scope.rootGroup = values.rootGroup; - - $scope.managementPermissions = ManagementPermissions.fromPermissionSet( - values.permissions, - PermissionSet.SystemPermissionType.CREATE_CONNECTION, - PermissionSet.hasConnectionGroupPermission, - identifier); - - }, requestService.DIE); - - /** - * Cancels all pending edits, returning to the main list of connections - * within the selected data source. - */ - $scope.returnToConnectionList = function returnToConnectionList() { - $location.path('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }; - - /** - * Cancels all pending edits, opening an edit page for a new connection - * group which is prepopulated with the data from the connection group - * currently being edited. - */ - $scope.cloneConnectionGroup = function cloneConnectionGRoup() { - $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/connectionGroups').search('clone', identifier); - $window.scrollTo(0,0); - }; - - /** - * Saves the current connection group, creating a new connection group or - * updating the existing connection group, returning a promise which is - * resolved if the save operation succeeds and rejected if the save - * operation fails. - * - * @returns {Promise} - * A promise which is resolved if the save operation succeeds and is - * rejected with an {@link Error} if the save operation fails. - */ - $scope.saveConnectionGroup = function saveConnectionGroup() { - return connectionGroupService.saveConnectionGroup($scope.selectedDataSource, $scope.connectionGroup); - }; - - /** - * Deletes the current connection group, returning a promise which is - * resolved if the delete operation succeeds and rejected if the delete - * operation fails. - * - * @returns {Promise} - * A promise which is resolved if the delete operation succeeds and is - * rejected with an {@link Error} if the delete operation fails. - */ - $scope.deleteConnectionGroup = function deleteConnectionGroup() { - return connectionGroupService.deleteConnectionGroup($scope.selectedDataSource, $scope.connectionGroup); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageSharingProfileController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageSharingProfileController.js deleted file mode 100644 index 139eaf893b..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageSharingProfileController.js +++ /dev/null @@ -1,338 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for editing or creating sharing profiles. - */ -angular.module('manage').controller('manageSharingProfileController', ['$scope', '$injector', - function manageSharingProfileController($scope, $injector) { - - // Required types - var ManagementPermissions = $injector.get('ManagementPermissions'); - var SharingProfile = $injector.get('SharingProfile'); - var PermissionSet = $injector.get('PermissionSet'); - var Protocol = $injector.get('Protocol'); - - // Required services - var $location = $injector.get('$location'); - var $q = $injector.get('$q'); - var $routeParams = $injector.get('$routeParams'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var connectionService = $injector.get('connectionService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); - var sharingProfileService = $injector.get('sharingProfileService'); - - /** - * The unique identifier of the data source containing the sharing profile - * being edited. - * - * @type String - */ - $scope.selectedDataSource = $routeParams.dataSource; - - /** - * The identifier of the original sharing profile from which this sharing - * profile is being cloned. Only valid if this is a new sharing profile. - * - * @type String - */ - var cloneSourceIdentifier = $location.search().clone; - - /** - * The identifier of the sharing profile being edited. If a new sharing - * profile is being created, this will not be defined. - * - * @type String - */ - var identifier = $routeParams.id; - - /** - * Map of protocol name to corresponding Protocol object. - * - * @type Object. - */ - $scope.protocols = null; - - /** - * The sharing profile being modified. - * - * @type SharingProfile - */ - $scope.sharingProfile = null; - - /** - * The parameter name/value pairs associated with the sharing profile being - * modified. - * - * @type Object. - */ - $scope.parameters = null; - - /** - * The management-related actions that the current user may perform on the - * sharing profile currently being created/modified, or null if the current - * user's permissions have not yet been loaded. - * - * @type ManagementPermissions - */ - $scope.managementPermissions = null; - - /** - * All available sharing profile attributes. This is only the set of - * attribute definitions, organized as logical groupings of attributes, not - * attribute values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.protocols !== null - && $scope.sharingProfile !== null - && $scope.primaryConnection !== null - && $scope.parameters !== null - && $scope.managementPermissions !== null - && $scope.attributes !== null; - - }; - - /** - * Loads the data associated with the sharing profile having the given - * identifier, preparing the interface for making modifications to that - * existing sharing profile. - * - * @param {String} dataSource - * The unique identifier of the data source containing the sharing - * profile to load. - * - * @param {String} identifier - * The identifier of the sharing profile to load. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * editing the given sharing profile. - */ - var loadExistingSharingProfile = function loadExistingSharingProfile(dataSource, identifier) { - return $q.all({ - sharingProfile : sharingProfileService.getSharingProfile(dataSource, identifier), - parameters : sharingProfileService.getSharingProfileParameters(dataSource, identifier) - }) - .then(function sharingProfileDataRetrieved(values) { - - $scope.sharingProfile = values.sharingProfile; - $scope.parameters = values.parameters; - - // Load connection object for associated primary connection - return connectionService.getConnection( - dataSource, - values.sharingProfile.primaryConnectionIdentifier - ) - .then(function connectionRetrieved(connection) { - $scope.primaryConnection = connection; - }); - - }); - }; - - /** - * Loads the data associated with the sharing profile having the given - * identifier, preparing the interface for cloning that existing - * sharing profile. - * - * @param {String} dataSource - * The unique identifier of the data source containing the sharing - * profile to be cloned. - * - * @param {String} identifier - * The identifier of the sharing profile being cloned. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * cloning the given sharing profile. - */ - var loadClonedSharingProfile = function loadClonedSharingProfile(dataSource, identifier) { - return $q.all({ - sharingProfile : sharingProfileService.getSharingProfile(dataSource, identifier), - parameters : sharingProfileService.getSharingProfileParameters(dataSource, identifier) - }) - .then(function sharingProfileDataRetrieved(values) { - - $scope.sharingProfile = values.sharingProfile; - $scope.parameters = values.parameters; - - // Clear the identifier field because this sharing profile is new - delete $scope.sharingProfile.identifier; - - // Load connection object for associated primary connection - return connectionService.getConnection( - dataSource, - values.sharingProfile.primaryConnectionIdentifier - ) - .then(function connectionRetrieved(connection) { - $scope.primaryConnection = connection; - }); - - }); - }; - - /** - * Loads skeleton sharing profile data, preparing the interface for - * creating a new sharing profile. - * - * @param {String} dataSource - * The unique identifier of the data source containing the sharing - * profile to be created. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * creating a new sharing profile. - */ - var loadSkeletonSharingProfile = function loadSkeletonSharingProfile(dataSource) { - - // Use skeleton sharing profile object with no associated parameters - $scope.sharingProfile = new SharingProfile({ - primaryConnectionIdentifier : $location.search().parent - }); - $scope.parameters = {}; - - // Load connection object for associated primary connection - return connectionService.getConnection( - dataSource, - $scope.sharingProfile.primaryConnectionIdentifier - ) - .then(function connectionRetrieved(connection) { - $scope.primaryConnection = connection; - }); - - }; - - /** - * Loads the data required for performing the management task requested - * through the route parameters given at load time, automatically preparing - * the interface for editing an existing sharing profile, cloning an - * existing sharing profile, or creating an entirely new sharing profile. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared - * for performing the requested management task. - */ - var loadRequestedSharingProfile = function loadRequestedSharingProfile() { - - // If we are editing an existing sharing profile, pull its data - if (identifier) - return loadExistingSharingProfile($scope.selectedDataSource, identifier); - - // If we are cloning an existing sharing profile, pull its data instead - if (cloneSourceIdentifier) - return loadClonedSharingProfile($scope.selectedDataSource, cloneSourceIdentifier); - - // If we are creating a new sharing profile, populate skeleton sharing - // profile data - return loadSkeletonSharingProfile($scope.selectedDataSource); - - }; - - - // Query the user's permissions for the current sharing profile - $q.all({ - sharingProfileData : loadRequestedSharingProfile(), - attributes : schemaService.getSharingProfileAttributes($scope.selectedDataSource), - protocols : schemaService.getProtocols($scope.selectedDataSource), - permissions : permissionService.getEffectivePermissions($scope.selectedDataSource, authenticationService.getCurrentUsername()) - }) - .then(function sharingProfileDataRetrieved(values) { - - $scope.attributes = values.attributes; - $scope.protocols = values.protocols; - - $scope.managementPermissions = ManagementPermissions.fromPermissionSet( - values.permissions, - PermissionSet.SystemPermissionType.CREATE_CONNECTION, - PermissionSet.hasConnectionPermission, - identifier); - - }, requestService.DIE); - - /** - * @borrows Protocol.getNamespace - */ - $scope.getNamespace = Protocol.getNamespace; - - /** - * Cancels all pending edits, returning to the main list of connections - * within the selected data source. - */ - $scope.returnToConnectionList = function returnToConnectionList() { - $location.url('/settings/' + encodeURIComponent($scope.selectedDataSource) + '/connections'); - }; - - /** - * Cancels all pending edits, opening an edit page for a new sharing profile - * which is prepopulated with the data from the sharing profile currently - * being edited. - */ - $scope.cloneSharingProfile = function cloneSharingProfile() { - $location.path('/manage/' + encodeURIComponent($scope.selectedDataSource) + '/sharingProfiles').search('clone', identifier); - $window.scrollTo(0,0); - }; - - /** - * Saves the current sharing profile, creating a new sharing profile or - * updating the existing sharing profile, returning a promise which is - * resolved if the save operation succeeds and rejected if the save - * operation fails. - * - * @returns {Promise} - * A promise which is resolved if the save operation succeeds and is - * rejected with an {@link Error} if the save operation fails. - */ - $scope.saveSharingProfile = function saveSharingProfile() { - - $scope.sharingProfile.parameters = $scope.parameters; - - // Save the sharing profile - return sharingProfileService.saveSharingProfile($scope.selectedDataSource, $scope.sharingProfile); - - }; - - /** - * Deletes the current sharing profile, returning a promise which is - * resolved if the delete operation succeeds and rejected if the delete - * operation fails. - * - * @returns {Promise} - * A promise which is resolved if the delete operation succeeds and is - * rejected with an {@link Error} if the delete operation fails. - */ - $scope.deleteSharingProfile = function deleteSharingProfile() { - return sharingProfileService.deleteSharingProfile($scope.selectedDataSource, $scope.sharingProfile); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js deleted file mode 100644 index d1bbbb52fb..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserController.js +++ /dev/null @@ -1,506 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for editing users. - */ -angular.module('manage').controller('manageUserController', ['$scope', '$injector', - function manageUserController($scope, $injector) { - - // Required types - var Error = $injector.get('Error'); - var ManagementPermissions = $injector.get('ManagementPermissions'); - var PermissionFlagSet = $injector.get('PermissionFlagSet'); - var PermissionSet = $injector.get('PermissionSet'); - var User = $injector.get('User'); - - // Required services - var $location = $injector.get('$location'); - var $routeParams = $injector.get('$routeParams'); - var $q = $injector.get('$q'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var dataSourceService = $injector.get('dataSourceService'); - var membershipService = $injector.get('membershipService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); - var userGroupService = $injector.get('userGroupService'); - var userService = $injector.get('userService'); - - /** - * The identifiers of all data sources currently available to the - * authenticated user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * The username of the current, authenticated user. - * - * @type String - */ - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * The username of the original user from which this user is - * being cloned. Only valid if this is a new user. - * - * @type String - */ - var cloneSourceUsername = $location.search().clone; - - /** - * The username of the user being edited. If a new user is - * being created, this will not be defined. - * - * @type String - */ - var username = $routeParams.id; - - /** - * The unique identifier of the data source containing the user being - * edited. - * - * @type String - */ - $scope.dataSource = $routeParams.dataSource; - - /** - * The string value representing the user currently being edited within the - * permission flag set. Note that his may not match the user's actual - * username - it is a marker that is (1) guaranteed to be associated with - * the current user's permissions in the permission set and (2) guaranteed - * not to collide with any user that does not represent the current user - * within the permission set. - * - * @type String - */ - $scope.selfUsername = ''; - - /** - * All user accounts associated with the same username as the account being - * created or edited, as a map of data source identifier to the User object - * within that data source. - * - * @type Object. - */ - $scope.users = null; - - /** - * The user being modified. - * - * @type User - */ - $scope.user = null; - - /** - * All permissions associated with the user being modified. - * - * @type PermissionFlagSet - */ - $scope.permissionFlags = null; - - /** - * The set of permissions that will be added to the user when the user is - * saved. Permissions will only be present in this set if they are - * manually added, and not later manually removed before saving. - * - * @type PermissionSet - */ - $scope.permissionsAdded = new PermissionSet(); - - /** - * The set of permissions that will be removed from the user when the user - * is saved. Permissions will only be present in this set if they are - * manually removed, and not later manually added before saving. - * - * @type PermissionSet - */ - $scope.permissionsRemoved = new PermissionSet(); - - /** - * The identifiers of all user groups which can be manipulated (all groups - * for which the user accessing this interface has UPDATE permission), - * either through adding the current user as a member or removing the - * current user from that group. If this information has not yet been - * retrieved, this will be null. - * - * @type String[] - */ - $scope.availableGroups = null; - - /** - * The identifiers of all user groups of which the user is a member, - * taking into account any user groups which will be added/removed when - * saved. If this information has not yet been retrieved, this will be - * null. - * - * @type String[] - */ - $scope.parentGroups = null; - - /** - * The set of identifiers of all parent user groups to which the user will - * be added when saved. Parent groups will only be present in this set if - * they are manually added, and not later manually removed before saving. - * - * @type String[] - */ - $scope.parentGroupsAdded = []; - - /** - * The set of identifiers of all parent user groups from which the user - * will be removed when saved. Parent groups will only be present in this - * set if they are manually removed, and not later manually added before - * saving. - * - * @type String[] - */ - $scope.parentGroupsRemoved = []; - - /** - * For each applicable data source, the management-related actions that the - * current user may perform on the user account currently being created - * or modified, as a map of data source identifier to the - * {@link ManagementPermissions} object describing the actions available - * within that data source, or null if the current user's permissions have - * not yet been loaded. - * - * @type Object. - */ - $scope.managementPermissions = null; - - /** - * All available user attributes. This is only the set of attribute - * definitions, organized as logical groupings of attributes, not attribute - * values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.users !== null - && $scope.permissionFlags !== null - && $scope.managementPermissions !== null - && $scope.availableGroups !== null - && $scope.parentGroups !== null - && $scope.attributes !== null; - - }; - - /** - * Returns whether the current user can edit the username of the user being - * edited within the given data source. - * - * @param {String} [dataSource] - * The identifier of the data source to check. If omitted, this will - * default to the currently-selected data source. - * - * @returns {Boolean} - * true if the current user can edit the username of the user being - * edited, false otherwise. - */ - $scope.canEditUsername = function canEditUsername(dataSource) { - return !username; - }; - - /** - * Loads the data associated with the user having the given username, - * preparing the interface for making modifications to that existing user. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to - * load. - * - * @param {String} username - * The username of the user to load. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * editing the given user. - */ - var loadExistingUser = function loadExistingUser(dataSource, username) { - return $q.all({ - users : dataSourceService.apply(userService.getUser, dataSources, username), - - // Use empty permission set if user cannot be found - permissions: - permissionService.getPermissions(dataSource, username) - ['catch'](requestService.defaultValue(new PermissionSet())), - - // Assume no parent groups if user cannot be found - parentGroups: - membershipService.getUserGroups(dataSource, username) - ['catch'](requestService.defaultValue([])) - - }) - .then(function userDataRetrieved(values) { - - $scope.users = values.users; - $scope.parentGroups = values.parentGroups; - - // Create skeleton user if user does not exist - $scope.user = values.users[dataSource] || new User({ - 'username' : username - }); - - // The current user will be associated with username of the existing - // user in the retrieved permission set - $scope.selfUsername = username; - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); - - }); - }; - - /** - * Loads the data associated with the user having the given username, - * preparing the interface for cloning that existing user. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to - * be cloned. - * - * @param {String} username - * The username of the user being cloned. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * cloning the given user. - */ - var loadClonedUser = function loadClonedUser(dataSource, username) { - return $q.all({ - users : dataSourceService.apply(userService.getUser, [dataSource], username), - permissions : permissionService.getPermissions(dataSource, username), - parentGroups : membershipService.getUserGroups(dataSource, username) - }) - .then(function userDataRetrieved(values) { - - $scope.users = {}; - $scope.user = values.users[dataSource]; - $scope.parentGroups = values.parentGroups; - $scope.parentGroupsAdded = values.parentGroups; - - // The current user will be associated with cloneSourceUsername in the - // retrieved permission set - $scope.selfUsername = username; - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); - $scope.permissionsAdded = values.permissions; - - }); - }; - - /** - * Loads skeleton user data, preparing the interface for creating a new - * user. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * creating a new user. - */ - var loadSkeletonUser = function loadSkeletonUser() { - - // No users exist regardless of data source if there is no username - $scope.users = {}; - - // Use skeleton user object with no associated permissions - $scope.user = new User(); - $scope.parentGroups = []; - $scope.permissionFlags = new PermissionFlagSet(); - - // As no permissions are yet associated with the user, it is safe to - // use any non-empty username as a placeholder for self-referential - // permissions - $scope.selfUsername = 'SELF'; - - return $q.resolve(); - - }; - - /** - * Loads the data required for performing the management task requested - * through the route parameters given at load time, automatically preparing - * the interface for editing an existing user, cloning an existing user, or - * creating an entirely new user. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared - * for performing the requested management task. - */ - var loadRequestedUser = function loadRequestedUser() { - - // Pull user data and permissions if we are editing an existing user - if (username) - return loadExistingUser($scope.dataSource, username); - - // If we are cloning an existing user, pull his/her data instead - if (cloneSourceUsername) - return loadClonedUser($scope.dataSource, cloneSourceUsername); - - // If we are creating a new user, populate skeleton user data - return loadSkeletonUser(); - - }; - - // Populate interface with requested data - $q.all({ - userData : loadRequestedUser(), - permissions : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername), - userGroups : userGroupService.getUserGroups($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]), - attributes : schemaService.getUserAttributes($scope.dataSource) - }) - .then(function dataReceived(values) { - - $scope.attributes = values.attributes; - - $scope.managementPermissions = {}; - angular.forEach(dataSources, function addAccountPage(dataSource) { - - // Determine whether data source contains this user - var exists = (dataSource in $scope.users); - - // Add the identifiers of all modifiable user groups - $scope.availableGroups = []; - angular.forEach(values.userGroups, function addUserGroupIdentifier(userGroup) { - $scope.availableGroups.push(userGroup.identifier); - }); - - // Calculate management actions available for this specific account - $scope.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( - values.permissions[dataSource], - PermissionSet.SystemPermissionType.CREATE_USER, - PermissionSet.hasUserPermission, - exists ? username : null); - - }); - - }, requestService.DIE); - - /** - * Returns the URL for the page which manages the user account currently - * being edited under the given data source. The given data source need not - * be the same as the data source currently selected. - * - * @param {String} dataSource - * The unique identifier of the data source that the URL is being - * generated for. - * - * @returns {String} - * The URL for the page which manages the user account currently being - * edited under the given data source. - */ - $scope.getUserURL = function getUserURL(dataSource) { - return '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username || ''); - }; - - /** - * Cancels all pending edits, returning to the main list of users. - */ - $scope.returnToUserList = function returnToUserList() { - $location.url('/settings/users'); - }; - - /** - * Cancels all pending edits, opening an edit page for a new user - * which is prepopulated with the data from the user currently being edited. - */ - $scope.cloneUser = function cloneUser() { - $location.path('/manage/' + encodeURIComponent($scope.dataSource) + '/users').search('clone', username); - $window.scrollTo(0,0); - }; - - /** - * Saves the current user, creating a new user or updating the existing - * user depending on context, returning a promise which is resolved if the - * save operation succeeds and rejected if the save operation fails. - * - * @returns {Promise} - * A promise which is resolved if the save operation succeeds and is - * rejected with an {@link Error} if the save operation fails. - */ - $scope.saveUser = function saveUser() { - - // Verify passwords match - if ($scope.passwordMatch !== $scope.user.password) { - return $q.reject(new Error({ - translatableMessage : { - key : 'MANAGE_USER.ERROR_PASSWORD_MISMATCH' - } - })); - } - - // Save or create the user, depending on whether the user exists - var saveUserPromise; - if ($scope.dataSource in $scope.users) - saveUserPromise = userService.saveUser($scope.dataSource, $scope.user); - else - saveUserPromise = userService.createUser($scope.dataSource, $scope.user); - - return saveUserPromise.then(function savedUser() { - - // Move permission flags if username differs from marker - if ($scope.selfUsername !== $scope.user.username) { - - // Rename added permission - if ($scope.permissionsAdded.userPermissions[$scope.selfUsername]) { - $scope.permissionsAdded.userPermissions[$scope.user.username] = $scope.permissionsAdded.userPermissions[$scope.selfUsername]; - delete $scope.permissionsAdded.userPermissions[$scope.selfUsername]; - } - - // Rename removed permission - if ($scope.permissionsRemoved.userPermissions[$scope.selfUsername]) { - $scope.permissionsRemoved.userPermissions[$scope.user.username] = $scope.permissionsRemoved.userPermissions[$scope.selfUsername]; - delete $scope.permissionsRemoved.userPermissions[$scope.selfUsername]; - } - - } - - // Upon success, save any changed permissions/groups - return $q.all([ - permissionService.patchPermissions($scope.dataSource, $scope.user.username, $scope.permissionsAdded, $scope.permissionsRemoved), - membershipService.patchUserGroups($scope.dataSource, $scope.user.username, $scope.parentGroupsAdded, $scope.parentGroupsRemoved) - ]); - - }); - - }; - - /** - * Deletes the current user, returning a promise which is resolved if the - * delete operation succeeds and rejected if the delete operation fails. - * - * @returns {Promise} - * A promise which is resolved if the delete operation succeeds and is - * rejected with an {@link Error} if the delete operation fails. - */ - $scope.deleteUser = function deleteUser() { - return userService.deleteUser($scope.dataSource, $scope.user); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserGroupController.js b/guacamole/src/main/frontend/src/app/manage/controllers/manageUserGroupController.js deleted file mode 100644 index b58fe283e9..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/controllers/manageUserGroupController.js +++ /dev/null @@ -1,555 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for editing user groups. - */ -angular.module('manage').controller('manageUserGroupController', ['$scope', '$injector', - function manageUserGroupController($scope, $injector) { - - // Required types - var ManagementPermissions = $injector.get('ManagementPermissions'); - var PermissionFlagSet = $injector.get('PermissionFlagSet'); - var PermissionSet = $injector.get('PermissionSet'); - var UserGroup = $injector.get('UserGroup'); - - // Required services - var $location = $injector.get('$location'); - var $routeParams = $injector.get('$routeParams'); - var $q = $injector.get('$q'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var dataSourceService = $injector.get('dataSourceService'); - var membershipService = $injector.get('membershipService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var schemaService = $injector.get('schemaService'); - var userGroupService = $injector.get('userGroupService'); - var userService = $injector.get('userService'); - - /** - * The identifiers of all data sources currently available to the - * authenticated user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * The username of the current, authenticated user. - * - * @type String - */ - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * The identifier of the original user group from which this user group is - * being cloned. Only valid if this is a new user group. - * - * @type String - */ - var cloneSourceIdentifier = $location.search().clone; - - /** - * The identifier of the user group being edited. If a new user group is - * being created, this will not be defined. - * - * @type String - */ - var identifier = $routeParams.id; - - /** - * The unique identifier of the data source containing the user group being - * edited. - * - * @type String - */ - $scope.dataSource = $routeParams.dataSource; - - /** - * All user groups associated with the same identifier as the group being - * created or edited, as a map of data source identifier to the UserGroup - * object within that data source. - * - * @type Object. - */ - $scope.userGroups = null; - - /** - * The user group being modified. - * - * @type UserGroup - */ - $scope.userGroup = null; - - /** - * All permissions associated with the user group being modified. - * - * @type PermissionFlagSet - */ - $scope.permissionFlags = null; - - /** - * The set of permissions that will be added to the user group when the - * user group is saved. Permissions will only be present in this set if they - * are manually added, and not later manually removed before saving. - * - * @type PermissionSet - */ - $scope.permissionsAdded = new PermissionSet(); - - /** - * The set of permissions that will be removed from the user group when the - * user group is saved. Permissions will only be present in this set if they - * are manually removed, and not later manually added before saving. - * - * @type PermissionSet - */ - $scope.permissionsRemoved = new PermissionSet(); - - /** - * The identifiers of all user groups which can be manipulated (all groups - * for which the user accessing this interface has UPDATE permission), - * whether that means changing the members of those groups or changing the - * groups of which those groups are members. If this information has not - * yet been retrieved, this will be null. - * - * @type String[] - */ - $scope.availableGroups = null; - - /** - * The identifiers of all users which can be manipulated (all users for - * which the user accessing this interface has UPDATE permission), either - * through adding those users as a member of the current group or removing - * those users from the current group. If this information has not yet been - * retrieved, this will be null. - * - * @type String[] - */ - $scope.availableUsers = null; - - /** - * The identifiers of all user groups of which this group is a member, - * taking into account any user groups which will be added/removed when - * saved. If this information has not yet been retrieved, this will be - * null. - * - * @type String[] - */ - $scope.parentGroups = null; - - /** - * The set of identifiers of all parent user groups to which this group - * will be added when saved. Parent groups will only be present in this set - * if they are manually added, and not later manually removed before - * saving. - * - * @type String[] - */ - $scope.parentGroupsAdded = []; - - /** - * The set of identifiers of all parent user groups from which this group - * will be removed when saved. Parent groups will only be present in this - * set if they are manually removed, and not later manually added before - * saving. - * - * @type String[] - */ - $scope.parentGroupsRemoved = []; - - /** - * The identifiers of all user groups which are members of this group, - * taking into account any user groups which will be added/removed when - * saved. If this information has not yet been retrieved, this will be - * null. - * - * @type String[] - */ - $scope.memberGroups = null; - - /** - * The set of identifiers of all member user groups which will be added to - * this group when saved. Member groups will only be present in this set if - * they are manually added, and not later manually removed before saving. - * - * @type String[] - */ - $scope.memberGroupsAdded = []; - - /** - * The set of identifiers of all member user groups which will be removed - * from this group when saved. Member groups will only be present in this - * set if they are manually removed, and not later manually added before - * saving. - * - * @type String[] - */ - $scope.memberGroupsRemoved = []; - - /** - * The identifiers of all users which are members of this group, taking - * into account any users which will be added/removed when saved. If this - * information has not yet been retrieved, this will be null. - * - * @type String[] - */ - $scope.memberUsers = null; - - /** - * The set of identifiers of all member users which will be added to this - * group when saved. Member users will only be present in this set if they - * are manually added, and not later manually removed before saving. - * - * @type String[] - */ - $scope.memberUsersAdded = []; - - /** - * The set of identifiers of all member users which will be removed from - * this group when saved. Member users will only be present in this set if - * they are manually removed, and not later manually added before saving. - * - * @type String[] - */ - $scope.memberUsersRemoved = []; - - /** - * For each applicable data source, the management-related actions that the - * current user may perform on the user group currently being created - * or modified, as a map of data source identifier to the - * {@link ManagementPermissions} object describing the actions available - * within that data source, or null if the current user's permissions have - * not yet been loaded. - * - * @type Object. - */ - $scope.managementPermissions = null; - - /** - * All available user group attributes. This is only the set of attribute - * definitions, organized as logical groupings of attributes, not attribute - * values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user group interface to - * be useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.userGroups !== null - && $scope.permissionFlags !== null - && $scope.managementPermissions !== null - && $scope.availableGroups !== null - && $scope.availableUsers !== null - && $scope.parentGroups !== null - && $scope.memberGroups !== null - && $scope.memberUsers !== null - && $scope.attributes !== null; - - }; - - /** - * Returns whether the current user can edit the identifier of the user - * group being edited. - * - * @returns {Boolean} - * true if the current user can edit the identifier of the user group - * being edited, false otherwise. - */ - $scope.canEditIdentifier = function canEditIdentifier() { - return !identifier; - }; - - /** - * Loads the data associated with the user group having the given - * identifier, preparing the interface for making modifications to that - * existing user group. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user group - * to load. - * - * @param {String} identifier - * The unique identifier of the user group to load. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * editing the given user group. - */ - var loadExistingUserGroup = function loadExistingGroup(dataSource, identifier) { - return $q.all({ - userGroups : dataSourceService.apply(userGroupService.getUserGroup, dataSources, identifier), - - // Use empty permission set if group cannot be found - permissions: - permissionService.getPermissions(dataSource, identifier, true) - ['catch'](requestService.defaultValue(new PermissionSet())), - - // Assume no parent groups if group cannot be found - parentGroups: - membershipService.getUserGroups(dataSource, identifier, true) - ['catch'](requestService.defaultValue([])), - - // Assume no member groups if group cannot be found - memberGroups: - membershipService.getMemberUserGroups(dataSource, identifier) - ['catch'](requestService.defaultValue([])), - - // Assume no member users if group cannot be found - memberUsers: - membershipService.getMemberUsers(dataSource, identifier) - ['catch'](requestService.defaultValue([])) - - }) - .then(function userGroupDataRetrieved(values) { - - $scope.userGroups = values.userGroups; - $scope.parentGroups = values.parentGroups; - $scope.memberGroups = values.memberGroups; - $scope.memberUsers = values.memberUsers; - - // Create skeleton user group if user group does not exist - $scope.userGroup = values.userGroups[dataSource] || new UserGroup({ - 'identifier' : identifier - }); - - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); - - }); - }; - - /** - * Loads the data associated with the user group having the given - * identifier, preparing the interface for cloning that existing user - * group. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user group to - * be cloned. - * - * @param {String} identifier - * The unique identifier of the user group being cloned. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * cloning the given user group. - */ - var loadClonedUserGroup = function loadClonedUserGroup(dataSource, identifier) { - return $q.all({ - userGroups : dataSourceService.apply(userGroupService.getUserGroup, [dataSource], identifier), - permissions : permissionService.getPermissions(dataSource, identifier, true), - parentGroups : membershipService.getUserGroups(dataSource, identifier, true), - memberGroups : membershipService.getMemberUserGroups(dataSource, identifier), - memberUsers : membershipService.getMemberUsers(dataSource, identifier) - }) - .then(function userGroupDataRetrieved(values) { - - $scope.userGroups = {}; - $scope.userGroup = values.userGroups[dataSource]; - $scope.parentGroups = values.parentGroups; - $scope.parentGroupsAdded = values.parentGroups; - $scope.memberGroups = values.memberGroups; - $scope.memberGroupsAdded = values.memberGroups; - $scope.memberUsers = values.memberUsers; - $scope.memberUsersAdded = values.memberUsers; - - $scope.permissionFlags = PermissionFlagSet.fromPermissionSet(values.permissions); - $scope.permissionsAdded = values.permissions; - - }); - }; - - /** - * Loads skeleton user group data, preparing the interface for creating a - * new user group. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared for - * creating a new user group. - */ - var loadSkeletonUserGroup = function loadSkeletonUserGroup() { - - // No user groups exist regardless of data source if the user group is - // being created - $scope.userGroups = {}; - - // Use skeleton user group object with no associated permissions - $scope.userGroup = new UserGroup(); - $scope.parentGroups = []; - $scope.memberGroups = []; - $scope.memberUsers = []; - $scope.permissionFlags = new PermissionFlagSet(); - - return $q.resolve(); - - }; - - /** - * Loads the data required for performing the management task requested - * through the route parameters given at load time, automatically preparing - * the interface for editing an existing user group, cloning an existing - * user group, or creating an entirely new user group. - * - * @returns {Promise} - * A promise which is resolved when the interface has been prepared - * for performing the requested management task. - */ - var loadRequestedUserGroup = function loadRequestedUserGroup() { - - // Pull user group data and permissions if we are editing an existing - // user group - if (identifier) - return loadExistingUserGroup($scope.dataSource, identifier); - - // If we are cloning an existing user group, pull its data instead - if (cloneSourceIdentifier) - return loadClonedUserGroup($scope.dataSource, cloneSourceIdentifier); - - // If we are creating a new user group, populate skeleton user group data - return loadSkeletonUserGroup(); - - }; - - // Populate interface with requested data - $q.all({ - userGroupData : loadRequestedUserGroup(), - permissions : dataSourceService.apply(permissionService.getEffectivePermissions, dataSources, currentUsername), - userGroups : userGroupService.getUserGroups($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]), - users : userService.getUsers($scope.dataSource, [ PermissionSet.ObjectPermissionType.UPDATE ]), - attributes : schemaService.getUserGroupAttributes($scope.dataSource) - }) - .then(function dataReceived(values) { - - $scope.attributes = values.attributes; - - $scope.managementPermissions = {}; - angular.forEach(dataSources, function deriveManagementPermissions(dataSource) { - - // Determine whether data source contains this user group - var exists = (dataSource in $scope.userGroups); - - // Add the identifiers of all modifiable user groups - $scope.availableGroups = []; - angular.forEach(values.userGroups, function addUserGroupIdentifier(userGroup) { - $scope.availableGroups.push(userGroup.identifier); - }); - - // Add the identifiers of all modifiable users - $scope.availableUsers = []; - angular.forEach(values.users, function addUserIdentifier(user) { - $scope.availableUsers.push(user.username); - }); - - // Calculate management actions available for this specific group - $scope.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( - values.permissions[dataSource], - PermissionSet.SystemPermissionType.CREATE_USER_GROUP, - PermissionSet.hasUserGroupPermission, - exists ? identifier : null); - - }); - - }, requestService.WARN); - - /** - * Returns the URL for the page which manages the user group currently - * being edited under the given data source. The given data source need not - * be the same as the data source currently selected. - * - * @param {String} dataSource - * The unique identifier of the data source that the URL is being - * generated for. - * - * @returns {String} - * The URL for the page which manages the user group currently being - * edited under the given data source. - */ - $scope.getUserGroupURL = function getUserGroupURL(dataSource) { - return '/manage/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier || ''); - }; - - /** - * Cancels all pending edits, returning to the main list of user groups. - */ - $scope.returnToUserGroupList = function returnToUserGroupList() { - $location.url('/settings/userGroups'); - }; - - /** - * Cancels all pending edits, opening an edit page for a new user group - * which is prepopulated with the data from the user currently being edited. - */ - $scope.cloneUserGroup = function cloneUserGroup() { - $location.path('/manage/' + encodeURIComponent($scope.dataSource) + '/userGroups').search('clone', identifier); - $window.scrollTo(0,0); - }; - - /** - * Saves the current user group, creating a new user group or updating the - * existing user group depending on context, returning a promise which is - * resolved if the save operation succeeds and rejected if the save - * operation fails. - * - * @returns {Promise} - * A promise which is resolved if the save operation succeeds and is - * rejected with an {@link Error} if the save operation fails. - */ - $scope.saveUserGroup = function saveUserGroup() { - - // Save or create the user group, depending on whether the user group exists - var saveUserGroupPromise; - if ($scope.dataSource in $scope.userGroups) - saveUserGroupPromise = userGroupService.saveUserGroup($scope.dataSource, $scope.userGroup); - else - saveUserGroupPromise = userGroupService.createUserGroup($scope.dataSource, $scope.userGroup); - - return saveUserGroupPromise.then(function savedUserGroup() { - return $q.all([ - permissionService.patchPermissions($scope.dataSource, $scope.userGroup.identifier, $scope.permissionsAdded, $scope.permissionsRemoved, true), - membershipService.patchUserGroups($scope.dataSource, $scope.userGroup.identifier, $scope.parentGroupsAdded, $scope.parentGroupsRemoved, true), - membershipService.patchMemberUserGroups($scope.dataSource, $scope.userGroup.identifier, $scope.memberGroupsAdded, $scope.memberGroupsRemoved), - membershipService.patchMemberUsers($scope.dataSource, $scope.userGroup.identifier, $scope.memberUsersAdded, $scope.memberUsersRemoved) - ]); - }); - - }; - - /** - * Deletes the current user group, returning a promise which is resolved if - * the delete operation succeeds and rejected if the delete operation - * fails. - * - * @returns {Promise} - * A promise which is resolved if the delete operation succeeds and is - * rejected with an {@link Error} if the delete operation fails. - */ - $scope.deleteUserGroup = function deleteUserGroup() { - return userGroupService.deleteUserGroup($scope.dataSource, $scope.userGroup); - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/directives/connectionPermissionEditor.js b/guacamole/src/main/frontend/src/app/manage/directives/connectionPermissionEditor.js deleted file mode 100644 index 9392bb0fc2..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/connectionPermissionEditor.js +++ /dev/null @@ -1,557 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for manipulating the connection permissions granted within a - * given {@link PermissionFlagSet}, tracking the specific permissions added or - * removed within a separate pair of {@link PermissionSet} objects. - */ -angular.module('manage').directive('connectionPermissionEditor', ['$injector', - function connectionPermissionEditor($injector) { - - // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var GroupListItem = $injector.get('GroupListItem'); - var PermissionSet = $injector.get('PermissionSet'); - - // Required services - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var requestService = $injector.get('requestService'); - - var directive = { - - // Element only - restrict: 'E', - replace: true, - - scope: { - - /** - * The unique identifier of the data source associated with the - * permissions being manipulated. - * - * @type String - */ - dataSource : '=', - - /** - * The current state of the permissions being manipulated. This - * {@link PemissionFlagSet} will be modified as changes are made - * through this permission editor. - * - * @type PermissionFlagSet - */ - permissionFlags : '=', - - /** - * The set of permissions that have been added, relative to the - * initial state of the permissions being manipulated. - * - * @type PermissionSet - */ - permissionsAdded : '=', - - /** - * The set of permissions that have been removed, relative to the - * initial state of the permissions being manipulated. - * - * @type PermissionSet - */ - permissionsRemoved : '=' - - }, - - templateUrl: 'app/manage/templates/connectionPermissionEditor.html' - - }; - - directive.controller = ['$scope', function connectionPermissionEditorController($scope) { - - /** - * A map of data source identifiers to all root connection groups - * within those data sources, regardless of the permissions granted for - * the items within those groups. As only one data source is applicable - * to any particular permission set being edited/created, this will only - * contain a single key. If the data necessary to produce this map has - * not yet been loaded, this will be null. - * - * @type Object. - */ - var allRootGroups = null; - - /** - * A map of data source identifiers to the root connection groups within - * those data sources, excluding all items which are not explicitly - * readable according to $scope.permissionFlags. As only one data - * source is applicable to any particular permission set being - * edited/created, this will only contain a single key. If the data - * necessary to produce this map has not yet been loaded, this will be - * null. - * - * @type Object. - */ - var readableRootGroups = null; - - /** - * The name of the tab within the connection permission editor which - * displays currently selected (readable) connections only. - * - * @constant - * @type String - */ - var CURRENT_CONNECTIONS = 'CURRENT_CONNECTIONS'; - - /** - * The name of the tab within the connection permission editor which - * displays all connections, regardless of whether they are readable. - * - * @constant - * @type String - */ - var ALL_CONNECTIONS = 'ALL_CONNECTIONS'; - - /** - * The names of all tabs which should be available within the - * connection permission editor, in display order. - * - * @type String[] - */ - $scope.tabs = [ - CURRENT_CONNECTIONS, - ALL_CONNECTIONS - ]; - - /** - * The name of the currently selected tab. - * - * @type String - */ - $scope.currentTab = ALL_CONNECTIONS; - - /** - * Array of all connection properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionProperties = [ - 'name', - 'protocol' - ]; - - /** - * Array of all connection group properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionGroupProperties = [ - 'name' - ]; - - /** - * Returns the root groups which should be displayed within the - * connection permission editor. - * - * @returns {Object.} - * The root groups which should be displayed within the connection - * permission editor as a map of data source identifiers to the - * root connection groups within those data sources. - */ - $scope.getRootGroups = function getRootGroups() { - return $scope.currentTab === CURRENT_CONNECTIONS ? readableRootGroups : allRootGroups; - }; - - /** - * Returns whether the given PermissionFlagSet declares explicit READ - * permission for the connection, connection group, or sharing profile - * represented by the given GroupListItem. - * - * @param {GroupListItem} item - * The GroupListItem which should be checked against the - * PermissionFlagSet. - * - * @param {PemissionFlagSet} flags - * The set of permissions which should be used to determine whether - * explicit READ permission is granted for the given item. - * - * @returns {Boolean} - * true if explicit READ permission is granted for the given item - * according to the given permission set, false otherwise. - */ - var isReadable = function isReadable(item, flags) { - - switch (item.type) { - - case GroupListItem.Type.CONNECTION: - return flags.connectionPermissions.READ[item.identifier]; - - case GroupListItem.Type.CONNECTION_GROUP: - return flags.connectionGroupPermissions.READ[item.identifier]; - - case GroupListItem.Type.SHARING_PROFILE: - return flags.sharingProfilePermissions.READ[item.identifier]; - - } - - return false; - - }; - - /** - * Expands all items within the tree descending from the given - * GroupListItem which have at least one descendant for which explicit - * READ permission is granted. The expanded state of all other items is - * left untouched. - * - * @param {GroupListItem} item - * The GroupListItem which should be conditionally expanded - * depending on whether READ permission is granted for any of its - * descendants. - * - * @param {PemissionFlagSet} flags - * The set of permissions which should be used to determine whether - * the given item and its descendants are expanded. - * - * @returns {Boolean} - * true if the given item has been expanded, false otherwise. - */ - var expandReadable = function expandReadable(item, flags) { - - // If the current item is expandable and has defined children, - // determine whether it should be expanded - if (item.expandable && item.children) { - angular.forEach(item.children, function expandReadableChild(child) { - - // The parent should be expanded by default if the child is - // expanded by default OR the permission set contains READ - // permission on the child - item.expanded |= expandReadable(child, flags) || isReadable(child, flags); - - }); - } - - return item.expanded; - - }; - - /** - * Creates a deep copy of all items within the tree descending from the - * given GroupListItem which have at least one descendant for which - * explicit READ permission is granted. Items which lack explicit READ - * permission and which have no descendants having explicit READ - * permission are omitted from the copy. - * - * @param {GroupListItem} item - * The GroupListItem which should be conditionally copied - * depending on whether READ permission is granted for any of its - * descendants. - * - * @param {PemissionFlagSet} flags - * The set of permissions which should be used to determine whether - * the given item or any of its descendants are copied. - * - * @returns {GroupListItem} - * A new GroupListItem containing a deep copy of the given item, - * omitting any items which lack explicit READ permission and whose - * descendants also lack explicit READ permission, or null if even - * the given item would not be copied. - */ - var copyReadable = function copyReadable(item, flags) { - - // Produce initial shallow copy of given item - item = new GroupListItem(item); - - // Replace children array with an array containing only readable - // children (or children with at least one readable descendant), - // flagging the current item for copying if any such children exist - if (item.children) { - - var children = []; - angular.forEach(item.children, function copyReadableChildren(child) { - - // Reduce child tree to only explicitly readable items and - // their parents - child = copyReadable(child, flags); - - // Include child only if they are explicitly readable, they - // have explicitly readable descendants, or their parent is - // readable (and thus all children are relevant) - if ((child.children && child.children.length) - || isReadable(item, flags) - || isReadable(child, flags)) - children.push(child); - - }); - - item.children = children; - - } - - return item; - - }; - - // Retrieve all connections for which we have ADMINISTER permission - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - [$scope.dataSource], - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.ADMINISTER] - ) - .then(function connectionGroupReceived(rootGroups) { - - // Update default expanded state and the all / readable-only views - // when associated permissions change - $scope.$watchGroup(['permissionFlags'], function updateDefaultExpandedStates() { - - if (!$scope.permissionFlags) - return; - - allRootGroups = {}; - readableRootGroups = {}; - - angular.forEach(rootGroups, function addGroupListItem(rootGroup, dataSource) { - - // Convert all received ConnectionGroup objects into GroupListItems - var item = GroupListItem.fromConnectionGroup(dataSource, rootGroup); - allRootGroups[dataSource] = item; - - // Automatically expand all objects with any descendants for - // which the permission set contains READ permission - expandReadable(item, $scope.permissionFlags); - - // Create a duplicate view which contains only readable - // items - readableRootGroups[dataSource] = copyReadable(item, $scope.permissionFlags); - - }); - - // Display only readable connections by default if at least one - // readable connection exists - $scope.currentTab = !!readableRootGroups[$scope.dataSource].children.length ? CURRENT_CONNECTIONS : ALL_CONNECTIONS; - - }); - - }, requestService.DIE); - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the addition of the given connection permission. - * - * @param {String} identifier - * The identifier of the connection to add READ permission for. - */ - var addConnectionPermission = function addConnectionPermission(identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given connection permission. - * - * @param {String} identifier - * The identifier of the connection to remove READ permission for. - */ - var removeConnectionPermission = function removeConnectionPermission(identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addConnectionPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the addition of the given connection group permission. - * - * @param {String} identifier - * The identifier of the connection group to add READ permission - * for. - */ - var addConnectionGroupPermission = function addConnectionGroupPermission(identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given connection group permission. - * - * @param {String} identifier - * The identifier of the connection group to remove READ permission - * for. - */ - var removeConnectionGroupPermission = function removeConnectionGroupPermission(identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeConnectionGroupPermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addConnectionGroupPermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the addition of the given sharing profile permission. - * - * @param {String} identifier - * The identifier of the sharing profile to add READ permission for. - */ - var addSharingProfilePermission = function addSharingProfilePermission(identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasSharingProfilePermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeSharingProfilePermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addSharingProfilePermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given sharing profile permission. - * - * @param {String} identifier - * The identifier of the sharing profile to remove READ permission - * for. - */ - var removeSharingProfilePermission = function removeSharingProfilePermission(identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasSharingProfilePermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) - PermissionSet.removeSharingProfilePermission($scope.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addSharingProfilePermission($scope.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); - - }; - - // Expose permission query and modification functions to group list template - $scope.groupListContext = { - - /** - * Returns the PermissionFlagSet that contains the current state of - * granted permissions. - * - * @returns {PermissionFlagSet} - * The PermissionFlagSet describing the current state of granted - * permissions for the permission set being edited. - */ - getPermissionFlags : function getPermissionFlags() { - return $scope.permissionFlags; - }, - - /** - * Notifies the controller that a change has been made to the given - * connection permission for the permission set being edited. This - * only applies to READ permissions. - * - * @param {String} identifier - * The identifier of the connection affected by the changed - * permission. - */ - connectionPermissionChanged : function connectionPermissionChanged(identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.connectionPermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addConnectionPermission(identifier); - else - removeConnectionPermission(identifier); - - }, - - /** - * Notifies the controller that a change has been made to the given - * connection group permission for the permission set being edited. - * This only applies to READ permissions. - * - * @param {String} identifier - * The identifier of the connection group affected by the - * changed permission. - */ - connectionGroupPermissionChanged : function connectionGroupPermissionChanged(identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.connectionGroupPermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addConnectionGroupPermission(identifier); - else - removeConnectionGroupPermission(identifier); - - }, - - /** - * Notifies the controller that a change has been made to the given - * sharing profile permission for the permission set being edited. - * This only applies to READ permissions. - * - * @param {String} identifier - * The identifier of the sharing profile affected by the changed - * permission. - */ - sharingProfilePermissionChanged : function sharingProfilePermissionChanged(identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.sharingProfilePermissions.READ[identifier]; - - // Add/remove permission depending on flag state - if (granted) - addSharingProfilePermission(identifier); - else - removeSharingProfilePermission(identifier); - - } - - }; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/directives/dataSourceTabs.js b/guacamole/src/main/frontend/src/app/manage/directives/dataSourceTabs.js deleted file mode 100644 index 627197bf4f..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/dataSourceTabs.js +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Directive which displays a set of tabs pointing to the same object within - * different data sources, such as user accounts which span multiple data - * sources. - */ -angular.module('manage').directive('dataSourceTabs', ['$injector', - function dataSourceTabs($injector) { - - // Required types - var PageDefinition = $injector.get('PageDefinition'); - - // Required services - var translationStringService = $injector.get('translationStringService'); - - var directive = { - - restrict : 'E', - replace : true, - templateUrl : 'app/manage/templates/dataSourceTabs.html', - - scope : { - - /** - * The permissions which dictate the management actions available - * to the current user. - * - * @type Object. - */ - permissions : '=', - - /** - * A function which returns the URL of the object within a given - * data source. The relevant data source will be made available to - * the Angular expression defining this function as the - * "dataSource" variable. No other values will be made available, - * including values from the scope. - * - * @type Function - */ - url : '&' - - } - - }; - - directive.controller = ['$scope', function dataSourceTabsController($scope) { - - /** - * The set of pages which each manage the same object within different - * data sources. - * - * @type PageDefinition[] - */ - $scope.pages = null; - - // Keep pages synchronized with permissions - $scope.$watch('permissions', function permissionsChanged(permissions) { - - $scope.pages = []; - - var dataSources = _.keys($scope.permissions).sort(); - angular.forEach(dataSources, function addDataSourcePage(dataSource) { - - // Determine whether data source contains this object - var managementPermissions = permissions[dataSource]; - var exists = !!managementPermissions.identifier; - - // Data source is not relevant if the associated object does not - // exist and cannot be created - var readOnly = !managementPermissions.canSaveObject; - if (!exists && readOnly) - return; - - // Determine class name based on read-only / linked status - var className; - if (readOnly) className = 'read-only'; - else if (exists) className = 'linked'; - else className = 'unlinked'; - - // Add page entry - $scope.pages.push(new PageDefinition({ - name : translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', - url : $scope.url({ dataSource : dataSource }), - className : className - })); - - }); - - }); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/directives/identifierSetEditor.js b/guacamole/src/main/frontend/src/app/manage/directives/identifierSetEditor.js deleted file mode 100644 index d2936e7b9c..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/identifierSetEditor.js +++ /dev/null @@ -1,300 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for manipulating a set of objects sharing some common relation - * and represented by an array of their identifiers. The specific objects - * added or removed are tracked within a separate pair of arrays of - * identifiers. - */ -angular.module('manage').directive('identifierSetEditor', ['$injector', - function identifierSetEditor($injector) { - - var directive = { - - // Element only - restrict: 'E', - replace: true, - - scope: { - - /** - * The translation key of the text which should be displayed within - * the main header of the identifier set editor. - * - * @type String - */ - header : '@', - - /** - * The translation key of the text which should be displayed if no - * identifiers are currently present within the set. - * - * @type String - */ - emptyPlaceholder : '@', - - /** - * The translation key of the text which should be displayed if no - * identifiers are available to be added within the set. - * - * @type String - */ - unavailablePlaceholder : '@', - - /** - * All identifiers which are available to be added to or removed - * from the identifier set being edited. - * - * @type String[] - */ - identifiersAvailable : '=', - - /** - * The current state of the identifier set being manipulated. This - * array will be modified as changes are made through this - * identifier set editor. - * - * @type String[] - */ - identifiers : '=', - - /** - * The set of identifiers that have been added, relative to the - * initial state of the identifier set being manipulated. - * - * @type String[] - */ - identifiersAdded : '=', - - /** - * The set of identifiers that have been removed, relative to the - * initial state of the identifier set being manipulated. - * - * @type String[] - */ - identifiersRemoved : '=' - - }, - - templateUrl: 'app/manage/templates/identifierSetEditor.html' - - }; - - directive.controller = ['$scope', function identifierSetEditorController($scope) { - - /** - * Whether the full list of available identifiers should be displayed. - * Initially, only an abbreviated list of identifiers currently present - * is shown. - * - * @type Boolean - */ - $scope.expanded = false; - - /** - * Map of identifiers to boolean flags indicating whether that - * identifier is currently present (true) or absent (false). If an - * identifier is absent, it may also be absent from this map. - * - * @type Object. - */ - $scope.identifierFlags = {}; - - /** - * Map of identifiers to boolean flags indicating whether that - * identifier is editable. If an identifier is not editable, it will be - * absent from this map. - * - * @type Object. - */ - $scope.isEditable = {}; - - /** - * Adds the given identifier to the given sorted array of identifiers, - * preserving the sorted order of the array. If the identifier is - * already present, no change is made to the array. The given array - * must already be sorted in ascending order. - * - * @param {String[]} arr - * The sorted array of identifiers to add the given identifier to. - * - * @param {String} identifier - * The identifier to add to the given array. - */ - var addIdentifier = function addIdentifier(arr, identifier) { - - // Determine location that the identifier should be added to - // maintain sorted order - var index = _.sortedIndex(arr, identifier); - - // Do not add if already present - if (arr[index] === identifier) - return; - - // Insert identifier at determined location - arr.splice(index, 0, identifier); - - }; - - /** - * Removes the given identifier from the given sorted array of - * identifiers, preserving the sorted order of the array. If the - * identifier is already absent, no change is made to the array. The - * given array must already be sorted in ascending order. - * - * @param {String[]} arr - * The sorted array of identifiers to remove the given identifier - * from. - * - * @param {String} identifier - * The identifier to remove from the given array. - * - * @returns {Boolean} - * true if the identifier was present in the given array and has - * been removed, false otherwise. - */ - var removeIdentifier = function removeIdentifier(arr, identifier) { - - // Search for identifier in sorted array - var index = _.sortedIndexOf(arr, identifier); - - // Nothing to do if already absent - if (index === -1) - return false; - - // Remove identifier - arr.splice(index, 1); - return true; - - }; - - // Keep identifierFlags up to date when identifiers array is replaced - // or initially assigned - $scope.$watch('identifiers', function identifiersChanged(identifiers) { - - // Maintain identifiers in sorted order so additions and removals - // can be made more efficiently - if (identifiers) - identifiers.sort(); - - // Convert array of identifiers into set of boolean - // presence/absence flags - $scope.identifierFlags = {}; - angular.forEach(identifiers, function storeIdentifierFlag(identifier) { - $scope.identifierFlags[identifier] = true; - }); - - }); - - // An identifier is editable iff it is available to be added or removed - // from the identifier set being edited (iff it is within the - // identifiersAvailable array) - $scope.$watch('identifiersAvailable', function availableIdentifiersChanged(identifiers) { - $scope.isEditable = {}; - angular.forEach(identifiers, function storeEditableIdentifier(identifier) { - $scope.isEditable[identifier] = true; - }); - }); - - /** - * Notifies the controller that a change has been made to the flag - * denoting presence/absence of a particular identifier within the - * identifierFlags map. The identifiers, - * identifiersAdded, and identifiersRemoved - * arrays are updated accordingly. - * - * @param {String} identifier - * The identifier which has been added or removed through modifying - * its boolean flag within identifierFlags. - */ - $scope.identifierChanged = function identifierChanged(identifier) { - - // Determine status of modified identifier - var present = !!$scope.identifierFlags[identifier]; - - // Add/remove identifier from added/removed sets depending on - // change in flag state - if (present) { - - addIdentifier($scope.identifiers, identifier); - - if (!removeIdentifier($scope.identifiersRemoved, identifier)) - addIdentifier($scope.identifiersAdded, identifier); - - } - else { - - removeIdentifier($scope.identifiers, identifier); - - if (!removeIdentifier($scope.identifiersAdded, identifier)) - addIdentifier($scope.identifiersRemoved, identifier); - - } - - }; - - /** - * Removes the given identifier, updating identifierFlags, - * identifiers, identifiersAdded, and - * identifiersRemoved accordingly. - * - * @param {String} identifier - * The identifier to remove. - */ - $scope.removeIdentifier = function removeIdentifier(identifier) { - $scope.identifierFlags[identifier] = false; - $scope.identifierChanged(identifier); - }; - - /** - * Shows the full list of available identifiers. If the full list is - * already shown, this function has no effect. - */ - $scope.expand = function expand() { - $scope.expanded = true; - }; - - /** - * Hides the full list of available identifiers. If the full list is - * already hidden, this function has no effect. - */ - $scope.collapse = function collapse() { - $scope.expanded = false; - }; - - /** - * Returns whether there are absolutely no identifiers that can be - * managed using this editor. If true, the editor is effectively - * useless, as there is nothing whatsoever to display. - * - * @returns {Boolean} - * true if there are no identifiers that can be managed using this - * editor, false otherwise. - */ - $scope.isEmpty = function isEmpty() { - return _.isEmpty($scope.identifiers) - && _.isEmpty($scope.identifiersAvailable); - }; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/directives/locationChooser.js b/guacamole/src/main/frontend/src/app/manage/directives/locationChooser.js deleted file mode 100644 index d91c5d4932..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/locationChooser.js +++ /dev/null @@ -1,169 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - -/** - * A directive for choosing the location of a connection or connection group. - */ -angular.module('manage').directive('locationChooser', [function locationChooser() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: { - - /** - * The identifier of the data source from which the given root - * connection group was retrieved. - * - * @type String - */ - dataSource : '=', - - /** - * The root connection group of the connection group hierarchy to - * display. - * - * @type ConnectionGroup - */ - rootGroup : '=', - - /** - * The unique identifier of the currently-selected connection - * group. If not specified, the root group will be used. - * - * @type String - */ - value : '=' - - }, - - templateUrl: 'app/manage/templates/locationChooser.html', - controller: ['$scope', function locationChooserController($scope) { - - /** - * Map of unique identifiers to their corresponding connection - * groups. - * - * @type Object. - */ - var connectionGroups = {}; - - /** - * Recursively traverses the given connection group and all - * children, storing each encountered connection group within the - * connectionGroups map by its identifier. - * - * @param {GroupListItem} group - * The connection group to traverse. - */ - var mapConnectionGroups = function mapConnectionGroups(group) { - - // Map given group - connectionGroups[group.identifier] = group; - - // Map all child groups - if (group.childConnectionGroups) - group.childConnectionGroups.forEach(mapConnectionGroups); - - }; - - /** - * Whether the group list menu is currently open. - * - * @type Boolean - */ - $scope.menuOpen = false; - - /** - * The human-readable name of the currently-chosen connection - * group. - * - * @type String - */ - $scope.chosenConnectionGroupName = null; - - /** - * Toggle the current state of the menu listing connection groups. - * If the menu is currently open, it will be closed. If currently - * closed, it will be opened. - */ - $scope.toggleMenu = function toggleMenu() { - $scope.menuOpen = !$scope.menuOpen; - }; - - // Update the root group map when data source or root group change - $scope.$watchGroup(['dataSource', 'rootGroup'], function updateRootGroups() { - - // Abort if the root group is not set - if (!$scope.dataSource || !$scope.rootGroup) - return null; - - // Wrap root group in map - $scope.rootGroups = {}; - $scope.rootGroups[$scope.dataSource] = $scope.rootGroup; - - }); - - // Expose selection function to group list template - $scope.groupListContext = { - - /** - * Selects the given group item. - * - * @param {GroupListItem} item - * The chosen item. - */ - chooseGroup : function chooseGroup(item) { - - // Record new parent - $scope.value = item.identifier; - $scope.chosenConnectionGroupName = item.name; - - // Close menu - $scope.menuOpen = false; - - } - - }; - - $scope.$watch('rootGroup', function setRootGroup(rootGroup) { - - connectionGroups = {}; - - if (!rootGroup) - return; - - // Map all known groups - mapConnectionGroups(rootGroup); - - // If no value is specified, default to the root identifier - if (!$scope.value || !($scope.value in connectionGroups)) - $scope.value = rootGroup.identifier; - - $scope.chosenConnectionGroupName = connectionGroups[$scope.value].name; - - }); - - }] - }; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/directives/managementButtons.js b/guacamole/src/main/frontend/src/app/manage/directives/managementButtons.js deleted file mode 100644 index f44515aa6e..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/managementButtons.js +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Directive which displays a set of object management buttons (save, delete, - * clone, etc.) representing the actions available to the current user in - * context of the object being edited/created. - */ -angular.module('manage').directive('managementButtons', ['$injector', - function managementButtons($injector) { - - // Required services - var guacNotification = $injector.get('guacNotification'); - - var directive = { - - restrict : 'E', - replace : true, - templateUrl : 'app/manage/templates/managementButtons.html', - - scope : { - - /** - * The translation namespace associated with all applicable - * translation strings. This directive requires at least the - * following translation strings within the given namespace: - * - * - ACTION_CANCEL - * - ACTION_CLONE - * - ACTION_DELETE - * - ACTION_SAVE - * - DIALOG_HEADER_CONFIRM_DELETE - * - TEXT_CONFIRM_DELETE - * - * @type String - */ - namespace : '@', - - /** - * The permissions which dictate the management actions available - * to the current user. - * - * @type ManagementPermissions - */ - permissions : '=', - - /** - * The function to invoke to save the arbitrary object being edited - * if the current user has permission to do so. The provided - * function MUST return a promise which is resolved if the save - * operation succeeds and is rejected with an {@link Error} if the - * save operation fails. - * - * @type Function - */ - save : '&', - - /** - * The function to invoke when the current user chooses to clone - * the object being edited. The provided function MUST perform the - * actions necessary to produce an interface which will clone the - * object. - * - * @type Function - */ - clone : '&', - - /** - * The function to invoke to delete the arbitrary object being edited - * if the current user has permission to do so. The provided - * function MUST return a promise which is resolved if the delete - * operation succeeds and is rejected with an {@link Error} if the - * delete operation fails. - * - * @type Function - */ - delete : '&', - - /** - * The function to invoke when the current user chooses to cancel - * the edit in progress, or when a save/delete operation has - * succeeded. The provided function MUST perform the actions - * necessary to return the user to a reasonable starting point. - * - * @type Function - */ - return : '&' - - } - - }; - - directive.controller = ['$scope', function managementButtonsController($scope) { - - /** - * An action to be provided along with the object sent to showStatus which - * immediately deletes the current connection. - */ - var DELETE_ACTION = { - name : $scope.namespace + '.ACTION_DELETE', - className : 'danger', - callback : function deleteCallback() { - deleteObjectImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * An action to be provided along with the object sent to showStatus which - * closes the currently-shown status dialog. - */ - var CANCEL_ACTION = { - name : $scope.namespace + '.ACTION_CANCEL', - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * Invokes the provided return function to navigate the user back to - * the page they started from. - */ - var navigateBack = function navigateBack() { - $scope['return']($scope.$parent); - }; - - /** - * Invokes the provided delete function, immediately deleting the - * current object without prompting the user for confirmation. If - * deletion is successful, the user is navigated back to the page they - * started from. If the deletion fails, an error notification is - * displayed. - */ - var deleteObjectImmediately = function deleteObjectImmediately() { - $scope['delete']($scope.$parent).then(navigateBack, guacNotification.SHOW_REQUEST_ERROR); - }; - - /** - * Cancels all pending edits, returning to the page the user started - * from. - */ - $scope.cancel = navigateBack; - - /** - * Cancels all pending edits, invoking the provided clone function to - * open an edit page for a new object which is prepopulated with the - * data from the current object. - */ - $scope.cloneObject = function cloneObject () { - $scope.clone($scope.$parent); - }; - - /** - * Invokes the provided save function to save the current object. If - * saving is successful, the user is navigated back to the page they - * started from. If saving fails, an error notification is displayed. - */ - $scope.saveObject = function saveObject() { - $scope.save($scope.$parent).then(navigateBack, guacNotification.SHOW_REQUEST_ERROR); - }; - - /** - * Deletes the current object, prompting the user first to confirm that - * deletion is desired. If the user confirms that deletion is desired, - * the object is deleted through invoking the provided delete function. - * The user is automatically navigated back to the page they started - * from or given an error notification depending on whether deletion - * succeeds. - */ - $scope.deleteObject = function deleteObject() { - - // Confirm deletion request - guacNotification.showStatus({ - title : $scope.namespace + '.DIALOG_HEADER_CONFIRM_DELETE', - text : { key : $scope.namespace + '.TEXT_CONFIRM_DELETE' }, - actions : [ DELETE_ACTION, CANCEL_ACTION] - }); - - }; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/directives/systemPermissionEditor.js b/guacamole/src/main/frontend/src/app/manage/directives/systemPermissionEditor.js deleted file mode 100644 index 079c0d03ff..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/directives/systemPermissionEditor.js +++ /dev/null @@ -1,316 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for manipulating the system permissions granted within a given - * {@link PermissionFlagSet}, tracking the specific permissions added or - * removed within a separate pair of {@link PermissionSet} objects. Optionally, - * the permission for a particular user to update themselves (change their own - * password/attributes) may also be manipulated. - */ -angular.module('manage').directive('systemPermissionEditor', ['$injector', - function systemPermissionEditor($injector) { - - // Required services - var authenticationService = $injector.get('authenticationService'); - var dataSourceService = $injector.get('dataSourceService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - - // Required types - var PermissionSet = $injector.get('PermissionSet'); - - var directive = { - - // Element only - restrict: 'E', - replace: true, - - scope: { - - /** - * The unique identifier of the data source associated with the - * permissions being manipulated. - * - * @type String - */ - dataSource : '=', - - /** - * The username of the user whose self-update permission (whether - * the user has permission to update their own user account) should - * be additionally controlled by this editor. If no such user - * permissions should be controlled, this should be left undefined. - * - * @type String - */ - username : '=', - - /** - * The current state of the permissions being manipulated. This - * {@link PemissionFlagSet} will be modified as changes are made - * through this permission editor. - * - * @type PermissionFlagSet - */ - permissionFlags : '=', - - /** - * The set of permissions that have been added, relative to the - * initial state of the permissions being manipulated. - * - * @type PermissionSet - */ - permissionsAdded : '=', - - /** - * The set of permissions that have been removed, relative to the - * initial state of the permissions being manipulated. - * - * @type PermissionSet - */ - permissionsRemoved : '=' - - }, - - templateUrl: 'app/manage/templates/systemPermissionEditor.html' - - }; - - directive.controller = ['$scope', function systemPermissionEditorController($scope) { - - /** - * The identifiers of all data sources currently available to the - * authenticated user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * The username of the current, authenticated user. - * - * @type String - */ - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * Available system permission types, as translation string / internal - * value pairs. - * - * @type Object[] - */ - $scope.systemPermissionTypes = [ - { - label: "MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - value: PermissionSet.SystemPermissionType.ADMINISTER - }, - { - label: "MANAGE_USER.FIELD_HEADER_AUDIT_SYSTEM", - value: PermissionSet.SystemPermissionType.AUDIT - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - value: PermissionSet.SystemPermissionType.CREATE_USER - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - value: PermissionSet.SystemPermissionType.CREATE_USER_GROUP - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - value: PermissionSet.SystemPermissionType.CREATE_CONNECTION - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - value: PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP - }, - { - label: "MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", - value: PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE - } - ]; - - // Query the permissions granted to the currently-authenticated user - dataSourceService.apply( - permissionService.getEffectivePermissions, - dataSources, - currentUsername - ) - .then(function permissionsReceived(permissions) { - $scope.permissions = permissions; - }, requestService.DIE); - - /** - * Returns whether the current user has permission to change the system - * permissions granted to users. - * - * @returns {Boolean} - * true if the current user can grant or revoke system permissions - * to the permission set being edited, false otherwise. - */ - $scope.canChangeSystemPermissions = function canChangeSystemPermissions() { - - // Do not check if permissions are not yet loaded - if (!$scope.permissions) - return false; - - // Only the administrator can modify system permissions - return PermissionSet.hasSystemPermission($scope.permissions[$scope.dataSource], - PermissionSet.SystemPermissionType.ADMINISTER); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the addition of the given system permission. - * - * @param {String} type - * The system permission to add, as defined by - * PermissionSet.SystemPermissionType. - */ - var addSystemPermission = function addSystemPermission(type) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasSystemPermission($scope.permissionsRemoved, type)) - PermissionSet.removeSystemPermission($scope.permissionsRemoved, type); - - // Otherwise, explicitly add the permission - else - PermissionSet.addSystemPermission($scope.permissionsAdded, type); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given system permission. - * - * @param {String} type - * The system permission to remove, as defined by - * PermissionSet.SystemPermissionType. - */ - var removeSystemPermission = function removeSystemPermission(type) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasSystemPermission($scope.permissionsAdded, type)) - PermissionSet.removeSystemPermission($scope.permissionsAdded, type); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addSystemPermission($scope.permissionsRemoved, type); - - }; - - /** - * Notifies the controller that a change has been made to the given - * system permission for the permission set being edited. - * - * @param {String} type - * The system permission that was changed, as defined by - * PermissionSet.SystemPermissionType. - */ - $scope.systemPermissionChanged = function systemPermissionChanged(type) { - - // Determine current permission setting - var granted = $scope.permissionFlags.systemPermissions[type]; - - // Add/remove permission depending on flag state - if (granted) - addSystemPermission(type); - else - removeSystemPermission(type); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the addition of the given user permission. - * - * @param {String} type - * The user permission to add, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the permission being added. - */ - var addUserPermission = function addUserPermission(type, identifier) { - - // If permission was previously removed, simply un-remove it - if (PermissionSet.hasUserPermission($scope.permissionsRemoved, type, identifier)) - PermissionSet.removeUserPermission($scope.permissionsRemoved, type, identifier); - - // Otherwise, explicitly add the permission - else - PermissionSet.addUserPermission($scope.permissionsAdded, type, identifier); - - }; - - /** - * Updates the permissionsAdded and permissionsRemoved permission sets - * to reflect the removal of the given user permission. - * - * @param {String} type - * The user permission to remove, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the permission being - * removed. - */ - var removeUserPermission = function removeUserPermission(type, identifier) { - - // If permission was previously added, simply un-add it - if (PermissionSet.hasUserPermission($scope.permissionsAdded, type, identifier)) - PermissionSet.removeUserPermission($scope.permissionsAdded, type, identifier); - - // Otherwise, explicitly remove the permission - else - PermissionSet.addUserPermission($scope.permissionsRemoved, type, identifier); - - }; - - /** - * Notifies the controller that a change has been made to the given user - * permission for the permission set being edited. - * - * @param {String} type - * The user permission that was changed, as defined by - * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier - * The identifier of the user affected by the changed permission. - */ - $scope.userPermissionChanged = function userPermissionChanged(type, identifier) { - - // Determine current permission setting - var granted = $scope.permissionFlags.userPermissions[type][identifier]; - - // Add/remove permission depending on flag state - if (granted) - addUserPermission(type, identifier); - else - removeUserPermission(type, identifier); - - }; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/manage/manageModule.js b/guacamole/src/main/frontend/src/app/manage/manageModule.js deleted file mode 100644 index 3e775dba21..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/manageModule.js +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The module for the administration functionality. - */ -angular.module('manage', [ - 'form', - 'groupList', - 'list', - 'locale', - 'navigation', - 'notification', - 'rest' -]); diff --git a/guacamole/src/main/frontend/src/app/manage/templates/connectionGroupPermission.html b/guacamole/src/main/frontend/src/app/manage/templates/connectionGroupPermission.html deleted file mode 100644 index 2bc90cce9e..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/connectionGroupPermission.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - - -
    - - - - - - {{item.name}} - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/connectionPermission.html b/guacamole/src/main/frontend/src/app/manage/templates/connectionPermission.html deleted file mode 100644 index e94f9114e9..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/connectionPermission.html +++ /dev/null @@ -1,13 +0,0 @@ -
    - - -
    - - - - - - {{item.name}} - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/connectionPermissionEditor.html b/guacamole/src/main/frontend/src/app/manage/templates/connectionPermissionEditor.html deleted file mode 100644 index f010fb9ca4..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/connectionPermissionEditor.html +++ /dev/null @@ -1,22 +0,0 @@ -
    -
    -

    {{'MANAGE_USER.SECTION_HEADER_CONNECTIONS' | translate}}

    - -
    - -
    - -
    -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/templates/dataSourceTabs.html b/guacamole/src/main/frontend/src/app/manage/templates/dataSourceTabs.html deleted file mode 100644 index a8a08431b2..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/dataSourceTabs.html +++ /dev/null @@ -1,3 +0,0 @@ -
    - -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/templates/identifierSetEditor.html b/guacamole/src/main/frontend/src/app/manage/templates/identifierSetEditor.html deleted file mode 100644 index e6a309a781..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/identifierSetEditor.html +++ /dev/null @@ -1,47 +0,0 @@ - \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/templates/locationChooser.html b/guacamole/src/main/frontend/src/app/manage/templates/locationChooser.html deleted file mode 100644 index 068b13b3a6..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/locationChooser.html +++ /dev/null @@ -1,17 +0,0 @@ -
    - - -
    {{chosenConnectionGroupName}}
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/locationChooserConnectionGroup.html b/guacamole/src/main/frontend/src/app/manage/templates/locationChooserConnectionGroup.html deleted file mode 100644 index e49b4c0b94..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/locationChooserConnectionGroup.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{item.name}} - diff --git a/guacamole/src/main/frontend/src/app/manage/templates/manageConnection.html b/guacamole/src/main/frontend/src/app/manage/templates/manageConnection.html deleted file mode 100644 index 7be5d75de0..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/manageConnection.html +++ /dev/null @@ -1,96 +0,0 @@ - -
    - - -
    -

    {{'MANAGE_CONNECTION.SECTION_HEADER_EDIT_CONNECTION' | translate}}

    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    {{'MANAGE_CONNECTION.FIELD_HEADER_NAME' | translate}}
    {{'MANAGE_CONNECTION.FIELD_HEADER_LOCATION' | translate}} - -
    {{'MANAGE_CONNECTION.FIELD_HEADER_PROTOCOL' | translate}} - -
    -
    - - -
    - -
    - - -

    {{'MANAGE_CONNECTION.SECTION_HEADER_PARAMETERS' | translate}}

    -
    - -
    - - - - - - -

    {{'MANAGE_CONNECTION.SECTION_HEADER_HISTORY' | translate}}

    -
    -

    {{'MANAGE_CONNECTION.INFO_CONNECTION_NOT_USED' | translate}}

    - - - - - - - - - - - - - - - - - - - -
    {{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_USERNAME' | translate}}{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_START' | translate}}{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_DURATION' | translate}}{{'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_REMOTEHOST' | translate}}
    {{wrapper.entry.startDate | date:historyDateFormat}}{{wrapper.entry.remoteHost}}
    - - - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/manageConnectionGroup.html b/guacamole/src/main/frontend/src/app/manage/templates/manageConnectionGroup.html deleted file mode 100644 index 712183dc68..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/manageConnectionGroup.html +++ /dev/null @@ -1,56 +0,0 @@ - -
    - - -
    -

    {{'MANAGE_CONNECTION_GROUP.SECTION_HEADER_EDIT_CONNECTION_GROUP' | translate}}

    - -
    -
    - - - - - - - - - - - - - - - - - - - - - - -
    {{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_NAME' | translate}}
    {{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_LOCATION' | translate}} - -
    {{'MANAGE_CONNECTION_GROUP.FIELD_HEADER_TYPE' | translate}} - -
    -
    - - -
    - -
    - - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/manageSharingProfile.html b/guacamole/src/main/frontend/src/app/manage/templates/manageSharingProfile.html deleted file mode 100644 index 6458075c07..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/manageSharingProfile.html +++ /dev/null @@ -1,45 +0,0 @@ -
    - - -
    -

    {{'MANAGE_SHARING_PROFILE.SECTION_HEADER_EDIT_SHARING_PROFILE' | translate}}

    - -
    -
    - - - - - - - - - -
    {{'MANAGE_SHARING_PROFILE.FIELD_HEADER_NAME' | translate}}
    {{'MANAGE_SHARING_PROFILE.FIELD_HEADER_PRIMARY_CONNECTION' | translate}}{{primaryConnection.name}}
    -
    - - -
    - -
    - - -

    {{'MANAGE_SHARING_PROFILE.SECTION_HEADER_PARAMETERS' | translate}}

    -
    - -
    - - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/manageUser.html b/guacamole/src/main/frontend/src/app/manage/templates/manageUser.html deleted file mode 100644 index d5896ecdd5..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/manageUser.html +++ /dev/null @@ -1,93 +0,0 @@ - -
    - - -
    -

    {{'MANAGE_USER.SECTION_HEADER_EDIT_USER' | translate}}

    - -
    - - - - -
    -

    {{'MANAGE_USER.INFO_READ_ONLY' | translate}}

    -
    - - -
    - - -
    - - - - - - - - - - - - - - - - - -
    {{'MANAGE_USER.FIELD_HEADER_USERNAME' | translate}} - - {{user.username}} -
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD' | translate}}
    {{'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | translate}}
    {{'MANAGE_USER.FIELD_HEADER_USER_DISABLED' | translate}}
    -
    - - -
    - -
    - - - - - - - - - - - - - - - - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/manageUserGroup.html b/guacamole/src/main/frontend/src/app/manage/templates/manageUserGroup.html deleted file mode 100644 index c450baab98..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/manageUserGroup.html +++ /dev/null @@ -1,105 +0,0 @@ -
    - - -
    -

    {{'MANAGE_USER_GROUP.SECTION_HEADER_EDIT_USER_GROUP' | translate}}

    - -
    - - - - -
    -

    {{'MANAGE_USER_GROUP.INFO_READ_ONLY' | translate}}

    -
    - - -
    - - -
    - - - - - - - - - -
    {{'MANAGE_USER_GROUP.FIELD_HEADER_USER_GROUP_NAME' | translate}} - - {{userGroup.identifier}} -
    {{'MANAGE_USER_GROUP.FIELD_HEADER_USER_GROUP_DISABLED' | translate}}
    -
    - - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/managementButtons.html b/guacamole/src/main/frontend/src/app/manage/templates/managementButtons.html deleted file mode 100644 index 68e4108cc6..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/managementButtons.html +++ /dev/null @@ -1,6 +0,0 @@ -
    - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/manage/templates/sharingProfilePermission.html b/guacamole/src/main/frontend/src/app/manage/templates/sharingProfilePermission.html deleted file mode 100644 index 0d16472556..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/sharingProfilePermission.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/manage/templates/systemPermissionEditor.html b/guacamole/src/main/frontend/src/app/manage/templates/systemPermissionEditor.html deleted file mode 100644 index aa5ee475ac..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/templates/systemPermissionEditor.html +++ /dev/null @@ -1,18 +0,0 @@ -
    -

    {{'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | translate}}

    -
    - - - - - - - - - -
    {{systemPermissionType.label | translate}}
    {{'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | translate}}
    -
    -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/manage/types/ManageableUser.js b/guacamole/src/main/frontend/src/app/manage/types/ManageableUser.js deleted file mode 100644 index d6c987472d..0000000000 --- a/guacamole/src/main/frontend/src/app/manage/types/ManageableUser.js +++ /dev/null @@ -1,64 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the ManageableUser class. - */ -angular.module('manage').factory('ManageableUser', [function defineManageableUser() { - - /** - * A pairing of an @link{User} with the identifier of its corresponding - * data source. - * - * @constructor - * @param {Object|ManageableUser} template - */ - var ManageableUser = function ManageableUser(template) { - - /** - * The unique identifier of the data source containing this user. - * - * @type String - */ - this.dataSource = template.dataSource; - - /** - * The @link{User} object represented by this ManageableUser and - * contained within the associated data source. - * - * @type User - */ - this.user = template.user; - - /** - * Return true if the underlying user account is disabled, otherwise - * return false. - * - * @returns - * True if the underlying user account is disabled, otherwise false. - */ - this.isDisabled = function isDisabled() { - return template.user.disabled; - }; - - }; - - return ManageableUser; - -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/directives/guacMenu.js b/guacamole/src/main/frontend/src/app/navigation/directives/guacMenu.js deleted file mode 100644 index 230e9028de..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/directives/guacMenu.js +++ /dev/null @@ -1,111 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides an arbitrary menu-style container. The contents - * of the directive are displayed only when the menu is open. - */ -angular.module('navigation').directive('guacMenu', [function guacMenu() { - - return { - restrict: 'E', - transclude: true, - replace: true, - scope: { - - /** - * The string which should be rendered as the menu title. - * - * @type String - */ - menuTitle : '=', - - /** - * Whether the menu should remain open while the user interacts - * with the contents of the menu. By default, the menu will close - * if the user clicks within the menu contents. - * - * @type Boolean - */ - interactive : '=' - - }, - - templateUrl: 'app/navigation/templates/guacMenu.html', - controller: ['$scope', '$injector', '$element', - function guacMenuController($scope, $injector, $element) { - - // Get required services - var $document = $injector.get('$document'); - - /** - * The outermost element of the guacMenu directive. - * - * @type Element - */ - var element = $element[0]; - - /** - * The element containing the menu contents that display when the - * menu is open. - * - * @type Element - */ - var contents = $element.find('.menu-contents')[0]; - - /** - * The main document object. - * - * @type Document - */ - var document = $document[0]; - - /** - * Whether the contents of the menu are currently shown. - * - * @type Boolean - */ - $scope.menuShown = false; - - /** - * Toggles visibility of the menu contents. - */ - $scope.toggleMenu = function toggleMenu() { - $scope.menuShown = !$scope.menuShown; - }; - - // Close menu when user clicks anywhere outside this specific menu - document.body.addEventListener('click', function clickOutsideMenu(e) { - $scope.$apply(function closeMenu() { - if (e.target !== element && !element.contains(e.target)) - $scope.menuShown = false; - }); - }, false); - - // Prevent clicks within menu contents from toggling menu visibility - // if the menu contents are intended to be interactive - contents.addEventListener('click', function clickInsideMenuContents(e) { - if ($scope.interactive) - e.stopPropagation(); - }, false); - - }] // end controller - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/directives/guacPageList.js b/guacamole/src/main/frontend/src/app/navigation/directives/guacPageList.js deleted file mode 100644 index c8eb90a966..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/directives/guacPageList.js +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides a list of links to specific pages. - */ -angular.module('navigation').directive('guacPageList', [function guacPageList() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The array of pages to display. - * - * @type PageDefinition[] - */ - pages : '=' - - }, - - templateUrl: 'app/navigation/templates/guacPageList.html', - controller: ['$scope', '$injector', function guacPageListController($scope, $injector) { - - // Required types - var PageDefinition = $injector.get('PageDefinition'); - - // Required services - var $location = $injector.get('$location'); - - /** - * The URL of the currently-displayed page. - * - * @type String - */ - var currentURL = $location.url(); - - /** - * The names associated with the current page, if the current page - * is known. The value of this property corresponds to the value of - * PageDefinition.name. Though PageDefinition.name may be a String, - * this will always be an Array. - * - * @type String[] - */ - var currentPageName = []; - - /** - * Array of each level of the page list, where a level is defined - * by a mapping of names (translation strings) to the - * PageDefinitions corresponding to those names. - * - * @type Object.[] - */ - $scope.levels = []; - - /** - * Returns the names associated with the given page, in - * hierarchical order. If the page is only associated with a single - * name, and that name is not stored as an array, it will be still - * be returned as an array containing a single item. - * - * @param {PageDefinition} page - * The page to return the names of. - * - * @return {String[]} - * An array of all names associated with the given page, in - * hierarchical order. - */ - var getPageNames = function getPageNames(page) { - - // If already an array, simply return the name - if (angular.isArray(page.name)) - return page.name; - - // Otherwise, transform into array - return [page.name]; - - }; - - /** - * Adds the given PageDefinition to the overall set of pages - * displayed by this guacPageList, automatically updating the - * available levels ($scope.levels) and the contents of those - * levels. - * - * @param {PageDefinition} page - * The PageDefinition to add. - * - * @param {Number} weight - * The sorting weight to use for the page if it does not - * already have an associated weight. - */ - var addPage = function addPage(page, weight) { - - // Pull all names for page - var names = getPageNames(page); - - // Copy the hierarchy of this page into the displayed levels - // as far as is relevant for the currently-displayed page - for (var i = 0; i < names.length; i++) { - - // Create current level, if it doesn't yet exist - var pages = $scope.levels[i]; - if (!pages) - pages = $scope.levels[i] = {}; - - // Get the name at the current level - var name = names[i]; - - // Determine whether this page definition is part of the - // hierarchy containing the current page - var isCurrentPage = (currentPageName[i] === name); - - // Store new page if it doesn't yet exist at this level - if (!pages[name]) { - pages[name] = new PageDefinition({ - name : name, - url : isCurrentPage ? currentURL : page.url, - className : page.className, - weight : page.weight || (weight + i) - }); - } - - // If the name at this level no longer matches the - // hierarchy of the current page, do not go any deeper - if (currentPageName[i] !== name) - break; - - } - - }; - - /** - * Navigate to the given page. - * - * @param {PageDefinition} page - * The page to navigate to. - */ - $scope.navigateToPage = function navigateToPage(page) { - $location.path(page.url); - }; - - /** - * Tests whether the given page is the page currently being viewed. - * - * @param {PageDefinition} page - * The page to test. - * - * @returns {Boolean} - * true if the given page is the current page, false otherwise. - */ - $scope.isCurrentPage = function isCurrentPage(page) { - return currentURL === page.url; - }; - - /** - * Given an arbitrary map of PageDefinitions, returns an array of - * those PageDefinitions, sorted by weight. - * - * @param {Object.<*, PageDefinition>} level - * A map of PageDefinitions with arbitrary keys. The value of - * each key is ignored. - * - * @returns {PageDefinition[]} - * An array of all PageDefinitions in the given map, sorted by - * weight. - */ - $scope.getPages = function getPages(level) { - - var pages = []; - - // Convert contents of level to a flat array of pages - angular.forEach(level, function addPageFromLevel(page) { - pages.push(page); - }); - - // Sort page array by weight - pages.sort(function comparePages(a, b) { - return a.weight - b.weight; - }); - - return pages; - - }; - - // Update page levels whenever pages changes - $scope.$watch('pages', function setPages(pages) { - - // Determine current page name - currentPageName = []; - angular.forEach(pages, function findCurrentPageName(page) { - - // If page is current page, store its names - if ($scope.isCurrentPage(page)) - currentPageName = getPageNames(page); - - }); - - // Reset contents of levels - $scope.levels = []; - - // Add all page definitions - angular.forEach(pages, addPage); - - // Filter to only relevant levels - $scope.levels = $scope.levels.filter(function isRelevant(level) { - - // Determine relevancy by counting the number of pages - var pageCount = 0; - for (var name in level) { - - // Level is relevant if it has two or more pages - if (++pageCount === 2) - return true; - - } - - // Otherwise, the level is not relevant - return false; - - }); - - }); - - }] // end controller - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/directives/guacSectionTabs.js b/guacamole/src/main/frontend/src/app/navigation/directives/guacSectionTabs.js deleted file mode 100644 index 97c87c2e6b..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/directives/guacSectionTabs.js +++ /dev/null @@ -1,143 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Directive which displays a set of tabs dividing a section of a page into - * logical subsections or views. The currently selected tab is communicated - * through assignment to the variable bound to the current - * attribute. No navigation occurs as a result of selecting a tab. - */ -angular.module('navigation').directive('guacSectionTabs', ['$injector', - function guacSectionTabs($injector) { - - // Required services - var translationStringService = $injector.get('translationStringService'); - - var directive = { - - restrict : 'E', - replace : true, - templateUrl : 'app/navigation/templates/guacSectionTabs.html', - - scope : { - - /** - * The translation namespace to use when producing translation - * strings for each tab. Tab translation strings will be of the - * form: - * - * NAMESPACE.SECTION_HEADER_NAME - * - * where NAMESPACE is the namespace provided to this - * attribute and NAME is one of the names within the - * array provided to the tabs attribute and - * transformed via translationStringService.canonicalize(). - */ - namespace : '@', - - /** - * The name of the currently selected tab. This name MUST be one of - * the names present in the array given via the tabs - * attribute. This directive will not automatically choose an - * initially selected tab, and a default value should be manually - * assigned to current to ensure a tab is initially - * selected. - * - * @type String - */ - current : '=', - - /** - * The unique names of all tabs which should be made available, in - * display order. These names will be assigned to the variable - * bound to the current attribute when the current - * tab changes. - * - * @type String[] - */ - tabs : '=' - - } - - }; - - directive.controller = ['$scope', function dataSourceTabsController($scope) { - - /** - * Produces the translation string for the section header representing - * the tab having the given name. The translation string will be of the - * form: - * - * NAMESPACE.SECTION_HEADER_NAME - * - * where NAMESPACE is the namespace provided to the - * directive and NAME is the given name transformed - * via translationStringService.canonicalize(). - * - * @param {String} name - * The name of the tab. - * - * @returns {String} - * The translation string which produces the translated header - * of the tab having the given name. - */ - $scope.getSectionHeader = function getSectionHeader(name) { - - // If no name, then no header - if (!name) - return ''; - - return translationStringService.canonicalize($scope.namespace || 'MISSING_NAMESPACE') - + '.SECTION_HEADER_' + translationStringService.canonicalize(name); - - }; - - /** - * Selects the tab having the given name. The name of the currently - * selected tab will be communicated outside the directive through - * $scope.current. - * - * @param {String} name - * The name of the tab to select. - */ - $scope.selectTab = function selectTab(name) { - $scope.current = name; - }; - - /** - * Returns whether the tab having the given name is currently - * selected. A tab is currently selected if its name is stored within - * $scope.current, as assigned externally or by selectTab(). - * - * @param {String} name - * The name of the tab to test. - * - * @returns {Boolean} - * true if the tab having the given name is currently selected, - * false otherwise. - */ - $scope.isSelected = function isSelected(name) { - return $scope.current === name; - }; - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/directives/guacUserMenu.js b/guacamole/src/main/frontend/src/app/navigation/directives/guacUserMenu.js deleted file mode 100644 index 44a2fb5a7c..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/directives/guacUserMenu.js +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which provides a user-oriented menu containing options for - * navigation and configuration. - */ -angular.module('navigation').directive('guacUserMenu', [function guacUserMenu() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * Optional array of actions which are specific to this particular - * location, as these actions may not be appropriate for other - * locations which contain the user menu. - * - * @type MenuAction[] - */ - localActions : '=' - - }, - - templateUrl: 'app/navigation/templates/guacUserMenu.html', - controller: ['$scope', '$injector', - function guacUserMenuController($scope, $injector) { - - // Required types - var User = $injector.get('User'); - - // Get required services - var $location = $injector.get('$location'); - var $route = $injector.get('$route'); - var authenticationService = $injector.get('authenticationService'); - var requestService = $injector.get('requestService'); - var userService = $injector.get('userService'); - var userPageService = $injector.get('userPageService'); - - /** - * The username of the current user. - * - * @type String - */ - $scope.username = authenticationService.getCurrentUsername(); - - /** - * The user's full name. If not yet available, or if not defined, - * this will be null. - * - * @type String - */ - $scope.fullName = null; - - /** - * A URL pointing to relevant user information such as the user's - * email address. If not yet available, or if no such URL can be - * determined, this will be null. - * - * @type String - */ - $scope.userURL = null; - - /** - * The organization, company, group, etc. that the user belongs to. - * If not yet available, or if not defined, this will be null. - * - * @type String - */ - $scope.organization = null; - - /** - * The role that the user has at the organization, company, group, - * etc. they belong to. If not yet available, or if not defined, - * this will be null. - * - * @type String - */ - $scope.role = null; - - // Display user profile attributes if available - userService.getUser(authenticationService.getDataSource(), $scope.username) - .then(function userRetrieved(user) { - - // Pull basic profile information - $scope.fullName = user.attributes[User.Attributes.FULL_NAME]; - $scope.organization = user.attributes[User.Attributes.ORGANIZATION]; - $scope.role = user.attributes[User.Attributes.ORGANIZATIONAL_ROLE]; - - // Link to email address if available - var email = user.attributes[User.Attributes.EMAIL_ADDRESS]; - $scope.userURL = email ? 'mailto:' + email : null; - - }, requestService.IGNORE); - - /** - * The available main pages for the current user. - * - * @type Page[] - */ - $scope.pages = null; - - // Retrieve the main pages from the user page service - userPageService.getMainPages() - .then(function retrievedMainPages(pages) { - $scope.pages = pages; - }); - - /** - * Returns whether the current user has authenticated anonymously. - * - * @returns {Boolean} - * true if the current user has authenticated anonymously, false - * otherwise. - */ - $scope.isAnonymous = function isAnonymous() { - return authenticationService.isAnonymous(); - }; - - /** - * Logs out the current user, redirecting them to back to the root - * after logout completes. - */ - $scope.logout = function logout() { - authenticationService.logout() - ['catch'](requestService.IGNORE); - }; - - /** - * Action which logs out the current user, redirecting them to back - * to the login screen after logout completes. - */ - var LOGOUT_ACTION = { - name : 'USER_MENU.ACTION_LOGOUT', - className : 'logout', - callback : $scope.logout - }; - - /** - * All available actions for the current user. - */ - $scope.actions = [ LOGOUT_ACTION ]; - - }] // end controller - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/navigationModule.js b/guacamole/src/main/frontend/src/app/navigation/navigationModule.js deleted file mode 100644 index 24c63e3c3d..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/navigationModule.js +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Module for generating and implementing user navigation options. - */ -angular.module('navigation', [ - 'auth', - 'form', - 'notification', - 'rest' -]); diff --git a/guacamole/src/main/frontend/src/app/navigation/services/userPageService.js b/guacamole/src/main/frontend/src/app/navigation/services/userPageService.js deleted file mode 100644 index 9a9f693f6d..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/services/userPageService.js +++ /dev/null @@ -1,488 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for generating all the important pages a user can visit. - */ -angular.module('navigation').factory('userPageService', ['$injector', - function userPageService($injector) { - - // Get required types - var ClientIdentifier = $injector.get('ClientIdentifier'); - var ConnectionGroup = $injector.get('ConnectionGroup'); - var PageDefinition = $injector.get('PageDefinition'); - var PermissionSet = $injector.get('PermissionSet'); - - // Get required services - var $q = $injector.get('$q'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var translationStringService = $injector.get('translationStringService'); - - var service = {}; - - /** - * The home page to assign to a user if they can navigate to more than one - * page. - * - * @type PageDefinition - */ - var SYSTEM_HOME_PAGE = new PageDefinition({ - name : 'USER_MENU.ACTION_NAVIGATE_HOME', - url : '/' - }); - - /** - * Returns an appropriate home page for the current user. - * - * @param {Object.} rootGroups - * A map of all root connection groups visible to the current user, - * where each key is the identifier of the corresponding data source. - * - * @param {Object.} permissions - * A map of all permissions granted to the current user, where each - * key is the identifier of the corresponding data source. - * - * @returns {PageDefinition} - * The user's home page. - */ - var generateHomePage = function generateHomePage(rootGroups, permissions) { - - var settingsPages = generateSettingsPages(permissions); - - // If user has access to settings pages, return home page and skip - // evaluation for automatic connections. The Preferences page is - // a Settings page and is always visible, and the Session management - // page is also available to all users so that they can kill their - // own session. We look for more than those two pages to determine - // if we should go to the home page. - if (settingsPages.length > 2) - return SYSTEM_HOME_PAGE; - - // If exactly one connection or balancing group is available, use - // that as the home page - var clientPages = service.getClientPages(rootGroups); - return (clientPages.length === 1) ? clientPages[0] : SYSTEM_HOME_PAGE; - - }; - - /** - * Adds to the given array all pages that the current user may use to - * access connections or balancing groups that are descendants of the given - * connection group. - * - * @param {PageDefinition[]} clientPages - * The array that pages should be added to. - * - * @param {String} dataSource - * The data source containing the given connection group. - * - * @param {ConnectionGroup} connectionGroup - * The connection group ancestor of the connection or balancing group - * descendants whose pages should be added to the given array. - */ - var addClientPages = function addClientPages(clientPages, dataSource, connectionGroup) { - - // Add pages for all child connections - angular.forEach(connectionGroup.childConnections, function addConnectionPage(connection) { - clientPages.push(new PageDefinition({ - name : connection.name, - url : '/client/' + ClientIdentifier.toString({ - dataSource : dataSource, - type : ClientIdentifier.Types.CONNECTION, - id : connection.identifier - }) - })); - }); - - // Add pages for all child balancing groups, as well as the connectable - // descendants of all balancing groups of any type - angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupPage(connectionGroup) { - - if (connectionGroup.type === ConnectionGroup.Type.BALANCING) { - clientPages.push(new PageDefinition({ - name : connectionGroup.name, - url : '/client/' + ClientIdentifier.toString({ - dataSource : dataSource, - type : ClientIdentifier.Types.CONNECTION_GROUP, - id : connectionGroup.identifier - }) - })); - } - - addClientPages(clientPages, dataSource, connectionGroup); - - }); - - }; - - /** - * Returns a full list of all pages that the current user may use to access - * a connection or balancing group, regardless of the depth of those - * connections/groups within the connection hierarchy. - * - * @param {Object.} rootGroups - * A map of all root connection groups visible to the current user, - * where each key is the identifier of the corresponding data source. - * - * @returns {PageDefinition[]} - * A list of all pages that the current user may use to access a - * connection or balancing group. - */ - service.getClientPages = function getClientPages(rootGroups) { - - var clientPages = []; - - // Determine whether a connection or balancing group should serve as - // the home page - for (var dataSource in rootGroups) { - addClientPages(clientPages, dataSource, rootGroups[dataSource]); - } - - return clientPages; - - }; - - /** - * Returns a promise which resolves with an appropriate home page for the - * current user. The promise will not be rejected. - * - * @returns {Promise.} - * A promise which resolves with the user's default home page. - */ - service.getHomePage = function getHomePage() { - - var deferred = $q.defer(); - - // Resolve promise using home page derived from root connection groups - var getRootGroups = dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - authenticationService.getAvailableDataSources(), - ConnectionGroup.ROOT_IDENTIFIER - ); - var getPermissionSets = dataSourceService.apply( - permissionService.getEffectivePermissions, - authenticationService.getAvailableDataSources(), - authenticationService.getCurrentUsername() - ); - - $q.all({ - rootGroups : getRootGroups, - permissionsSets : getPermissionSets - }) - .then(function rootConnectionGroupsPermissionsRetrieved(data) { - deferred.resolve(generateHomePage(data.rootGroups,data.permissionsSets)); - }, requestService.DIE); - - return deferred.promise; - - }; - - /** - * Returns all settings pages that the current user can visit. This can - * include any of the various manage pages. - * - * @param {Object.} permissionSets - * A map of all permissions granted to the current user, where each - * key is the identifier of the corresponding data source. - * - * @returns {Page[]} - * An array of all settings pages that the current user can visit. - */ - var generateSettingsPages = function generateSettingsPages(permissionSets) { - - var pages = []; - - var canManageUsers = []; - var canManageUserGroups = []; - var canManageConnections = []; - var canViewConnectionRecords = []; - - // Inspect the contents of each provided permission set - angular.forEach(authenticationService.getAvailableDataSources(), function inspectPermissions(dataSource) { - - // Get permissions for current data source, skipping if non-existent - var permissions = permissionSets[dataSource]; - if (!permissions) - return; - - // Do not modify original object - permissions = angular.copy(permissions); - - // Ignore permission to update root group - PermissionSet.removeConnectionGroupPermission(permissions, - PermissionSet.ObjectPermissionType.UPDATE, - ConnectionGroup.ROOT_IDENTIFIER); - - // Ignore permission to update self - PermissionSet.removeUserPermission(permissions, - PermissionSet.ObjectPermissionType.UPDATE, - authenticationService.getCurrentUsername()); - - // Determine whether the current user needs access to the user management UI - if ( - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) - - // Permission to update users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - - // Permission to delete users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - - // Permission to administer users - || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - ) { - canManageUsers.push(dataSource); - } - - // Determine whether the current user needs access to the group management UI - if ( - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER_GROUP) - - // Permission to update user groups - || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - - // Permission to delete user groups - || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - - // Permission to administer user groups - || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - ) { - canManageUserGroups.push(dataSource); - } - - // Determine whether the current user needs access to the connection management UI - if ( - // System permissions - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) - - // Permission to update connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) - - // Permission to delete connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) - - // Permission to administer connections or connection groups - || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) - ) { - canManageConnections.push(dataSource); - } - - // Determine whether the current user needs access to view connection history - if ( - // A user must be a system administrator or auditor to view connection records - PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.AUDIT) - ) { - canViewConnectionRecords.push(dataSource); - } - - }); - - // Add link to Session management (always accessible) - pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_SESSIONS', - url : '/settings/sessions' - })); - - // If user can view connection records, add links for connection history pages - angular.forEach(canViewConnectionRecords, function addConnectionHistoryLink(dataSource) { - pages.push(new PageDefinition({ - name : [ - 'USER_MENU.ACTION_VIEW_HISTORY', - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' - ], - url : '/settings/' + encodeURIComponent(dataSource) + '/history' - })); - }); - - // If user can manage users, add link to user management page - if (canManageUsers.length) { - pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_USERS', - url : '/settings/users' - })); - } - - // If user can manage user groups, add link to group management page - if (canManageUserGroups.length) { - pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_USER_GROUPS', - url : '/settings/userGroups' - })); - } - - // If user can manage connections, add links for connection management pages - angular.forEach(canManageConnections, function addConnectionManagementLink(dataSource) { - pages.push(new PageDefinition({ - name : [ - 'USER_MENU.ACTION_MANAGE_CONNECTIONS', - translationStringService.canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' - ], - url : '/settings/' + encodeURIComponent(dataSource) + '/connections' - })); - }); - - // Add link to user preferences (always accessible) - pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_PREFERENCES', - url : '/settings/preferences' - })); - - return pages; - }; - - /** - * Returns a promise which resolves to an array of all settings pages that - * the current user can visit. This can include any of the various manage - * pages. The promise will not be rejected. - * - * @returns {Promise.} - * A promise which resolves to an array of all settings pages that the - * current user can visit. - */ - service.getSettingsPages = function getSettingsPages() { - - var deferred = $q.defer(); - - // Retrieve current permissions - dataSourceService.apply( - permissionService.getEffectivePermissions, - authenticationService.getAvailableDataSources(), - authenticationService.getCurrentUsername() - ) - - // Resolve promise using settings pages derived from permissions - .then(function permissionsRetrieved(permissions) { - deferred.resolve(generateSettingsPages(permissions)); - }, requestService.DIE); - - return deferred.promise; - - }; - - /** - * Returns all the main pages that the current user can visit. This can - * include the home page, manage pages, etc. In the case that there are no - * applicable pages of this sort, it may return a client page. - * - * @param {Object.} rootGroups - * A map of all root connection groups visible to the current user, - * where each key is the identifier of the corresponding data source. - * - * @param {Object.} permissions - * A map of all permissions granted to the current user, where each - * key is the identifier of the corresponding data source. - * - * @returns {Page[]} - * An array of all main pages that the current user can visit. - */ - var generateMainPages = function generateMainPages(rootGroups, permissions) { - - var pages = []; - - // Get home page and settings pages - var homePage = generateHomePage(rootGroups, permissions); - var settingsPages = generateSettingsPages(permissions); - - // Only include the home page in the list of main pages if the user - // can navigate elsewhere. - if (homePage === SYSTEM_HOME_PAGE || settingsPages.length) - pages.push(homePage); - - // Add generic link to the first-available settings page - if (settingsPages.length) { - pages.push(new PageDefinition({ - name : 'USER_MENU.ACTION_MANAGE_SETTINGS', - url : settingsPages[0].url - })); - } - - return pages; - }; - - /** - * Returns a promise which resolves to an array of all main pages that the - * current user can visit. This can include the home page, manage pages, - * etc. In the case that there are no applicable pages of this sort, it may - * return a client page. The promise will not be rejected. - * - * @returns {Promise.} - * A promise which resolves to an array of all main pages that the - * current user can visit. - */ - service.getMainPages = function getMainPages() { - - var deferred = $q.defer(); - - var rootGroups = null; - var permissions = null; - - /** - * Resolves the main pages retrieval promise, if possible. If - * insufficient data is available, this function does nothing. - */ - var resolveMainPages = function resolveMainPages() { - if (rootGroups && permissions) - deferred.resolve(generateMainPages(rootGroups, permissions)); - }; - - // Retrieve root group, resolving main pages if possible - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - authenticationService.getAvailableDataSources(), - ConnectionGroup.ROOT_IDENTIFIER - ) - .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) { - rootGroups = retrievedRootGroups; - resolveMainPages(); - }, requestService.DIE); - - // Retrieve current permissions - dataSourceService.apply( - permissionService.getEffectivePermissions, - authenticationService.getAvailableDataSources(), - authenticationService.getCurrentUsername() - ) - - // Resolving main pages if possible - .then(function permissionsRetrieved(retrievedPermissions) { - permissions = retrievedPermissions; - resolveMainPages(); - }, requestService.DIE); - - return deferred.promise; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/templates/guacMenu.html b/guacamole/src/main/frontend/src/app/navigation/templates/guacMenu.html deleted file mode 100644 index f7ddee974c..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/templates/guacMenu.html +++ /dev/null @@ -1,7 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/navigation/templates/guacPageList.html b/guacamole/src/main/frontend/src/app/navigation/templates/guacPageList.html deleted file mode 100644 index efadd0a903..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/templates/guacPageList.html +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/navigation/templates/guacSectionTabs.html b/guacamole/src/main/frontend/src/app/navigation/templates/guacSectionTabs.html deleted file mode 100644 index a028715343..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/templates/guacSectionTabs.html +++ /dev/null @@ -1,10 +0,0 @@ - \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/navigation/templates/guacUserMenu.html b/guacamole/src/main/frontend/src/app/navigation/templates/guacUserMenu.html deleted file mode 100644 index 9869cd6e83..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/templates/guacUserMenu.html +++ /dev/null @@ -1,33 +0,0 @@ -
    - - - -
    - -
    {{ role }}
    -
    {{ organization }}
    -
    - - - - - - - - - - -
    -
    diff --git a/guacamole/src/main/frontend/src/app/navigation/types/ClientIdentifier.js b/guacamole/src/main/frontend/src/app/navigation/types/ClientIdentifier.js deleted file mode 100644 index 47af7adc53..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/types/ClientIdentifier.js +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the ClientIdentifier class definition. - */ -angular.module('navigation').factory('ClientIdentifier', ['$injector', - function defineClientIdentifier($injector) { - - // Required services - var authenticationService = $injector.get('authenticationService'); - var $window = $injector.get('$window'); - - /** - * Object which uniquely identifies a particular connection or connection - * group within Guacamole. This object can be converted to/from a string to - * generate a guaranteed-unique, deterministic identifier for client URLs. - * - * @constructor - * @param {ClientIdentifier|Object} [template={}] - * The object whose properties should be copied within the new - * ClientIdentifier. - */ - var ClientIdentifier = function ClientIdentifier(template) { - - // Use empty object by default - template = template || {}; - - /** - * The identifier of the data source associated with the object to - * which the client will connect. This identifier will be the - * identifier of an AuthenticationProvider within the Guacamole web - * application. - * - * @type String - */ - this.dataSource = template.dataSource; - - /** - * The type of object to which the client will connect. Possible values - * are defined within ClientIdentifier.Types. - * - * @type String - */ - this.type = template.type; - - /** - * The unique identifier of the object to which the client will - * connect. - * - * @type String - */ - this.id = template.id; - - }; - - /** - * All possible ClientIdentifier types. - * - * @type Object. - */ - ClientIdentifier.Types = { - - /** - * The type string for a Guacamole connection. - * - * @type String - */ - CONNECTION : 'c', - - /** - * The type string for a Guacamole connection group. - * - * @type String - */ - CONNECTION_GROUP : 'g', - - /** - * The type string for an active Guacamole connection. - * - * @type String - */ - ACTIVE_CONNECTION : 'a' - - }; - - /** - * Encodes the given value as base64url, a variant of base64 defined by - * RFC 4648: https://datatracker.ietf.org/doc/html/rfc4648#section-5. - * - * The "base64url" variant is identical to standard base64 except that it - * uses "-" instead of "+", "_" instead of "/", and padding with "=" is - * optional. - * - * @param {string} value - * The string value to encode. - * - * @returns {string} - * The provided string value encoded as unpadded base64url. - */ - var base64urlEncode = function base64urlEncode(value) { - - // Translate padded standard base64 to unpadded base64url - return $window.btoa(value).replace(/[+/=]/g, - (str) => ({ - '+' : '-', - '/' : '_', - '=' : '' - })[str] - ); - - }; - - /** - * Decodes the given base64url or base64 string. The input string may - * contain "=" padding characters, but this is not required. - * - * @param {string} value - * The base64url or base64 value to decode. - * - * @returns {string} - * The result of decoding the provided base64url or base64 string. - */ - var base64urlDecode = function base64urlDecode(value) { - - // Add any missing padding (standard base64 requires input strings to - // be multiples of 4 in length, padded using '=') - value += ([ - '', - '===', - '==', - '=' - ])[value.length % 4]; - - // Translate padded base64url to padded standard base64 - return $window.atob(value.replace(/[-_]/g, - (str) => ({ - '-' : '+', - '_' : '/' - })[str] - )); - }; - - /** - * Converts the given ClientIdentifier or ClientIdentifier-like object to - * a String representation. Any object having the same properties as - * ClientIdentifier may be used, but only those properties will be taken - * into account when producing the resulting String. - * - * @param {ClientIdentifier|Object} id - * The ClientIdentifier or ClientIdentifier-like object to convert to - * a String representation. - * - * @returns {String} - * A deterministic String representation of the given ClientIdentifier - * or ClientIdentifier-like object. - */ - ClientIdentifier.toString = function toString(id) { - return base64urlEncode([ - id.id, - id.type, - id.dataSource - ].join('\0')); - }; - - /** - * Converts the given String into the corresponding ClientIdentifier. If - * the provided String is not a valid identifier, it will be interpreted - * as the identifier of a connection within the data source that - * authenticated the current user. - * - * @param {String} str - * The String to convert to a ClientIdentifier. - * - * @returns {ClientIdentifier} - * The ClientIdentifier represented by the given String. - */ - ClientIdentifier.fromString = function fromString(str) { - - try { - var values = base64urlDecode(str).split('\0'); - return new ClientIdentifier({ - id : values[0], - type : values[1], - dataSource : values[2] - }); - } - - // If the provided string is invalid, transform into a reasonable guess - catch (e) { - return new ClientIdentifier({ - id : str, - type : ClientIdentifier.Types.CONNECTION, - dataSource : authenticationService.getDataSource() || 'default' - }); - } - - }; - - return ClientIdentifier; - -}]); diff --git a/guacamole/src/main/frontend/src/app/navigation/types/PageDefinition.js b/guacamole/src/main/frontend/src/app/navigation/types/PageDefinition.js deleted file mode 100644 index f78129c4f7..0000000000 --- a/guacamole/src/main/frontend/src/app/navigation/types/PageDefinition.js +++ /dev/null @@ -1,75 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the PageDefinition class definition. - */ -angular.module('navigation').factory('PageDefinition', [function definePageDefinition() { - - /** - * Creates a new PageDefinition object which pairs the URL of a page with - * an arbitrary, human-readable name. - * - * @constructor - * @param {PageDefinition|Object} template - * The object whose properties should be copied within the new - * PageDefinition. - */ - var PageDefinition = function PageDefinition(template) { - - /** - * The name of the page, which should be a translation table key. - * Alternatively, this may also be a list of names, where the final - * name represents the page and earlier names represent categorization. - * Those categorical names may be rendered hierarchically as a system - * of menus, tabs, etc. - * - * @type String|String[] - */ - this.name = template.name; - - /** - * The URL of the page. - * - * @type String - */ - this.url = template.url; - - /** - * The CSS class name to associate with this page, if any. This will be - * an empty string by default. - * - * @type String - */ - this.className = template.className || ''; - - /** - * A numeric value denoting the relative sort order when compared to - * other sibling PageDefinitions. If unspecified, sort order is - * determined by the system using the PageDefinition. - * - * @type Number - */ - this.weight = template.weight; - - }; - - return PageDefinition; - -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/directives/guacModal.js b/guacamole/src/main/frontend/src/app/notification/directives/guacModal.js deleted file mode 100644 index 8cc6392cba..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/directives/guacModal.js +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for displaying arbitrary modal content. - */ -angular.module('notification').directive('guacModal', [function guacModal() { - return { - restrict: 'E', - templateUrl: 'app/notification/templates/guacModal.html', - transclude: true - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/directives/guacNotification.js b/guacamole/src/main/frontend/src/app/notification/directives/guacNotification.js deleted file mode 100644 index d3570e0c5e..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/directives/guacNotification.js +++ /dev/null @@ -1,86 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for the guacamole client. - */ -angular.module('notification').directive('guacNotification', [function guacNotification() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The notification to display. - * - * @type Notification|Object - */ - notification : '=' - - }, - - templateUrl: 'app/notification/templates/guacNotification.html', - controller: ['$scope', '$interval', function guacNotificationController($scope, $interval) { - - // Update progress bar if end known - $scope.$watch("notification.progress.ratio", function updateProgress(ratio) { - $scope.progressPercent = ratio * 100; - }); - - $scope.$watch("notification", function resetTimeRemaining(notification) { - - var countdown = notification.countdown; - - // Clean up any existing interval - if ($scope.interval) - $interval.cancel($scope.interval); - - // Update and handle countdown, if provided - if (countdown) { - - $scope.timeRemaining = countdown.remaining; - - $scope.interval = $interval(function updateTimeRemaining() { - - // Update time remaining - $scope.timeRemaining--; - - // Call countdown callback when time remaining expires - if ($scope.timeRemaining === 0 && countdown.callback) - countdown.callback(); - - }, 1000, $scope.timeRemaining); - - } - - }); - - // Clean up interval upon destruction - $scope.$on("$destroy", function destroyNotification() { - - if ($scope.interval) - $interval.cancel($scope.interval); - - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/templates/guacModal.html b/guacamole/src/main/frontend/src/app/notification/templates/guacModal.html deleted file mode 100644 index 57a1b5a464..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/templates/guacModal.html +++ /dev/null @@ -1,3 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/notification/templates/guacNotification.html b/guacamole/src/main/frontend/src/app/notification/templates/guacNotification.html deleted file mode 100644 index b002bb0f77..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/templates/guacNotification.html +++ /dev/null @@ -1,44 +0,0 @@ -
    - - -
    -
    {{notification.title | translate}}
    -
    - -
    - - -

    - - -
    - -
    - - -
    - - -

    - -
    - - -
    - -
    - - diff --git a/guacamole/src/main/frontend/src/app/notification/types/Notification.js b/guacamole/src/main/frontend/src/app/notification/types/Notification.js deleted file mode 100644 index eee16018b8..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/types/Notification.js +++ /dev/null @@ -1,123 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the Notification class definition. - */ -angular.module('notification').factory('Notification', [function defineNotification() { - - /** - * Creates a new Notification, initializing the properties of that - * Notification with the corresponding properties of the given template. - * - * @constructor - * @param {Notification|Object} [template={}] - * The object whose properties should be copied within the new - * Notification. - */ - var Notification = function Notification(template) { - - // Use empty object by default - template = template || {}; - - /** - * The CSS class to associate with the notification, if any. - * - * @type String - */ - this.className = template.className; - - /** - * The title of the notification. - * - * @type String - */ - this.title = template.title; - - /** - * The body text of the notification. - * - * @type TranslatableMessage - */ - this.text = template.text; - - /** - * The translation namespace of the translation strings that will - * be generated for all fields within the notification. This namespace - * is absolutely required if form fields will be included in the - * notification. - * - * @type String - */ - this.formNamespace = template.formNamespace; - - /** - * Optional form content to display. This may be a form, an array of - * forms, or a simple array of fields. - * - * @type Form[]|Form|Field[]|Field - */ - this.forms = template.forms; - - /** - * The object which will receive all field values. Each field value - * will be assigned to the property of this object having the same - * name. - * - * @type Object. - */ - this.formModel = template.model; - - /** - * The function to invoke when the form is submitted, if form fields - * are present within the notification. - * - * @type Function - */ - this.formSubmitCallback = template.formSubmitCallback; - - /** - * An array of all actions available to the user in response to this - * notification. - * - * @type NotificationAction[] - */ - this.actions = template.actions || []; - - /** - * The current progress state of the ongoing action associated with this - * notification. - * - * @type NotificationProgress - */ - this.progress = template.progress; - - /** - * The countdown and corresponding default action which applies to - * this notification, if any. - * - * @type NotificationCountdown - */ - this.countdown = template.countdown; - - }; - - return Notification; - -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/types/NotificationAction.js b/guacamole/src/main/frontend/src/app/notification/types/NotificationAction.js deleted file mode 100644 index cc41accfa7..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/types/NotificationAction.js +++ /dev/null @@ -1,73 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the NotificationAction class definition. - */ -angular.module('notification').factory('NotificationAction', [function defineNotificationAction() { - - /** - * Creates a new NotificationAction, which pairs an arbitrary callback with - * an action name. The name of this action will ultimately be presented to - * the user when the user is prompted to choose among available actions. - * - * @constructor - * @param {String} name The name of this action. - * - * @param {Function} callback - * The callback to call when the user elects to perform this action. - * - * @param {String} className - * The CSS class to associate with this action, if any. - */ - var NotificationAction = function NotificationAction(name, callback, className) { - - /** - * Reference to this NotificationAction. - * - * @type NotificationAction - */ - var action = this; - - /** - * The CSS class associated with this action. - * - * @type String - */ - this.className = className; - - /** - * The name of this action. - * - * @type String - */ - this.name = name; - - /** - * The callback to call when this action is performed. - * - * @type Function - */ - this.callback = callback; - - }; - - return NotificationAction; - -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/types/NotificationCountdown.js b/guacamole/src/main/frontend/src/app/notification/types/NotificationCountdown.js deleted file mode 100644 index fc607431eb..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/types/NotificationCountdown.js +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the NotificationCountdown class definition. - */ -angular.module('notification').factory('NotificationCountdown', [function defineNotificationCountdown() { - - /** - * Creates a new NotificationCountdown which describes an action that - * should be performed after a specific number of seconds has elapsed. - * - * @constructor - * @param {String} text The body text of the notification countdown. - * - * @param {Number} remaining - * The number of seconds remaining in the countdown. - * - * @param {Function} [callback] - * The callback to call when the countdown elapses. - */ - var NotificationCountdown = function NotificationCountdown(text, remaining, callback) { - - /** - * Reference to this NotificationCountdown. - * - * @type NotificationCountdown - */ - var countdown = this; - - /** - * The body text of the notification countdown. For the sake of i18n, - * the variable REMAINING should be applied within the translation - * string for formatting plurals, etc. - * - * @type String - */ - this.text = text; - - /** - * The number of seconds remaining in the countdown. After this number - * of seconds elapses, the callback associated with this - * NotificationCountdown will be called. - * - * @type Number - */ - this.remaining = remaining; - - /** - * The callback to call when this countdown expires. - * - * @type Function - */ - this.callback = callback; - - }; - - return NotificationCountdown; - -}]); diff --git a/guacamole/src/main/frontend/src/app/notification/types/NotificationProgress.js b/guacamole/src/main/frontend/src/app/notification/types/NotificationProgress.js deleted file mode 100644 index af754ab54a..0000000000 --- a/guacamole/src/main/frontend/src/app/notification/types/NotificationProgress.js +++ /dev/null @@ -1,84 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Provides the NotificationProgress class definition. - */ -angular.module('notification').factory('NotificationProgress', [function defineNotificationProgress() { - - /** - * Creates a new NotificationProgress which describes the current status - * of an operation, and how much of that operation remains to be performed. - * - * @constructor - * @param {String} text The text describing the operation progress. - * - * @param {Number} value - * The current state of operation progress, as an arbitrary number - * which increases as the operation continues. - * - * @param {String} [unit] - * The unit of the arbitrary value, if that value has an associated - * unit. - * - * @param {Number} [ratio] - * If known, the current status of the operation as a value between 0 - * and 1 inclusive, where 0 is not yet started, and 1 is complete. - */ - var NotificationProgress = function NotificationProgress(text, value, unit, ratio) { - - /** - * The text describing the operation progress. For the sake of i18n, - * the variable PROGRESS should be applied within the translation - * string for formatting plurals, etc., while UNIT should be used - * for the progress unit, if any. - * - * @type String - */ - this.text = text; - - /** - * The current state of operation progress, as an arbitrary number which - * increases as the operation continues. - * - * @type Number - */ - this.value = value; - - /** - * The unit of the arbitrary value, if that value has an associated - * unit. - * - * @type String - */ - this.unit = unit; - - /** - * If known, the current status of the operation as a value between 0 - * and 1 inclusive, where 0 is not yet started, and 1 is complete. - * - * @type String - */ - this.ratio = ratio; - - }; - - return NotificationProgress; - -}]); diff --git a/guacamole/src/main/frontend/src/app/osk/directives/guacOsk.js b/guacamole/src/main/frontend/src/app/osk/directives/guacOsk.js deleted file mode 100644 index 2236ac852a..0000000000 --- a/guacamole/src/main/frontend/src/app/osk/directives/guacOsk.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays the Guacamole on-screen keyboard. - */ -angular.module('osk').directive('guacOsk', [function guacOsk() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The URL for the Guacamole on-screen keyboard layout to use. - * - * @type String - */ - layout : '=' - - }, - - templateUrl: 'app/osk/templates/guacOsk.html', - controller: ['$scope', '$injector', '$element', - function guacOsk($scope, $injector, $element) { - - // Required services - var $http = $injector.get('$http'); - var $rootScope = $injector.get('$rootScope'); - var cacheService = $injector.get('cacheService'); - - /** - * The current on-screen keyboard, if any. - * - * @type Guacamole.OnScreenKeyboard - */ - var keyboard = null; - - /** - * The main containing element for the entire directive. - * - * @type Element - */ - var main = $element[0]; - - // Size keyboard to same size as main element - $scope.keyboardResized = function keyboardResized() { - - // Resize keyboard, if defined - if (keyboard) - keyboard.resize(main.offsetWidth); - - }; - - // Set layout whenever URL changes - $scope.$watch("layout", function setLayout(url) { - - // Remove current keyboard - if (keyboard) { - main.removeChild(keyboard.getElement()); - keyboard = null; - } - - // Load new keyboard - if (url) { - - // Retrieve layout JSON - $http({ - cache : cacheService.languages, - method : 'GET', - url : url - }) - - // Build OSK with retrieved layout - .then(function layoutRetrieved(request) { - - var layout = request.data; - - // Abort if the layout changed while we were waiting for a response - if ($scope.layout !== url) - return; - - // Add OSK element - keyboard = new Guacamole.OnScreenKeyboard(layout); - main.appendChild(keyboard.getElement()); - - // Init size - keyboard.resize(main.offsetWidth); - - // Broadcast keydown for each key pressed - keyboard.onkeydown = function(keysym) { - $rootScope.$broadcast('guacSyntheticKeydown', keysym); - }; - - // Broadcast keydown for each key released - keyboard.onkeyup = function(keysym) { - $rootScope.$broadcast('guacSyntheticKeyup', keysym); - }; - - }, angular.noop); - - } - - }); // end layout scope watch - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/osk/templates/guacOsk.html b/guacamole/src/main/frontend/src/app/osk/templates/guacOsk.html deleted file mode 100644 index 344c214868..0000000000 --- a/guacamole/src/main/frontend/src/app/osk/templates/guacOsk.html +++ /dev/null @@ -1,2 +0,0 @@ -
    -
    diff --git a/guacamole/src/main/frontend/src/app/player/directives/player.js b/guacamole/src/main/frontend/src/app/player/directives/player.js deleted file mode 100644 index 83acb68ae8..0000000000 --- a/guacamole/src/main/frontend/src/app/player/directives/player.js +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* - * NOTE: This session recording player implementation is based on the Session - * Recording Player for Glyptodon Enterprise which is available at - * https://github.com/glyptodon/glyptodon-enterprise-player under the - * following license: - * - * Copyright (C) 2019 Glyptodon, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* global _ */ - -/** - * Directive which plays back session recordings. This directive emits the - * following events based on state changes within the current recording: - * - * "guacPlayerLoading": - * A new recording has been selected and is now loading. - * - * "guacPlayerError": - * The current recording cannot be loaded or played due to an error. - * The recording may be unreadable (lack of permissions) or corrupt - * (protocol error). - * - * "guacPlayerProgress" - * Additional data has been loaded for the current recording and the - * recording's duration has changed. The new duration in milliseconds - * and the number of bytes loaded so far are passed to the event. - * - * "guacPlayerLoaded" - * The current recording has finished loading. - * - * "guacPlayerPlay" - * Playback of the current recording has started or has been resumed. - * - * "guacPlayerPause" - * Playback of the current recording has been paused. - * - * "guacPlayerSeek" - * The playback position of the current recording has changed. The new - * position within the recording is passed to the event as the number - * of milliseconds since the start of the recording. - */ -angular.module('player').directive('guacPlayer', ['$injector', function guacPlayer($injector) { - - // Required services - const keyEventDisplayService = $injector.get('keyEventDisplayService'); - const playerHeatmapService = $injector.get('playerHeatmapService'); - const playerTimeService = $injector.get('playerTimeService'); - - /** - * The number of milliseconds after the last detected mouse activity after - * which the associated CSS class should be removed. - * - * @type {number} - */ - const MOUSE_CLEANUP_DELAY = 4000; - - /** - * The number of milliseconds after the last detected mouse activity before - * the cleanup timer to remove the associated CSS class should be scheduled. - * - * @type {number} - */ - const MOUSE_DEBOUNCE_DELAY = 250; - - /** - * The number of milliseconds, after the debounce delay, before the mouse - * activity cleanup timer should run. - * - * @type {number} - */ - const MOUSE_CLEANUP_TIMER_DELAY = MOUSE_CLEANUP_DELAY - MOUSE_DEBOUNCE_DELAY; - - const config = { - restrict : 'E', - templateUrl : 'app/player/templates/player.html' - }; - - config.scope = { - - /** - * A Blob containing the Guacamole session recording to load. - * - * @type {!Blob|Guacamole.Tunnel} - */ - src : '=' - - }; - - config.controller = ['$scope', '$element', '$window', - function guacPlayerController($scope, $element, $window) { - - /** - * Guacamole.SessionRecording instance to be used to playback the - * session recording given via $scope.src. If the recording has not - * yet been loaded, this will be null. - * - * @type {Guacamole.SessionRecording} - */ - $scope.recording = null; - - /** - * The current playback position, in milliseconds. If a seek request is - * in progress, this will be the desired playback position of the - * pending request. - * - * @type {!number} - */ - $scope.playbackPosition = 0; - - /** - * The key of the translation string that describes the operation - * currently running in the background, or null if no such operation is - * running. - * - * @type {string} - */ - $scope.operationMessage = null; - - /** - * The current progress toward completion of the operation running in - * the background, where 0 represents no progress and 1 represents full - * completion. If no such operation is running, this value has no - * meaning. - * - * @type {!number} - */ - $scope.operationProgress = 0; - - /** - * The position within the recording of the current seek operation, in - * milliseconds. If a seek request is not in progress, this will be - * null. - * - * @type {number} - */ - $scope.seekPosition = null; - - /** - * Any batches of text typed during the recording. - * - * @type {keyEventDisplayService.TextBatch[]} - */ - $scope.textBatches = []; - - /** - * Whether or not the key log viewer should be displayed. False by - * default unless explicitly enabled by user interaction. - * - * @type {boolean} - */ - $scope.showKeyLog = false; - - /** - * The height, in pixels, of the SVG heatmap paths. Note that this is not - * necessarily the actual rendered height, just the initial size of the - * SVG path before any styling is applied. - * - * @type {!number} - */ - $scope.HEATMAP_HEIGHT = 100; - - /** - * The width, in pixels, of the SVG heatmap paths. Note that this is not - * necessarily the actual rendered width, just the initial size of the - * SVG path before any styling is applied. - * - * @type {!number} - */ - $scope.HEATMAP_WIDTH = 1000; - - /** - * The maximum number of key events per millisecond to display in the - * key event heatmap. Any key event rates exceeding this value will be - * capped at this rate to ensure that unsually large spikes don't make - * swamp the rest of the data. - * - * Note: This is 6 keys per second (events include both presses and - * releases) - equivalent to ~88 words per minute typed. - * - * @type {!number} - */ - const KEY_EVENT_RATE_CAP = 12 / 1000; - - /** - * The maximum number of frames per millisecond to display in the - * frame heatmap. Any frame rates exceeding this value will be - * capped at this rate to ensure that unsually large spikes don't make - * swamp the rest of the data. - * - * @type {!number} - */ - const FRAME_RATE_CAP = 10 / 1000; - - /** - * An SVG path describing a smoothed curve that visualizes the relative - * number of frames rendered throughout the recording - i.e. a heatmap - * of screen updates. - * - * @type {!string} - */ - $scope.frameHeatmap = ''; - - /** - * An SVG path describing a smoothed curve that visualizes the relative - * number of key events recorded throughout the recording - i.e. a - * heatmap of key events. - * - * @type {!string} - */ - $scope.keyHeatmap = ''; - - /** - * Whether a seek request is currently in progress. A seek request is - * in progress if the user is attempting to change the current playback - * position (the user is manipulating the playback position slider). - * - * @type {boolean} - */ - var pendingSeekRequest = false; - - /** - * Whether playback should be resumed (play() should be invoked on the - * recording) once the current seek request is complete. This value - * only has meaning if a seek request is pending. - * - * @type {boolean} - */ - var resumeAfterSeekRequest = false; - - /** - * A scheduled timer to clean up the mouse activity CSS class, or null - * if no timer is scheduled. - * - * @type {number} - */ - var mouseActivityTimer = null; - - /** - * The recording-relative timestamp of each frame of the recording that - * has been processed so far. - * - * @type {!number[]} - */ - var frameTimestamps = []; - - /** - * The recording-relative timestamp of each text event that has been - * processed so far. - * - * @type {!number[]} - */ - var keyTimestamps = []; - - /** - * Return true if any batches of key event logs are available for this - * recording, or false otherwise. - * - * @return - * True if any batches of key event logs are avaiable for this - * recording, or false otherwise. - */ - $scope.hasTextBatches = function hasTextBatches () { - return $scope.textBatches.length >= 0; - }; - - /** - * Toggle the visibility of the text key log viewer. - */ - $scope.toggleKeyLogView = function toggleKeyLogView() { - $scope.showKeyLog = !$scope.showKeyLog; - }; - - /** - * @borrows playerTimeService.formatTime - */ - $scope.formatTime = playerTimeService.formatTime; - - /** - * Pauses playback and decouples the position slider from current - * playback position, allowing the user to manipulate the slider - * without interference. Playback state will be resumed following a - * call to commitSeekRequest(). - */ - $scope.beginSeekRequest = function beginSeekRequest() { - - // If a recording is present, pause and save state if we haven't - // already done so - if ($scope.recording && !pendingSeekRequest) { - resumeAfterSeekRequest = $scope.recording.isPlaying(); - $scope.recording.pause(); - } - - // Flag seek request as in progress - pendingSeekRequest = true; - - }; - - /** - * Restores the playback state at the time beginSeekRequest() was - * called and resumes coupling between the playback position slider and - * actual playback position. - */ - $scope.commitSeekRequest = function commitSeekRequest() { - - // If a recording is present and there is an active seek request, - // restore the playback state at the time that request began and - // begin seeking to the requested position - if ($scope.recording && pendingSeekRequest) - $scope.seekToPlaybackPosition(); - - // Flag seek request as completed - pendingSeekRequest = false; - - }; - - /** - * Seek the recording to the specified position within the recording, - * in milliseconds. - * - * @param {Number} timestamp - * The position to seek to within the current record, - * in milliseconds. - */ - $scope.seekToTimestamp = function seekToTimestamp(timestamp) { - - // Set the timestamp and seek to it - $scope.playbackPosition = timestamp; - $scope.seekToPlaybackPosition(); - - }; - - /** - * Seek the recording to the current playback position value. - */ - $scope.seekToPlaybackPosition = function seekToPlaybackPosition() { - - $scope.seekPosition = null; - $scope.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS'; - $scope.operationProgress = 0; - - // Cancel seek when requested, updating playback position if - // that position changed - $scope.cancelOperation = function abortSeek() { - $scope.recording.cancel(); - $scope.playbackPosition = $scope.seekPosition || $scope.playbackPosition; - }; - - resumeAfterSeekRequest && $scope.recording.play(); - $scope.recording.seek($scope.playbackPosition, function seekComplete() { - $scope.seekPosition = null; - $scope.operationMessage = null; - $scope.$evalAsync(); - }); - }; - - /** - * Toggles the current playback state. If playback is currently paused, - * playback is resumed. If playback is currently active, playback is - * paused. If no recording has been loaded, this function has no - * effect. - */ - $scope.togglePlayback = function togglePlayback() { - if ($scope.recording) { - if ($scope.recording.isPlaying()) - $scope.recording.pause(); - else - $scope.recording.play(); - } - }; - - // Automatically load the requested session recording - $scope.$watch('src', function srcChanged(src) { - - // Reset position and seek state - pendingSeekRequest = false; - $scope.playbackPosition = 0; - - // Stop loading the current recording, if any - if ($scope.recording) { - $scope.recording.pause(); - $scope.recording.abort(); - } - - // If no recording is provided, reset to empty - if (!src) - $scope.recording = null; - - // Otherwise, begin loading the provided recording - else { - - $scope.recording = new Guacamole.SessionRecording(src); - - // Begin downloading the recording - $scope.recording.connect(); - - // Notify listeners and set any heatmap paths - // when the recording is completely loaded - $scope.recording.onload = function recordingLoaded() { - $scope.operationMessage = null; - $scope.$emit('guacPlayerLoaded'); - $scope.$evalAsync(); - - const recordingDuration = $scope.recording.getDuration(); - - // Generate heat maps for rendered frames and typed text - $scope.frameHeatmap = ( - playerHeatmapService.generateHeatmapPath( - frameTimestamps, recordingDuration, FRAME_RATE_CAP, - $scope.HEATMAP_HEIGHT, $scope.HEATMAP_WIDTH)); - $scope.keyHeatmap = ( - playerHeatmapService.generateHeatmapPath( - keyTimestamps, recordingDuration, KEY_EVENT_RATE_CAP, - $scope.HEATMAP_HEIGHT, $scope.HEATMAP_WIDTH)); - - }; - - // Notify listeners if an error occurs - $scope.recording.onerror = function recordingFailed(message) { - $scope.operationMessage = null; - $scope.$emit('guacPlayerError', message); - $scope.$evalAsync(); - }; - - // Notify listeners when additional recording data has been - // loaded - $scope.recording.onprogress = function recordingLoadProgressed(duration, current) { - $scope.operationProgress = src.size ? current / src.size : 0; - $scope.$emit('guacPlayerProgress', duration, current); - $scope.$evalAsync(); - - // Store the timestamp of the just-received frame - frameTimestamps.push(duration); - }; - - // Notify listeners when playback has started/resumed - $scope.recording.onplay = function playbackStarted() { - $scope.$emit('guacPlayerPlay'); - $scope.$evalAsync(); - }; - - // Notify listeners when playback has paused - $scope.recording.onpause = function playbackPaused() { - $scope.$emit('guacPlayerPause'); - $scope.$evalAsync(); - }; - - // Extract key events from the recording - $scope.recording.onkeyevents = function keyEventsReceived(events) { - - // Convert to a display-optimized format - $scope.textBatches = ( - keyEventDisplayService.parseEvents(events)); - - keyTimestamps = events.map(event => event.timestamp); - - }; - - // Notify listeners when current position within the recording - // has changed - $scope.recording.onseek = function positionChanged(position, current, total) { - - // Update current playback position while playing - if ($scope.recording.isPlaying()) - $scope.playbackPosition = position; - - // Update seek progress while seeking - else { - $scope.seekPosition = position; - $scope.operationProgress = current / total; - } - - $scope.$emit('guacPlayerSeek', position); - $scope.$evalAsync(); - - }; - - $scope.operationMessage = 'PLAYER.INFO_LOADING_RECORDING'; - $scope.operationProgress = 0; - - $scope.cancelOperation = function abortLoad() { - $scope.recording.abort(); - $scope.operationMessage = null; - }; - - $scope.$emit('guacPlayerLoading'); - - } - - }); - - // Clean up resources when player is destroyed - $scope.$on('$destroy', function playerDestroyed() { - $scope.recording.pause(); - $scope.recording.abort(); - mouseActivityTimer !== null && $window.clearTimeout(mouseActivityTimer); - }); - - /** - * Clean up the mouse movement class after no mouse activity has been - * detected for the appropriate time period. - */ - const scheduleCleanupTimeout = _.debounce(() => - mouseActivityTimer = $window.setTimeout(() => { - mouseActivityTimer = null; - $element.removeClass('recent-mouse-movement'); - }, MOUSE_CLEANUP_TIMER_DELAY), - - /* - * Only schedule the cleanup task after the mouse hasn't moved - * for a reasonable amount of time to ensure that the number of - * created cleanup timers remains reasonable. - */ - MOUSE_DEBOUNCE_DELAY); - - /* - * When the mouse moves inside the player, add a CSS class signifying - * recent mouse movement, to be automatically cleaned up after a period - * of time with no detected mouse movement. - */ - $element.on('mousemove', () => { - - // Clean up any existing cleanup timer - if (mouseActivityTimer !== null) { - $window.clearTimeout(mouseActivityTimer); - mouseActivityTimer = null; - } - - // Add the marker CSS class, and schedule its removal - $element.addClass('recent-mouse-movement'); - scheduleCleanupTimeout(); - - }); - - }]; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js b/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js deleted file mode 100644 index 114566a934..0000000000 --- a/guacamole/src/main/frontend/src/app/player/directives/playerDisplay.js +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/* - * NOTE: This session recording player implementation is based on the Session - * Recording Player for Glyptodon Enterprise which is available at - * https://github.com/glyptodon/glyptodon-enterprise-player under the - * following license: - * - * Copyright (C) 2019 Glyptodon, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/** - * Directive which contains a given Guacamole.Display, automatically scaling - * the display to fit available space. - */ -angular.module('player').directive('guacPlayerDisplay', [function guacPlayerDisplay() { - - const config = { - restrict : 'E', - templateUrl : 'app/player/templates/playerDisplay.html' - }; - - config.scope = { - - /** - * The Guacamole.Display instance which should be displayed within the - * directive. - * - * @type {Guacamole.Display} - */ - display : '=' - - }; - - config.controller = ['$scope', '$element', function guacPlayerDisplayController($scope, $element) { - - /** - * The root element of this instance of the guacPlayerDisplay - * directive. - * - * @type {Element} - */ - const element = $element.find('.guac-player-display')[0]; - - /** - * The element which serves as a container for the root element of the - * Guacamole.Display assigned to $scope.display. - * - * @type {HTMLDivElement} - */ - const container = $element.find('.guac-player-display-container')[0]; - - /** - * Rescales the Guacamole.Display currently assigned to $scope.display - * such that it exactly fits within this directive's available space. - * If no display is currently assigned or the assigned display is not - * at least 1x1 pixels in size, this function has no effect. - */ - $scope.fitDisplay = function fitDisplay() { - - // Ignore if no display is yet present - if (!$scope.display) - return; - - var displayWidth = $scope.display.getWidth(); - var displayHeight = $scope.display.getHeight(); - - // Ignore if the provided display is not at least 1x1 pixels - if (!displayWidth || !displayHeight) - return; - - // Fit display within available space - $scope.display.scale(Math.min(element.offsetWidth / displayWidth, - element.offsetHeight / displayHeight)); - - }; - - // Automatically add/remove the Guacamole.Display as $scope.display is - // updated - $scope.$watch('display', function displayChanged(display, oldDisplay) { - - // Clear out old display, if any - if (oldDisplay) { - container.innerHTML = ''; - oldDisplay.onresize = null; - } - - // If a new display is provided, add it to the container, keeping - // its scale in sync with changes to available space and display - // size - if (display) { - container.appendChild(display.getElement()); - display.onresize = $scope.fitDisplay; - $scope.fitDisplay(); - } - - }); - - }]; - - return config; - -}]); diff --git a/guacamole/src/main/frontend/src/app/player/directives/textView.js b/guacamole/src/main/frontend/src/app/player/directives/textView.js deleted file mode 100644 index 6e0ea060c9..0000000000 --- a/guacamole/src/main/frontend/src/app/player/directives/textView.js +++ /dev/null @@ -1,134 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -const fuzzysort = require('fuzzysort') - -/** - * Directive which plays back session recordings. - */ -angular.module('player').directive('guacPlayerTextView', - ['$injector', function guacPlayer($injector) { - - // Required services - const playerTimeService = $injector.get('playerTimeService'); - - const config = { - restrict : 'E', - templateUrl : 'app/player/templates/textView.html' - }; - - config.scope = { - - /** - * All the batches of text extracted from this recording. - * - * @type {!keyEventDisplayService.TextBatch[]} - */ - textBatches : '=', - - /** - * A callback that accepts a timestamp, and seeks the recording to - * that provided timestamp. - * - * @type {!Function} - */ - seek: '&', - - /** - * The current position within the recording. - * - * @type {!Number} - */ - currentPosition: '=' - - }; - - config.controller = ['$scope', '$element', '$injector', - function guacPlayerController($scope, $element) { - - /** - * The phrase to search within the text batches in order to produce the - * filtered list for display. - * - * @type {String} - */ - $scope.searchPhrase = ''; - - /** - * The text batches that match the current search phrase, or all - * batches if no search phrase is set. - * - * @type {!keyEventDisplayService.TextBatch[]} - */ - $scope.filteredBatches = $scope.textBatches; - - /** - * Whether or not the key log viewer should be full-screen. False by - * default unless explicitly enabled by user interaction. - * - * @type {boolean} - */ - $scope.fullscreenKeyLog = false; - - /** - * Toggle whether the key log viewer should take up the whole screen. - */ - $scope.toggleKeyLogFullscreen = function toggleKeyLogFullscreen() { - $element.toggleClass("fullscreen"); - }; - - /** - * Filter the provided text batches using the provided search phrase to - * generate the list of filtered batches, or set to all provided - * batches if no search phrase is provided. - * - * @param {String} searchPhrase - * The phrase to search the text batches for. If no phrase is - * provided, the list of batches will not be filtered. - */ - const applyFilter = searchPhrase => { - - // If there's search phrase entered, search the text within the - // batches for it - if (searchPhrase) - $scope.filteredBatches = fuzzysort.go( - searchPhrase, $scope.textBatches, {key: 'simpleValue'}) - .map(result => result.obj); - - // Otherwise, do not filter the batches - else - $scope.filteredBatches = $scope.textBatches; - - }; - - // Reapply the current filter to the updated text batches - $scope.$watch('textBatches', () => applyFilter($scope.searchPhrase)); - - // Reapply the filter whenever the search phrase is updated - $scope.$watch('searchPhrase', applyFilter); - - /** - * @borrows playerTimeService.formatTime - */ - $scope.formatTime = playerTimeService.formatTime; - - }]; - - return config; -}]); diff --git a/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js b/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js deleted file mode 100644 index cbfce14d76..0000000000 --- a/guacamole/src/main/frontend/src/app/player/services/keyEventDisplayService.js +++ /dev/null @@ -1,371 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - - /* - * NOTE: This session recording player implementation is based on the Session - * Recording Player for Glyptodon Enterprise which is available at - * https://github.com/glyptodon/glyptodon-enterprise-player under the - * following license: - * - * Copyright (C) 2019 Glyptodon, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ - -/* global _ */ - -/** - * A service for translating parsed key events in the format produced by - * KeyEventInterpreter into display-optimized text batches. - */ -angular.module('player').factory('keyEventDisplayService', - ['$injector', function keyEventDisplayService($injector) { - - /** - * A set of all keysyms corresponding to modifier keys. - * @type{Object.} - */ - const MODIFIER_KEYS = { - 0xFE03: true, // AltGr - 0xFFE1: true, // Left Shift - 0xFFE2: true, // Right Shift - 0xFFE3: true, // Left Control - 0xFFE4: true, // Right Control, - 0xFFE7: true, // Left Meta - 0xFFE8: true, // Right Meta - 0xFFE9: true, // Left Alt - 0xFFEA: true, // Right Alt - 0xFFEB: true, // Left Super - 0xFFEC: true, // Right Super - 0xFFED: true, // Left Hyper - 0xFFEE: true // Right Super - }; - - /** - * A set of all keysyms for which the name should be printed alongside the - * value of the key itself. - * @type{Object.} - */ - const PRINT_NAME_TOO_KEYS = { - 0xFF09: true, // Tab - 0xFF0D: true, // Return - 0xFF8D: true, // Enter - }; - - /** - * A set of all keysyms corresponding to keys commonly used in shortcuts. - * @type{Object.} - */ - const SHORTCUT_KEYS = { - 0xFFE3: true, // Left Control - 0xFFE4: true, // Right Control, - 0xFFE7: true, // Left Meta - 0xFFE8: true, // Right Meta - 0xFFE9: true, // Left Alt - 0xFFEA: true, // Right Alt - 0xFFEB: true, // Left Super - 0xFFEC: true, // Right Super - 0xFFED: true, // Left Hyper - 0xFFEE: true // Right Super - } - - /** - * Format and return a key name for display. - * - * @param {*} name - * The name of the key - * - * @returns - * The formatted key name. - */ - const formatKeyName = name => ('<' + name + '>'); - - const service = {}; - - /** - * A batch of text associated with a recording. The batch consists of a - * string representation of the text that would be typed based on the key - * events in the recording, as well as a timestamp when the batch started. - * - * @constructor - * @param {TextBatch|Object} [template={}] - * The object whose properties should be copied within the new TextBatch. - */ - service.TextBatch = function TextBatch(template) { - - // Use empty object by default - template = template || {}; - - /** - * All key events for this batch, some of which may be conslidated, - * representing multiple raw events. - * - * @type {ConsolidatedKeyEvent[]} - */ - this.events = template.events || []; - - /** - * The simplified, human-readable value representing the key events for - * this batch, equivalent to concatenating the `text` field of all key - * events in the batch. - * - * @type {!String} - */ - this.simpleValue = template.simpleValue || ''; - - }; - - /** - * A granular description of an extracted key event or sequence of events. - * It may contain multiple contiguous events of the same type, meaning that all - * event(s) that were combined into this event must have had the same `typed` - * field value. A single timestamp for the first combined event will be used - * for the whole batch if consolidated. - * - * @constructor - * @param {ConsolidatedKeyEvent|Object} [template={}] - * The object whose properties should be copied within the new KeyEventBatch. - */ - service.ConsolidatedKeyEvent = function ConsolidatedKeyEvent(template) { - - // Use empty object by default - template = template || {}; - - /** - * A human-readable representation of the event(s). If a series of printable - * characters was directly typed, this will just be those character(s). - * Otherwise it will be a string describing the event(s). - * - * @type {!String} - */ - this.text = template.text; - - /** - * True if this text of this event is exactly a typed character, or false - * otherwise. - * - * @type {!boolean} - */ - this.typed = template.typed; - - /** - * The timestamp from the recording when this event occured. - * - * @type {!Number} - */ - this.timestamp = template.timestamp; - - }; - - /** - * Accepts key events in the format produced by KeyEventInterpreter and returns - * human readable text batches, seperated by at least `batchSeperation` milliseconds - * if provided. - * - * NOTE: The event processing logic and output format is based on the `guaclog` - * tool, with the addition of batching support. - * - * @param {Guacamole.KeyEventInterpreter.KeyEvent[]} [rawEvents] - * The raw key events to prepare for display. - * - * @param {number} [batchSeperation=5000] - * The minimum number of milliseconds that must elapse between subsequent - * batches of key-event-generated text. If 0 or negative, no splitting will - * occur, resulting in a single batch for all provided key events. - * - * @param {boolean} [consolidateEvents=false] - * Whether consecutive sequences of events with similar properties - * should be consolidated into a single ConsolidatedKeyEvent object for - * display performance reasons. - */ - service.parseEvents = function parseEvents( - rawEvents, batchSeperation, consolidateEvents) { - - // Default to 5 seconds if the batch seperation was not provided - if (batchSeperation === undefined || batchSeperation === null) - batchSeperation = 5000; - /** - * A map of X11 keysyms to a KeyDefinition object, if the corresponding - * key is currently pressed. If a keysym has no entry in this map at all - * it means that the key is not being pressed. Note that not all keysyms - * are necessarily tracked within this map - only those that are - * explicitly tracked. - */ - const pressedKeys = {}; - - // The timestamp of the most recent key event processed - let lastKeyEvent = 0; - - // All text batches produced from the provided raw key events - const batches = [new service.TextBatch()]; - - // Process every provided raw - _.forEach(rawEvents, event => { - - // Extract all fields from the raw event - const { definition, pressed, timestamp } = event; - const { keysym, name, value } = definition; - - // Only switch to a new batch of text if sufficient time has passed - // since the last key event - const newBatch = (batchSeperation >= 0 - && (timestamp - lastKeyEvent) >= batchSeperation); - lastKeyEvent = timestamp; - - if (newBatch) - batches.push(new service.TextBatch()); - - const currentBatch = _.last(batches); - - /** - * Either push the a new event constructed using the provided fields - * into the latest batch, or consolidate into the latest event as - * appropriate given the consolidation configuration and event type. - * - * @param {!String} text - * The text representation of the event. - * - * @param {!Boolean} typed - * Whether the text value would be literally produced by typing - * the key that produced the event. - */ - const pushEvent = (text, typed) => { - const latestEvent = _.last(currentBatch.events); - - // Only consolidate the event if configured to do so and it - // matches the type of the previous event - if (consolidateEvents && latestEvent && latestEvent.typed === typed) { - latestEvent.text += text; - currentBatch.simpleValue += text; - } - - // Otherwise, push a new event - else { - currentBatch.events.push(new service.ConsolidatedKeyEvent({ - text, typed, timestamp})); - currentBatch.simpleValue += text; - } - } - - // Track modifier state - if (MODIFIER_KEYS[keysym]) { - if (pressed) - pressedKeys[keysym] = definition; - else - delete pressedKeys[keysym]; - } - - // Append to the current typed value when a printable - // (non-modifier) key is pressed - else if (pressed) { - - // If any shorcut keys are currently pressed - if (_.some(pressedKeys, (def, key) => SHORTCUT_KEYS[key])) { - - var shortcutText = '<'; - - var firstKey = true; - - // Compose entry by inspecting the state of each tracked key. - // At least one key must be pressed when in a shortcut. - for (let pressedKeysym in pressedKeys) { - - var pressedKeyDefinition = pressedKeys[pressedKeysym]; - - // Print name of key - if (firstKey) { - shortcutText += pressedKeyDefinition.name; - firstKey = false; - } - - else - shortcutText += ('+' + pressedKeyDefinition.name); - - } - - // Finally, append the printable key to close the shortcut - shortcutText += ('+' + name + '>') - - // Add the shortcut to the current batch - pushEvent(shortcutText, false); - } - } - - // Print the key itself - else { - - var keyText; - var typed; - - // Print the value if explicitly defined - if (value !== undefined) { - - keyText = value; - typed = true; - - // If the name should be printed in addition, add it as a - // seperate event before the actual character value - if (PRINT_NAME_TOO_KEYS[keysym]) - pushEvent(formatKeyName(name), false); - - } - - // Otherwise print the name - else { - - keyText = formatKeyName(name); - - // While this is a representation for a single character, - // the key text is the name of the key, not the actual - // character itself - typed = false; - - } - - // Add the key to the current batch - pushEvent(keyText, typed); - - } - - }); - - // All processed batches - return batches; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/player/templates/player.html b/guacamole/src/main/frontend/src/app/player/templates/player.html deleted file mode 100644 index f002bd42ca..0000000000 --- a/guacamole/src/main/frontend/src/app/player/templates/player.html +++ /dev/null @@ -1,83 +0,0 @@ -
    - - - - - - - -
    - - -
    - - - - -
    - - - - - - - - -
    - {{ 'PLAYER.INFO_FRAME_EVENTS_LEGEND' | translate }} - {{ 'PLAYER.INFO_KEY_EVENTS_LEGEND' | translate }} -
    -
    - -
    - - - - - - - - - - {{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }} - - - - {{ 'PLAYER.ACTION_SHOW_KEY_LOG' | translate }} - - - - {{ 'PLAYER.INFO_NO_KEY_LOG' | translate }} - -
    - -
    - - -
    - -

    - -
    diff --git a/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html b/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html deleted file mode 100644 index 606afafcbe..0000000000 --- a/guacamole/src/main/frontend/src/app/player/templates/playerDisplay.html +++ /dev/null @@ -1,3 +0,0 @@ -
    -
    -
    diff --git a/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html b/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html deleted file mode 100644 index 38c9768938..0000000000 --- a/guacamole/src/main/frontend/src/app/player/templates/progressIndicator.html +++ /dev/null @@ -1,12 +0,0 @@ -
    {{ percentage }}%
    -
    -
    -
    diff --git a/guacamole/src/main/frontend/src/app/player/templates/textView.html b/guacamole/src/main/frontend/src/app/player/templates/textView.html deleted file mode 100644 index 4cef97639e..0000000000 --- a/guacamole/src/main/frontend/src/app/player/templates/textView.html +++ /dev/null @@ -1,29 +0,0 @@ -
    - -
    -
    - -
    - -
    - -
    - -
    -
    -
    {{ formatTime(batch.events[0].timestamp) }}
    -
    - {{ event.text }} -
    - -
    -
    diff --git a/guacamole/src/main/frontend/src/app/rest/services/activeConnectionService.js b/guacamole/src/main/frontend/src/app/rest/services/activeConnectionService.js deleted file mode 100644 index 327153b3bb..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/activeConnectionService.js +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on active connections via the REST API. - */ -angular.module('rest').factory('activeConnectionService', ['$injector', - function activeConnectionService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - - var service = {}; - - /** - * Makes a request to the REST API to get a single active connection, - * returning a promise that provides the corresponding - * @link{ActiveConnection} if successful. - * - * @param {String} dataSource - * The identifier of the data source to retrieve the active connection - * from. - * - * @param {String} id - * The identifier of the active connection. - * - * @returns {Promise.} - * A promise which will resolve with a @link{ActiveConnection} upon - * success. - */ - service.getActiveConnection = function getActiveConnection(dataSource, id) { - - // Retrieve active connection - return authenticationService.request({ - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections/' + encodeURIComponent(id) - }); - - }; - - /** - * Makes a request to the REST API to get the list of active tunnels, - * returning a promise that provides a map of @link{ActiveConnection} - * objects if successful. - * - * @param {String[]} [permissionTypes] - * The set of permissions to filter with. A user must have one or more - * of these permissions for an active connection to appear in the - * result. If null, no filtering will be performed. Valid values are - * listed within PermissionSet.ObjectType. - * - * @returns {Promise.>} - * A promise which will resolve with a map of @link{ActiveConnection} - * objects, where each key is the identifier of the corresponding - * active connection. - */ - service.getActiveConnections = function getActiveConnections(dataSource, permissionTypes) { - - // Add permission filter if specified - var httpParameters = {}; - if (permissionTypes) - httpParameters.permission = permissionTypes; - - // Retrieve tunnels - return authenticationService.request({ - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections', - params : httpParameters - }); - - }; - - /** - * Makes a request to the REST API to delete the active connections having - * the given identifiers, effectively disconnecting them, returning a - * promise that can be used for processing the results of the call. - * - * @param {String[]} identifiers - * The identifiers of the active connections to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteActiveConnections = function deleteActiveConnections(dataSource, identifiers) { - - // Convert provided array of identifiers to a patch - var activeConnectionPatch = []; - identifiers.forEach(function addActiveConnectionPatch(identifier) { - activeConnectionPatch.push({ - op : 'remove', - path : '/' + identifier - }); - }); - - // Perform active connection deletion via PATCH - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections', - data : activeConnectionPatch - }); - - }; - - /** - * Makes a request to the REST API to generate credentials which have - * access strictly to the given active connection, using the restrictions - * defined by the given sharing profile, returning a promise that provides - * the resulting @link{UserCredentials} object if successful. - * - * @param {String} id - * The identifier of the active connection being shared. - * - * @param {String} sharingProfile - * The identifier of the sharing profile dictating the - * semantics/restrictions which apply to the shared session. - * - * @returns {Promise.} - * A promise which will resolve with a @link{UserCredentials} object - * upon success. - */ - service.getSharingCredentials = function getSharingCredentials(dataSource, id, sharingProfile) { - - // Generate sharing credentials - return authenticationService.request({ - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) - + '/activeConnections/' + encodeURIComponent(id) - + '/sharingCredentials/' + encodeURIComponent(sharingProfile) - }); - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/cacheService.js b/guacamole/src/main/frontend/src/app/rest/services/cacheService.js deleted file mode 100644 index 9a32004d3f..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/cacheService.js +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which contains all REST API response caches. - */ -angular.module('rest').factory('cacheService', ['$injector', - function cacheService($injector) { - - // Required services - var $cacheFactory = $injector.get('$cacheFactory'); - var $rootScope = $injector.get('$rootScope'); - - // Service containing all caches - var service = {}; - - /** - * Shared cache used by both connectionGroupService and - * connectionService. - * - * @type $cacheFactory.Cache - */ - service.connections = $cacheFactory('API-CONNECTIONS'); - - /** - * Cache used by languageService. - * - * @type $cacheFactory.Cache - */ - service.languages = $cacheFactory('API-LANGUAGES'); - - /** - * Cache used by patchService. - * - * @type $cacheFactory.Cache - */ - service.patches = $cacheFactory('API-PATCHES'); - - /** - * Cache used by schemaService. - * - * @type $cacheFactory.Cache - */ - service.schema = $cacheFactory('API-SCHEMA'); - - /** - * Shared cache used by userService, userGroupService, permissionService, - * and membershipService. - * - * @type $cacheFactory.Cache - */ - service.users = $cacheFactory('API-USERS'); - - /** - * Clear all caches defined in this service. - */ - service.clearCaches = function clearCaches() { - service.connections.removeAll(); - service.languages.removeAll(); - service.schema.removeAll(); - service.users.removeAll(); - }; - - // Clear caches on logout - $rootScope.$on('guacLogout', function handleLogout() { - service.clearCaches(); - }); - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionGroupService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionGroupService.js deleted file mode 100644 index ecfdee4f16..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionGroupService.js +++ /dev/null @@ -1,184 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on connection groups via the REST API. - */ -angular.module('rest').factory('connectionGroupService', ['$injector', - function connectionGroupService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - - var service = {}; - - /** - * Makes a request to the REST API to get an individual connection group - * and all descendants, returning a promise that provides the corresponding - * @link{ConnectionGroup} if successful. Descendant groups and connections - * will be stored as children of that connection group. If a permission - * type is specified, the result will be filtering by that permission. - * - * @param {String} [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] - * The ID of the connection group to retrieve. If not provided, the - * root connection group will be retrieved by default. - * - * @param {String[]} [permissionTypes] - * The set of permissions to filter with. A user must have one or more - * of these permissions for a connection to appear in the result. - * If null, no filtering will be performed. Valid values are listed - * within PermissionSet.ObjectType. - * - * @returns {Promise.ConnectionGroup} - * A promise which will resolve with a @link{ConnectionGroup} upon - * success. - */ - service.getConnectionGroupTree = function getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes) { - - // Use the root connection group ID if no ID is passed in - connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; - - // Add permission filter if specified - var httpParameters = {}; - if (permissionTypes) - httpParameters.permission = permissionTypes; - - // Retrieve connection group - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', - params : httpParameters - }); - - }; - - /** - * Makes a request to the REST API to get an individual connection group, - * returning a promise that provides the corresponding - * @link{ConnectionGroup} if successful. - * - * @param {String} [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] - * The ID of the connection group to retrieve. If not provided, the - * root connection group will be retrieved by default. - * - * @returns {Promise.} A promise for the HTTP call. - * A promise which will resolve with a @link{ConnectionGroup} upon - * success. - */ - service.getConnectionGroup = function getConnectionGroup(dataSource, connectionGroupID) { - - // Use the root connection group ID if no ID is passed in - connectionGroupID = connectionGroupID || ConnectionGroup.ROOT_IDENTIFIER; - - // Retrieve connection group - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) - }); - - }; - - /** - * Makes a request to the REST API to save a connection group, returning a - * promise that can be used for processing the results of the call. If the - * connection group is new, and thus does not yet have an associated - * identifier, the identifier will be automatically set in the provided - * connection group upon success. - * - * @param {ConnectionGroup} connectionGroup The connection group to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * save operation is successful. - */ - service.saveConnectionGroup = function saveConnectionGroup(dataSource, connectionGroup) { - - // If connection group is new, add it and set the identifier automatically - if (!connectionGroup.identifier) { - return authenticationService.request({ - method : 'POST', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups', - data : connectionGroup - }) - - // Set the identifier on the new connection group and clear the cache - .then(function connectionGroupCreated(newConnectionGroup){ - connectionGroup.identifier = newConnectionGroup.identifier; - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly created connection group - cacheService.users.removeAll(); - }); - } - - // Otherwise, update the existing connection group - else { - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), - data : connectionGroup - }) - - // Clear the cache - .then(function connectionGroupUpdated(){ - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly updated connection group - cacheService.users.removeAll(); - }); - } - - }; - - /** - * Makes a request to the REST API to delete a connection group, returning - * a promise that can be used for processing the results of the call. - * - * @param {ConnectionGroup} connectionGroup The connection group to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteConnectionGroup = function deleteConnectionGroup(dataSource, connectionGroup) { - - // Delete connection group - return authenticationService.request({ - method : 'DELETE', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier) - }) - - // Clear the cache - .then(function connectionGroupDeleted(){ - cacheService.connections.removeAll(); - }); - - }; - - return service; -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js b/guacamole/src/main/frontend/src/app/rest/services/connectionService.js deleted file mode 100644 index 9d41db6da1..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/connectionService.js +++ /dev/null @@ -1,226 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on connections via the REST API. - */ -angular.module('rest').factory('connectionService', ['$injector', - function connectionService($injector) { - - // Required services - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - var service = {}; - - /** - * Makes a request to the REST API to get a single connection, returning a - * promise that provides the corresponding @link{Connection} if successful. - * - * @param {String} id The ID of the connection. - * - * @returns {Promise.} - * A promise which will resolve with a @link{Connection} upon success. - * - * @example - * - * connectionService.getConnection('myConnection').then(function(connection) { - * // Do something with the connection - * }); - */ - service.getConnection = function getConnection(dataSource, id) { - - // Retrieve connection - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) - }); - - }; - - /** - * Makes a request to the REST API to get the usage history of a single - * connection, returning a promise that provides the corresponding - * array of @link{ConnectionHistoryEntry} objects if successful. - * - * @param {String} id - * The identifier of the connection. - * - * @returns {Promise.} - * A promise which will resolve with an array of - * @link{ConnectionHistoryEntry} objects upon success. - */ - service.getConnectionHistory = function getConnectionHistory(dataSource, id) { - - // Retrieve connection history - return authenticationService.request({ - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history' - }); - - }; - - /** - * Makes a request to the REST API to get the parameters of a single - * connection, returning a promise that provides the corresponding - * map of parameter name/value pairs if successful. - * - * @param {String} id - * The identifier of the connection. - * - * @returns {Promise.>} - * A promise which will resolve with an map of parameter name/value - * pairs upon success. - */ - service.getConnectionParameters = function getConnectionParameters(dataSource, id) { - - // Retrieve connection parameters - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters' - }); - - }; - - /** - * Makes a request to the REST API to save a connection, returning a - * promise that can be used for processing the results of the call. If the - * connection is new, and thus does not yet have an associated identifier, - * the identifier will be automatically set in the provided connection - * upon success. - * - * @param {Connection} connection The connection to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * save operation is successful. - */ - service.saveConnection = function saveConnection(dataSource, connection) { - - // If connection is new, add it and set the identifier automatically - if (!connection.identifier) { - return authenticationService.request({ - method : 'POST', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', - data : connection - }) - - // Set the identifier on the new connection and clear the cache - .then(function connectionCreated(newConnection){ - connection.identifier = newConnection.identifier; - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly created connection - cacheService.users.removeAll(); - }); - } - - // Otherwise, update the existing connection - else { - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), - data : connection - }) - - // Clear the cache - .then(function connectionUpdated(){ - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly updated connection - cacheService.users.removeAll(); - }); - } - - }; - - /** - * Makes a request to the REST API to apply a supplied list of connection - * patches, returning a promise that can be used for processing the results - * of the call. - * - * This operation is atomic - if any errors are encountered during the - * connection patching process, the entire request will fail, and no - * changes will be persisted. - * - * @param {String} dataSource - * The identifier of the data source associated with the connections to - * be patched. - * - * @param {DirectoryPatch.[]} patches - * An array of patches to apply. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * patch operation is successful. - */ - service.patchConnections = function patchConnections(dataSource, patches) { - - // Make the PATCH request - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', - data : patches - }) - - // Clear the cache - .then(function connectionsPatched(patchResponse){ - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for any - // newly created or replaced connections - cacheService.users.removeAll(); - - return patchResponse; - - }); - - }; - - /** - * Makes a request to the REST API to delete a connection, - * returning a promise that can be used for processing the results of the call. - * - * @param {Connection} connection The connection to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteConnection = function deleteConnection(dataSource, connection) { - - // Delete connection - return authenticationService.request({ - method : 'DELETE', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier) - }) - - // Clear the cache - .then(function connectionDeleted(){ - cacheService.connections.removeAll(); - }); - - }; - - return service; -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/dataSourceService.js b/guacamole/src/main/frontend/src/app/rest/services/dataSourceService.js deleted file mode 100644 index 6405394773..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/dataSourceService.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which contains all REST API response caches. - */ -angular.module('rest').factory('dataSourceService', ['$injector', - function dataSourceService($injector) { - - // Required types - var Error = $injector.get('Error'); - - // Required services - var $q = $injector.get('$q'); - var requestService = $injector.get('requestService'); - - // Service containing all caches - var service = {}; - - /** - * Invokes the given function once for each of the given data sources, - * passing that data source as the first argument to each invocation, - * followed by any additional arguments passed to apply(). The results of - * each invocation are aggregated into a map by data source identifier, - * and handled through a single promise which is resolved or rejected - * depending on the success/failure of each resulting REST call. Any error - * results in rejection of the entire apply() operation, except 404 ("NOT - * FOUND") errors, which are ignored. - * - * @param {Function} fn - * The function to call for each of the given data sources. The data - * source identifier will be given as the first argument, followed by - * the rest of the arguments given to apply(), in order. The function - * must return a Promise which is resolved or rejected depending on the - * result of the REST call. - * - * @param {String[]} dataSources - * The array or data source identifiers against which the given - * function should be called. - * - * @param {...*} args - * Any additional arguments to pass to the given function each time it - * is called. - * - * @returns {Promise.>} - * A Promise which resolves with a map of data source identifier to - * corresponding result. The result will be the exact object or value - * provided as the resolution to the Promise returned by calls to the - * given function. - */ - service.apply = function apply(fn, dataSources) { - - var deferred = $q.defer(); - - var requests = []; - var results = {}; - - // Build array of arguments to pass to the given function - var args = []; - for (var i = 2; i < arguments.length; i++) - args.push(arguments[i]); - - // Retrieve the root group from all data sources - angular.forEach(dataSources, function invokeAgainstDataSource(dataSource) { - - // Add promise to list of pending requests - var deferredRequest = $q.defer(); - requests.push(deferredRequest.promise); - - // Retrieve root group from data source - fn.apply(this, [dataSource].concat(args)) - - // Store result on success - .then(function immediateRequestSucceeded(data) { - results[dataSource] = data; - deferredRequest.resolve(); - }, - - // Fail on any errors (except "NOT FOUND") - requestService.createErrorCallback(function immediateRequestFailed(error) { - - if (error.type === Error.Type.NOT_FOUND) - deferredRequest.resolve(); - - // Explicitly abort for all other errors - else - deferredRequest.reject(error); - - })); - - }); - - // Resolve if all requests succeed - $q.all(requests).then(function requestsSucceeded() { - deferred.resolve(results); - }, - - // Reject if at least one request fails - requestService.createErrorCallback(function requestFailed(error) { - deferred.reject(error); - })); - - return deferred.promise; - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/requestService.js b/guacamole/src/main/frontend/src/app/rest/services/requestService.js deleted file mode 100644 index ead13320c4..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/requestService.js +++ /dev/null @@ -1,168 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for converting $http promises that pass the entire response into - * promises that pass only the data from that response. - */ -angular.module('rest').factory('requestService', ['$injector', - function requestService($injector) { - - // Required services - var $http = $injector.get('$http'); - var $log = $injector.get('$log'); - var $rootScope = $injector.get('$rootScope'); - - // Required types - var Error = $injector.get('Error'); - - /** - * Given a configuration object formatted for the $http service, returns - * a promise that will resolve or reject with the data from the HTTP - * response. If the promise is rejected due to the HTTP response indicating - * failure, the promise will be rejected strictly with an instance of an - * @link{Error} object. - * - * @param {Object} object - * Configuration object for $http service call. - * - * @returns {Promise.} - * A promise that will resolve with the data from the HTTP response for - * the underlying $http call if successful, or reject with an @link{Error} - * describing the failure. - */ - var service = function wrapHttpServiceCall(object) { - return $http(object).then( - function success(response) { return response.data; }, - function failure(response) { - - // Wrap true error responses from $http within REST Error objects - if (response.data) - throw new Error(response.data); - - // Fall back to a generic internal error if the request couldn't - // even be issued (webapp is down, etc.) - else if ('data' in response) - throw new Error({ message : 'Unknown failure sending HTTP request' }); - - // The value provided is not actually a response object from - // the $http service - throw response; - - } - ); - }; - - /** - * Creates a promise error callback which invokes the given callback only - * if the promise was rejected with a REST @link{Error} object. If the - * promise is rejected without an @link{Error} object, such as when a - * JavaScript error occurs within a callback earlier in the promise chain, - * the rejection is logged without invoking the given callback. - * - * @param {Function} callback - * The callback to invoke if the promise is rejected with an - * @link{Error} object. - * - * @returns {Function} - * A function which can be provided as the error callback for a - * promise. - */ - service.createErrorCallback = function createErrorCallback(callback) { - return (function generatedErrorCallback(error) { - - // Invoke given callback ONLY if due to a legitimate REST error - if (error instanceof Error) - return callback(error); - - // Log all other errors - $log.error(error); - - }); - }; - - /** - * Creates a promise error callback which resolves the promise with the - * given default value only if the @link{Error} in the original rejection - * is a NOT_FOUND error. All other errors are passed through and must be - * handled as yet more rejections. - * - * @param {*} value - * The default value to use to resolve the promise if the promise is - * rejected with a NOT_FOUND error. - * - * @returns {Function} - * A function which can be provided as the error callback for a - * promise. - */ - service.defaultValue = function defaultValue(value) { - return service.createErrorCallback(function resolveIfNotFound(error) { - - // Return default value only if not found - if (error.type === Error.Type.NOT_FOUND) - return value; - - // Reject promise with original error otherwise - throw error; - - }); - }; - - /** - * Promise error callback which ignores all rejections due to REST errors, - * but logs all other rejections, such as those due to JavaScript errors. - * This callback should be used in favor of angular.noop in cases where - * a REST response is being handled but REST errors should be ignored. - * - * @constant - * @type Function - */ - service.IGNORE = service.createErrorCallback(angular.noop); - - /** - * Promise error callback which logs all rejections due to REST errors as - * warnings to the browser console, and logs all other rejections as - * errors. This callback should be used in favor of angular.noop or - * @link{IGNORE} if REST errors are simply not expected. - * - * @constant - * @type Function - */ - service.WARN = service.createErrorCallback(function warnRequestFailed(error) { - $log.warn(error.type, error.message || error.translatableMessage); - }); - - /** - * Promise error callback which replaces the content of the page with a - * generic error message warning that the page could not be displayed. All - * rejections are logged to the browser console as errors. This callback - * should be used in favor of @link{WARN} if REST errors will result in the - * page being unusable. - * - * @constant - * @type Function - */ - service.DIE = service.createErrorCallback(function fatalPageError(error) { - $rootScope.$broadcast('guacFatalPageError', error); - $log.error(error.type, error.message || error.translatableMessage); - }); - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/sharingProfileService.js b/guacamole/src/main/frontend/src/app/rest/services/sharingProfileService.js deleted file mode 100644 index 57055ec760..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/sharingProfileService.js +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on sharing profiles via the REST API. - */ -angular.module('rest').factory('sharingProfileService', ['$injector', - function sharingProfileService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - var service = {}; - - /** - * Makes a request to the REST API to get a single sharing profile, - * returning a promise that provides the corresponding @link{SharingProfile} - * if successful. - * - * @param {String} id The ID of the sharing profile. - * - * @returns {Promise.} - * A promise which will resolve with a @link{SharingProfile} upon - * success. - * - * @example - * - * sharingProfileService.getSharingProfile('mySharingProfile').then(function(sharingProfile) { - * // Do something with the sharing profile - * }); - */ - service.getSharingProfile = function getSharingProfile(dataSource, id) { - - // Retrieve sharing profile - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(id) - }); - - }; - - /** - * Makes a request to the REST API to get the parameters of a single - * sharing profile, returning a promise that provides the corresponding - * map of parameter name/value pairs if successful. - * - * @param {String} id - * The identifier of the sharing profile. - * - * @returns {Promise.>} - * A promise which will resolve with an map of parameter name/value - * pairs upon success. - */ - service.getSharingProfileParameters = function getSharingProfileParameters(dataSource, id) { - - // Retrieve sharing profile parameters - return authenticationService.request({ - cache : cacheService.connections, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(id) + '/parameters' - }); - - }; - - /** - * Makes a request to the REST API to save a sharing profile, returning a - * promise that can be used for processing the results of the call. If the - * sharing profile is new, and thus does not yet have an associate - * identifier, the identifier will be automatically set in the provided - * sharing profile upon success. - * - * @param {SharingProfile} sharingProfile - * The sharing profile to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * save operation is successful. - */ - service.saveSharingProfile = function saveSharingProfile(dataSource, sharingProfile) { - - // If sharing profile is new, add it and set the identifier automatically - if (!sharingProfile.identifier) { - return authenticationService.request({ - method : 'POST', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles', - data : sharingProfile - }) - - // Set the identifier on the new sharing profile and clear the cache - .then(function sharingProfileCreated(newSharingProfile){ - sharingProfile.identifier = newSharingProfile.identifier; - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly created sharing profile - cacheService.users.removeAll(); - }); - } - - // Otherwise, update the existing sharing profile - else { - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(sharingProfile.identifier), - data : sharingProfile - }) - - // Clear the cache - .then(function sharingProfileUpdated(){ - cacheService.connections.removeAll(); - - // Clear users cache to force reload of permissions for this - // newly updated sharing profile - cacheService.users.removeAll(); - }); - } - - }; - - /** - * Makes a request to the REST API to delete a sharing profile, - * returning a promise that can be used for processing the results of the call. - * - * @param {SharingProfile} sharingProfile - * The sharing profile to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteSharingProfile = function deleteSharingProfile(dataSource, sharingProfile) { - - // Delete sharing profile - return authenticationService.request({ - method : 'DELETE', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(sharingProfile.identifier) - }) - - // Clear the cache - .then(function sharingProfileDeleted(){ - cacheService.connections.removeAll(); - }); - - }; - - return service; -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js b/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js deleted file mode 100644 index 1476218789..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/userGroupService.js +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on user groups via the REST API. - */ -angular.module('rest').factory('userGroupService', ['$injector', - function userGroupService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - var service = {}; - - /** - * Makes a request to the REST API to get the list of user groups, - * returning a promise that provides an array of @link{UserGroup} objects if - * successful. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user groups - * to be retrieved. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. - * - * @param {String[]} [permissionTypes] - * The set of permissions to filter with. A user group must have one or - * more of these permissions for a user group to appear in the result. - * If null, no filtering will be performed. Valid values are listed - * within PermissionSet.ObjectType. - * - * @returns {Promise.>} - * A promise which will resolve with a map of @link{UserGroup} objects - * where each key is the identifier of the corresponding user group. - */ - service.getUserGroups = function getUserGroups(dataSource, permissionTypes) { - - // Add permission filter if specified - var httpParameters = {}; - if (permissionTypes) - httpParameters.permission = permissionTypes; - - // Retrieve user groups - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', - params : httpParameters - }); - - }; - - /** - * Makes a request to the REST API to get the user group having the given - * identifier, returning a promise that provides the corresponding - * @link{UserGroup} if successful. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user group to - * be retrieved. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. - * - * @param {String} identifier - * The identifier of the user group to retrieve. - * - * @returns {Promise.} - * A promise which will resolve with a @link{UserGroup} upon success. - */ - service.getUserGroup = function getUserGroup(dataSource, identifier) { - - // Retrieve user group - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) - }); - - }; - - /** - * Makes a request to the REST API to delete a user group, returning a - * promise that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user group to - * be deleted. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {UserGroup} userGroup - * The user group to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteUserGroup = function deleteUserGroup(dataSource, userGroup) { - - // Delete user group - return authenticationService.request({ - method : 'DELETE', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier) - }) - - // Clear the cache - .then(function userGroupDeleted(){ - cacheService.users.removeAll(); - }); - - - }; - - /** - * Makes a request to the REST API to create a user group, returning a promise - * that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source in which the user group - * should be created. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. - * - * @param {UserGroup} userGroup - * The user group to create. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * create operation is successful. - */ - service.createUserGroup = function createUserGroup(dataSource, userGroup) { - - // Create user group - return authenticationService.request({ - method : 'POST', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', - data : userGroup - }) - - // Clear the cache - .then(function userGroupCreated(){ - cacheService.users.removeAll(); - }); - - }; - - /** - * Makes a request to the REST API to save a user group, returning a - * promise that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user group to - * be updated. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {UserGroup} userGroup - * The user group to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * save operation is successful. - */ - service.saveUserGroup = function saveUserGroup(dataSource, userGroup) { - - // Update user group - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier), - data : userGroup - }) - - // Clear the cache - .then(function userGroupUpdated(){ - cacheService.users.removeAll(); - }); - - }; - - /** - * Makes a request to the REST API to apply a supplied list of user group - * patches, returning a promise that can be used for processing the results - * of the call. - * - * This operation is atomic - if any errors are encountered during the - * connection patching process, the entire request will fail, and no - * changes will be persisted. - * - * @param {String} dataSource - * The identifier of the data source associated with the user groups to - * be patched. - * - * @param {DirectoryPatch.[]} patches - * An array of patches to apply. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * patch operation is successful. - */ - service.patchUserGroups = function patchUserGroups(dataSource, patches) { - - // Make the PATCH request - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', - data : patches - }) - - // Clear the cache - .then(function userGroupsPatched(patchResponse){ - cacheService.users.removeAll(); - return patchResponse; - }); - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/services/userService.js b/guacamole/src/main/frontend/src/app/rest/services/userService.js deleted file mode 100644 index c1d3c35728..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/services/userService.js +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service for operating on users via the REST API. - */ -angular.module('rest').factory('userService', ['$injector', - function userService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - // Get required types - var UserPasswordUpdate = $injector.get("UserPasswordUpdate"); - - var service = {}; - - /** - * Makes a request to the REST API to get the list of users, - * returning a promise that provides an array of @link{User} objects if - * successful. - * - * @param {String} dataSource - * The unique identifier of the data source containing the users to be - * retrieved. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {String[]} [permissionTypes] - * The set of permissions to filter with. A user must have one or more - * of these permissions for a user to appear in the result. - * If null, no filtering will be performed. Valid values are listed - * within PermissionSet.ObjectType. - * - * @returns {Promise.>} - * A promise which will resolve with a map of @link{User} objects - * where each key is the identifier (username) of the corresponding - * user. - */ - service.getUsers = function getUsers(dataSource, permissionTypes) { - - // Add permission filter if specified - var httpParameters = {}; - if (permissionTypes) - httpParameters.permission = permissionTypes; - - // Retrieve users - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', - params : httpParameters - }); - - }; - - /** - * Makes a request to the REST API to get the user having the given - * username, returning a promise that provides the corresponding - * @link{User} if successful. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to be - * retrieved. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {String} username - * The username of the user to retrieve. - * - * @returns {Promise.} - * A promise which will resolve with a @link{User} upon success. - */ - service.getUser = function getUser(dataSource, username) { - - // Retrieve user - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) - }); - - }; - - /** - * Makes a request to the REST API to delete a user, returning a promise - * that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to be - * deleted. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {User} user - * The user to delete. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * delete operation is successful. - */ - service.deleteUser = function deleteUser(dataSource, user) { - - // Delete user - return authenticationService.request({ - method : 'DELETE', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username) - }) - - // Clear the cache - .then(function userDeleted(){ - cacheService.users.removeAll(); - }); - - - }; - - /** - * Makes a request to the REST API to create a user, returning a promise - * that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source in which the user should be - * created. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {User} user - * The user to create. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * create operation is successful. - */ - service.createUser = function createUser(dataSource, user) { - - // Create user - return authenticationService.request({ - method : 'POST', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', - data : user - }) - - // Clear the cache - .then(function userCreated(){ - cacheService.users.removeAll(); - }); - - }; - - /** - * Makes a request to the REST API to save a user, returning a promise that - * can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to be - * updated. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {User} user - * The user to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * save operation is successful. - */ - service.saveUser = function saveUser(dataSource, user) { - - // Update user - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), - data : user - }) - - // Clear the cache - .then(function userUpdated(){ - cacheService.users.removeAll(); - }); - - }; - - /** - * Makes a request to the REST API to update the password for a user, - * returning a promise that can be used for processing the results of the call. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user to be - * updated. This identifier corresponds to an AuthenticationProvider - * within the Guacamole web application. - * - * @param {String} username - * The username of the user to update. - * - * @param {String} oldPassword - * The exiting password of the user to update. - * - * @param {String} newPassword - * The new password of the user to update. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * password update operation is successful. - */ - service.updateUserPassword = function updateUserPassword(dataSource, username, - oldPassword, newPassword) { - - // Update user password - return authenticationService.request({ - method : 'PUT', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password', - data : new UserPasswordUpdate({ - oldPassword : oldPassword, - newPassword : newPassword - }) - }) - - // Clear the cache - .then(function passwordChanged(){ - cacheService.users.removeAll(); - }); - - }; - - /** - * Makes a request to the REST API to apply a supplied list of user patches, - * returning a promise that can be used for processing the results of the - * call. - * - * This operation is atomic - if any errors are encountered during the - * connection patching process, the entire request will fail, and no - * changes will be persisted. - * - * @param {String} dataSource - * The identifier of the data source associated with the users to be - * patched. - * - * @param {DirectoryPatch.[]} patches - * An array of patches to apply. - * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the - * patch operation is successful. - */ - service.patchUsers = function patchUsers(dataSource, patches) { - - // Make the PATCH request - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/users', - data : patches - }) - - // Clear the cache - .then(function usersPatched(patchResponse){ - cacheService.users.removeAll(); - return patchResponse; - }); - - }; - - return service; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/ActiveConnection.js b/guacamole/src/main/frontend/src/app/rest/types/ActiveConnection.js deleted file mode 100644 index dc0b140fe3..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/ActiveConnection.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ActiveConnection class. - */ -angular.module('rest').factory('ActiveConnection', [function defineActiveConnection() { - - /** - * The object returned by REST API calls when representing the data - * associated with an active connection. Each active connection is - * effectively a pairing of a connection and the user currently using it, - * along with other information. - * - * @constructor - * @param {ActiveConnection|Object} [template={}] - * The object whose properties should be copied within the new - * ActiveConnection. - */ - var ActiveConnection = function ActiveConnection(template) { - - // Use empty object by default - template = template || {}; - - /** - * The identifier which uniquely identifies this specific active - * connection. - * - * @type String - */ - this.identifier = template.identifier; - - /** - * The identifier of the connection associated with this active - * connection. - * - * @type String - */ - this.connectionIdentifier = template.connectionIdentifier; - - /** - * The time that the connection began, in seconds since - * 1970-01-01 00:00:00 UTC, if known. - * - * @type Number - */ - this.startDate = template.startDate; - - /** - * The remote host that initiated the connection, if known. - * - * @type String - */ - this.remoteHost = template.remoteHost; - - /** - * The username of the user associated with the connection, if known. - * - * @type String - */ - this.username = template.username; - - /** - * Whether this active connection may be connected to, just as a - * normal connection. - * - * @type Boolean - */ - this.connectable = template.connectable; - - }; - - return ActiveConnection; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/Connection.js b/guacamole/src/main/frontend/src/app/rest/types/Connection.js deleted file mode 100644 index 89da4e1172..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/Connection.js +++ /dev/null @@ -1,120 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the Connection class. - */ -angular.module('rest').factory('Connection', [function defineConnection() { - - /** - * The object returned by REST API calls when representing the data - * associated with a connection. - * - * @constructor - * @param {Connection|Object} [template={}] - * The object whose properties should be copied within the new - * Connection. - */ - var Connection = function Connection(template) { - - // Use empty object by default - template = template || {}; - - /** - * The unique identifier associated with this connection. - * - * @type String - */ - this.identifier = template.identifier; - - /** - * The unique identifier of the connection group that contains this - * connection. - * - * @type String - */ - this.parentIdentifier = template.parentIdentifier; - - /** - * The human-readable name of this connection, which is not necessarily - * unique. - * - * @type String - */ - this.name = template.name; - - /** - * The name of the protocol associated with this connection, such as - * "vnc" or "rdp". - * - * @type String - */ - this.protocol = template.protocol; - - /** - * Connection configuration parameters, as dictated by the protocol in - * use, arranged as name/value pairs. This information may not be - * available until directly queried. If this information is - * unavailable, this property will be null or undefined. - * - * @type Object. - */ - this.parameters = template.parameters; - - /** - * Arbitrary name/value pairs which further describe this connection. - * The semantics and validity of these attributes are dictated by the - * extension which defines them. - * - * @type Object. - */ - this.attributes = template.attributes || {}; - - /** - * The count of currently active connections using this connection. - * This field will be returned from the REST API during a get - * operation, but manually setting this field will have no effect. - * - * @type Number - */ - this.activeConnections = template.activeConnections; - - /** - * An array of all associated sharing profiles, if known. This property - * may be null or undefined if sharing profiles have not been queried, - * and thus the sharing profiles are unknown. - * - * @type SharingProfile[] - */ - this.sharingProfiles = template.sharingProfiles; - - /** - * The time that this connection was last used, in milliseconds since - * 1970-01-01 00:00:00 UTC. If this information is unknown or - * unavailable, this will be null. - * - * @type Number - */ - this.lastActive = template.lastActive; - - }; - - return Connection; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/ConnectionGroup.js b/guacamole/src/main/frontend/src/app/rest/types/ConnectionGroup.js deleted file mode 100644 index 6da754c0c9..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/ConnectionGroup.js +++ /dev/null @@ -1,142 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ConnectionGroup class. - */ -angular.module('rest').factory('ConnectionGroup', [function defineConnectionGroup() { - - /** - * The object returned by REST API calls when representing the data - * associated with a connection group. - * - * @constructor - * @param {ConnectionGroup|Object} [template={}] - * The object whose properties should be copied within the new - * ConnectionGroup. - */ - var ConnectionGroup = function ConnectionGroup(template) { - - // Use empty object by default - template = template || {}; - - /** - * The unique identifier associated with this connection group. - * - * @type String - */ - this.identifier = template.identifier; - - /** - * The unique identifier of the connection group that contains this - * connection group. - * - * @type String - * @default ConnectionGroup.ROOT_IDENTIFIER - */ - this.parentIdentifier = template.parentIdentifier || ConnectionGroup.ROOT_IDENTIFIER; - - /** - * The human-readable name of this connection group, which is not - * necessarily unique. - * - * @type String - */ - this.name = template.name; - - /** - * The type of this connection group, which may be either - * ConnectionGroup.Type.ORGANIZATIONAL or - * ConnectionGroup.Type.BALANCING. - * - * @type String - * @default ConnectionGroup.Type.ORGANIZATIONAL - */ - this.type = template.type || ConnectionGroup.Type.ORGANIZATIONAL; - - /** - * An array of all child connections, if known. This property may be - * null or undefined if children have not been queried, and thus the - * child connections are unknown. - * - * @type Connection[] - */ - this.childConnections = template.childConnections; - - /** - * An array of all child connection groups, if known. This property may - * be null or undefined if children have not been queried, and thus the - * child connection groups are unknown. - * - * @type ConnectionGroup[] - */ - this.childConnectionGroups = template.childConnectionGroups; - - /** - * Arbitrary name/value pairs which further describe this connection - * group. The semantics and validity of these attributes are dictated - * by the extension which defines them. - * - * @type Object. - */ - this.attributes = template.attributes || {}; - - /** - * The count of currently active connections using this connection - * group. This field will be returned from the REST API during a get - * operation, but manually setting this field will have no effect. - * - * @type Number - */ - this.activeConnections = template.activeConnections; - - }; - - /** - * The reserved identifier which always represents the root connection - * group. - * - * @type String - */ - ConnectionGroup.ROOT_IDENTIFIER = "ROOT"; - - /** - * All valid connection group types. - */ - ConnectionGroup.Type = { - - /** - * The type string associated with balancing connection groups. - * - * @type String - */ - BALANCING : "BALANCING", - - /** - * The type string associated with organizational connection groups. - * - * @type String - */ - ORGANIZATIONAL : "ORGANIZATIONAL" - - }; - - return ConnectionGroup; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js b/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js deleted file mode 100644 index e035efc82d..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/ConnectionHistoryEntry.js +++ /dev/null @@ -1,230 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the ConnectionHistoryEntry class. - */ -angular.module('rest').factory('ConnectionHistoryEntry', [function defineConnectionHistoryEntry() { - - /** - * The object returned by REST API calls when representing the data - * associated with an entry in a connection's usage history. Each history - * entry represents the time at which a particular started using a - * connection and, if applicable, the time that usage stopped. - * - * @constructor - * @param {ConnectionHistoryEntry|Object} [template={}] - * The object whose properties should be copied within the new - * ConnectionHistoryEntry. - */ - var ConnectionHistoryEntry = function ConnectionHistoryEntry(template) { - - // Use empty object by default - template = template || {}; - - /** - * An arbitrary identifier that uniquely identifies this record - * relative to other records in the same set, or null if no such unique - * identifier exists. - * - * @type {string} - */ - this.identifier = template.identifier; - - /** - * A UUID that uniquely identifies this record, or null if no such - * unique identifier exists. - * - * @type {string} - */ - this.uuid = template.uuid; - - /** - * The identifier of the connection associated with this history entry. - * - * @type String - */ - this.connectionIdentifier = template.connectionIdentifier; - - /** - * The name of the connection associated with this history entry. - * - * @type String - */ - this.connectionName = template.connectionName; - - /** - * The remote host associated with this history entry. - * - * @type String - */ - this.remoteHost = template.remoteHost; - - /** - * The time that usage began, in seconds since 1970-01-01 00:00:00 UTC. - * - * @type Number - */ - this.startDate = template.startDate; - - /** - * The time that usage ended, in seconds since 1970-01-01 00:00:00 UTC. - * The absence of an endDate does NOT necessarily indicate that the - * connection is still in use, particularly if the server was shutdown - * or restarted before the history entry could be updated. To determine - * whether a connection is still active, check the active property of - * this history entry. - * - * @type Number - */ - this.endDate = template.endDate; - - /** - * The remote host that initiated this connection, if known. - * - * @type String - */ - this.remoteHost = template.remoteHost; - - /** - * The username of the user associated with this particular usage of - * the connection. - * - * @type String - */ - this.username = template.username; - - /** - * Whether this usage of the connection is still active. Note that this - * is the only accurate way to check for connection activity; the - * absence of endDate does not necessarily imply the connection is - * active, as the history entry may simply be incomplete. - * - * @type Boolean - */ - this.active = template.active; - - /** - * Arbitrary name/value pairs which further describe this history - * entry. The semantics and validity of these attributes are dictated - * by the extension which defines them. - * - * @type {!object.} - */ - this.attributes = template.attributes; - - /** - * All logs associated and accessible via this record, stored by their - * corresponding unique names. - * - * @type {!object.} - */ - this.logs = template.logs; - - }; - - /** - * All possible predicates for sorting ConnectionHistoryEntry objects using - * the REST API. By default, each predicate indicates ascending order. To - * indicate descending order, add "-" to the beginning of the predicate. - * - * @type Object. - */ - ConnectionHistoryEntry.SortPredicate = { - - /** - * The date and time that the connection associated with the history - * entry began (connected). - */ - START_DATE : 'startDate' - - }; - - /** - * Value/unit pair representing the length of time that a connection was - * used. - * - * @constructor - * @param {Number} milliseconds - * The number of milliseconds that the associated connection was used. - */ - ConnectionHistoryEntry.Duration = function Duration(milliseconds) { - - /** - * The provided duration in seconds. - * - * @type Number - */ - var seconds = milliseconds / 1000; - - /** - * Rounds the given value to the nearest tenth. - * - * @param {Number} value The value to round. - * @returns {Number} The given value, rounded to the nearest tenth. - */ - var round = function round(value) { - return Math.round(value * 10) / 10; - }; - - // Days - if (seconds >= 86400) { - this.value = round(seconds / 86400); - this.unit = 'day'; - } - - // Hours - else if (seconds >= 3600) { - this.value = round(seconds / 3600); - this.unit = 'hour'; - } - - // Minutes - else if (seconds >= 60) { - this.value = round(seconds / 60); - this.unit = 'minute'; - } - - // Seconds - else { - - /** - * The number of seconds (or minutes, or hours, etc.) that the - * connection was used. The units associated with this value are - * represented by the unit property. - * - * @type Number - */ - this.value = round(seconds); - - /** - * The units associated with the value of this duration. Valid - * units are 'second', 'minute', 'hour', and 'day'. - * - * @type String - */ - this.unit = 'second'; - - } - - }; - - return ConnectionHistoryEntry; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js deleted file mode 100644 index 664dcdcdf7..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatch.js +++ /dev/null @@ -1,94 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the DirectoryPatch class. - */ -angular.module('rest').factory('DirectoryPatch', [function defineDirectoryPatch() { - - /** - * The object consumed by REST API calls when representing changes to an - * arbitrary set of directory-based objects. - * @constructor - * - * @template DirectoryObject - * The directory-based object type that this DirectoryPatch will - * operate on. - * - * @param {DirectoryObject|Object} [template={}] - * The object whose properties should be copied within the new - * DirectoryPatch. - */ - var DirectoryPatch = function DirectoryPatch(template) { - - // Use empty object by default - template = template || {}; - - /** - * The operation to apply to the objects indicated by the path. Valid - * operation values are defined within DirectoryPatch.Operation. - * - * @type {String} - */ - this.op = template.op; - - /** - * The path of the objects to modify. For creation of new objects, this - * should be "/". Otherwise, it should be "/{identifier}", specifying - * the identifier of the existing object being modified. - * - * @type {String} - * @default '/' - */ - this.path = template.path || '/'; - - /** - * The object being added/replaced, or undefined if deleting. - * - * @type {DirectoryObject} - */ - this.value = template.value; - - }; - - /** - * All valid patch operations for directory-based objects. - */ - DirectoryPatch.Operation = { - - /** - * Adds the specified object to the relation. - */ - ADD : 'add', - - /** - * Replaces (updates) the specified object from the relation. - */ - REPLACE : 'replace', - - /** - * Removes the specified object from the relation. - */ - REMOVE : 'remove' - - }; - - return DirectoryPatch; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js b/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js deleted file mode 100644 index 324f1ae180..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchOutcome.js +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the DirectoryPatchOutcome class. - */ -angular.module('rest').factory('DirectoryPatchOutcome', [ - function defineDirectoryPatchOutcome() { - - /** - * An object returned by a PATCH request to a directory REST API, - * representing the outcome associated with a particular patch in the - * request. This object can indicate either a successful or unsuccessful - * response. The error field is only meaningful for unsuccessful patches. - * @constructor - * - * @param {DirectoryPatchOutcome|Object} [template={}] - * The object whose properties should be copied within the new - * DirectoryPatchOutcome. - */ - const DirectoryPatchOutcome = function DirectoryPatchOutcome(template) { - - // Use empty object by default - template = template || {}; - - /** - * The operation to apply to the objects indicated by the path. Valid - * operation values are defined within DirectoryPatch.Operation. - * - * @type {String} - */ - this.op = template.op; - - /** - * The path of the object operated on by the corresponding patch in the - * request. - * - * @type {String} - */ - this.path = template.path; - - /** - * The identifier of the object operated on by the corresponding patch - * in the request. If the object was newly created and the PATCH request - * did not fail, this will be the identifier of the newly created object. - * - * @type {String} - */ - this.identifier = template.identifier; - - /** - * The error message associated with the failure, if the patch failed to - * apply. - * - * @type {TranslatableMessage} - */ - this.error = template.error; - - }; - - return DirectoryPatchOutcome; - -}]); diff --git a/guacamole/src/main/frontend/src/app/rest/types/Error.js b/guacamole/src/main/frontend/src/app/rest/types/Error.js deleted file mode 100644 index 74b28ebae3..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/Error.js +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the Error class. - */ -angular.module('rest').factory('Error', [function defineError() { - - /** - * The object returned by REST API calls when an error occurs. - * - * @constructor - * @param {Error|Object} [template={}] - * The object whose properties should be copied within the new - * Error. - */ - var Error = function Error(template) { - - // Use empty object by default - template = template || {}; - - /** - * A human-readable message describing the error that occurred. - * - * @type String - */ - this.message = template.message; - - /** - * A message which can be translated using the translation service, - * consisting of a translation key and optional set of substitution - * variables. - * - * @type TranslatableMessage - */ - this.translatableMessage = template.translatableMessage; - - /** - * The Guacamole protocol status code associated with the error that - * occurred. This is only valid for errors of type STREAM_ERROR. - * - * @type Number - */ - this.statusCode = template.statusCode; - - /** - * The type string defining which values this parameter may contain, - * as well as what properties are applicable. Valid types are listed - * within Error.Type. - * - * @type String - * @default Error.Type.INTERNAL_ERROR - */ - this.type = template.type || Error.Type.INTERNAL_ERROR; - - /** - * Any parameters which were expected in the original request, or are - * now expected as a result of the original request, if any. If no - * such information is available, this will be null. - * - * @type Field[] - */ - this.expected = template.expected; - - /** - * The outcome for each patch that was submitted as part of the request - * that generated this error, if the request was a directory PATCH - * request. In all other cases, this will be null. - * - * @type DirectoryPatchOutcome[] - */ - this.patches = template.patches || null; - - }; - - /** - * All valid field types. - */ - Error.Type = { - - /** - * The requested operation could not be performed because the request - * itself was malformed. - * - * @type String - */ - BAD_REQUEST : 'BAD_REQUEST', - - /** - * The credentials provided were invalid. - * - * @type String - */ - INVALID_CREDENTIALS : 'INVALID_CREDENTIALS', - - /** - * The credentials provided were not necessarily invalid, but were not - * sufficient to determine validity. - * - * @type String - */ - INSUFFICIENT_CREDENTIALS : 'INSUFFICIENT_CREDENTIALS', - - /** - * An internal server error has occurred. - * - * @type String - */ - INTERNAL_ERROR : 'INTERNAL_ERROR', - - /** - * An object related to the request does not exist. - * - * @type String - */ - NOT_FOUND : 'NOT_FOUND', - - /** - * Permission was denied to perform the requested operation. - * - * @type String - */ - PERMISSION_DENIED : 'PERMISSION_DENIED', - - /** - * An error occurred within an intercepted stream, terminating that - * stream. The Guacamole protocol status code of that error will be - * stored within statusCode. - * - * @type String - */ - STREAM_ERROR : 'STREAM_ERROR' - - }; - - return Error; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/PermissionFlagSet.js b/guacamole/src/main/frontend/src/app/rest/types/PermissionFlagSet.js deleted file mode 100644 index f79e3b9649..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/PermissionFlagSet.js +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the PermissionFlagSet class. - */ -angular.module('rest').factory('PermissionFlagSet', ['PermissionSet', - function definePermissionFlagSet(PermissionSet) { - - /** - * Alternative view of a @link{PermissionSet} which allows manipulation of - * each permission through the setting (or retrieval) of boolean property - * values. - * - * @constructor - * @param {PermissionFlagSet|Object} template - * The object whose properties should be copied within the new - * PermissionFlagSet. - */ - var PermissionFlagSet = function PermissionFlagSet(template) { - - // Use empty object by default - template = template || {}; - - /** - * The granted state of each system permission, as a map of system - * permission type string to boolean value. A particular permission is - * granted if its corresponding boolean value is set to true. Valid - * permission type strings are defined within - * PermissionSet.SystemPermissionType. Permissions which are not - * granted may be set to false, but this is not required. - * - * @type Object. - */ - this.systemPermissions = template.systemPermissions || {}; - - /** - * The granted state of each permission for each connection, as a map - * of object permission type string to permission map. The permission - * map is, in turn, a map of connection identifier to boolean value. A - * particular permission is granted if its corresponding boolean value - * is set to true. Valid permission type strings are defined within - * PermissionSet.ObjectPermissionType. Permissions which are not - * granted may be set to false, but this is not required. - * - * @type Object.> - */ - this.connectionPermissions = template.connectionPermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - /** - * The granted state of each permission for each connection group, as a - * map of object permission type string to permission map. The - * permission map is, in turn, a map of connection group identifier to - * boolean value. A particular permission is granted if its - * corresponding boolean value is set to true. Valid permission type - * strings are defined within PermissionSet.ObjectPermissionType. - * Permissions which are not granted may be set to false, but this is - * not required. - * - * @type Object.> - */ - this.connectionGroupPermissions = template.connectionGroupPermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - /** - * The granted state of each permission for each sharing profile, as a - * map of object permission type string to permission map. The - * permission map is, in turn, a map of sharing profile identifier to - * boolean value. A particular permission is granted if its - * corresponding boolean value is set to true. Valid permission type - * strings are defined within PermissionSet.ObjectPermissionType. - * Permissions which are not granted may be set to false, but this is - * not required. - * - * @type Object.> - */ - this.sharingProfilePermissions = template.sharingProfilePermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - /** - * The granted state of each permission for each active connection, as - * a map of object permission type string to permission map. The - * permission map is, in turn, a map of active connection identifier to - * boolean value. A particular permission is granted if its - * corresponding boolean value is set to true. Valid permission type - * strings are defined within PermissionSet.ObjectPermissionType. - * Permissions which are not granted may be set to false, but this is - * not required. - * - * @type Object.> - */ - this.activeConnectionPermissions = template.activeConnectionPermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - /** - * The granted state of each permission for each user, as a map of - * object permission type string to permission map. The permission map - * is, in turn, a map of username to boolean value. A particular - * permission is granted if its corresponding boolean value is set to - * true. Valid permission type strings are defined within - * PermissionSet.ObjectPermissionType. Permissions which are not - * granted may be set to false, but this is not required. - * - * @type Object.> - */ - this.userPermissions = template.userPermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - /** - * The granted state of each permission for each user group, as a map of - * object permission type string to permission map. The permission map - * is, in turn, a map of group identifier to boolean value. A particular - * permission is granted if its corresponding boolean value is set to - * true. Valid permission type strings are defined within - * PermissionSet.ObjectPermissionType. Permissions which are not - * granted may be set to false, but this is not required. - * - * @type Object.> - */ - this.userGroupPermissions = template.userGroupPermissions || { - 'READ' : {}, - 'UPDATE' : {}, - 'DELETE' : {}, - 'ADMINISTER' : {} - }; - - }; - - /** - * Iterates through all permissions in the given permission map, setting - * the corresponding permission flags in the given permission flag map. - * - * @param {Object.} permMap - * Map of object identifiers to the set of granted permissions. Each - * permission is represented by a string listed within - * PermissionSet.ObjectPermissionType. - * - * @param {Object.>} flagMap - * Map of permission type strings to identifier/flag pairs representing - * whether the permission of that type is granted for the object having - * having the associated identifier. - */ - var addObjectPermissions = function addObjectPermissions(permMap, flagMap) { - - // For each defined identifier in the permission map - for (var identifier in permMap) { - - // Pull the permission array and loop through each permission - var permissions = permMap[identifier]; - permissions.forEach(function addObjectPermission(type) { - - // Get identifier/flag mapping, creating first if necessary - var objectFlags = flagMap[type] = flagMap[type] || {}; - - // Set flag for current permission - objectFlags[identifier] = true; - - }); - - } - - }; - - /** - * Creates a new PermissionFlagSet, populating it with all the permissions - * indicated as granted within the given PermissionSet. - * - * @param {PermissionSet} permissionSet - * The PermissionSet containing the permissions to be copied into a new - * PermissionFlagSet. - * - * @returns {PermissionFlagSet} - * A new PermissionFlagSet containing flags representing all granted - * permissions from the given PermissionSet. - */ - PermissionFlagSet.fromPermissionSet = function fromPermissionSet(permissionSet) { - - var permissionFlagSet = new PermissionFlagSet(); - - // Add all granted system permissions - permissionSet.systemPermissions.forEach(function addSystemPermission(type) { - permissionFlagSet.systemPermissions[type] = true; - }); - - // Add all granted connection permissions - addObjectPermissions(permissionSet.connectionPermissions, permissionFlagSet.connectionPermissions); - - // Add all granted connection group permissions - addObjectPermissions(permissionSet.connectionGroupPermissions, permissionFlagSet.connectionGroupPermissions); - - // Add all granted sharing profile permissions - addObjectPermissions(permissionSet.sharingProfilePermissions, permissionFlagSet.sharingProfilePermissions); - - // Add all granted active connection permissions - addObjectPermissions(permissionSet.activeConnectionPermissions, permissionFlagSet.activeConnectionPermissions); - - // Add all granted user permissions - addObjectPermissions(permissionSet.userPermissions, permissionFlagSet.userPermissions); - - // Add all granted user group permissions - addObjectPermissions(permissionSet.userGroupPermissions, permissionFlagSet.userGroupPermissions); - - return permissionFlagSet; - - }; - - return PermissionFlagSet; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/PermissionPatch.js b/guacamole/src/main/frontend/src/app/rest/types/PermissionPatch.js deleted file mode 100644 index dda440b5c8..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/PermissionPatch.js +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the PermissionPatch class. - */ -angular.module('rest').factory('PermissionPatch', [function definePermissionPatch() { - - /** - * The object returned by REST API calls when representing changes to the - * permissions granted to a specific user. - * - * @constructor - * @param {PermissionPatch|Object} [template={}] - * The object whose properties should be copied within the new - * PermissionPatch. - */ - var PermissionPatch = function PermissionPatch(template) { - - // Use empty object by default - template = template || {}; - - /** - * The operation to apply to the permissions indicated by the path. - * Valid operation values are defined within PermissionPatch.Operation. - * - * @type String - */ - this.op = template.op; - - /** - * The path of the permissions to modify. Depending on the type of the - * permission, this will be either "/connectionPermissions/ID", - * "/connectionGroupPermissions/ID", "/userPermissions/ID", or - * "/systemPermissions", where "ID" is the identifier of the object - * to which the permissions apply, if any. - * - * @type String - */ - this.path = template.path; - - /** - * The permissions being added or removed. If the permission applies to - * an object, such as a connection or connection group, this will be a - * value from PermissionSet.ObjectPermissionType. If the permission - * applies to the system as a whole (the path is "/systemPermissions"), - * this will be a value from PermissionSet.SystemPermissionType. - * - * @type String - */ - this.value = template.value; - - }; - - /** - * All valid patch operations for permissions. Currently, only add and - * remove are supported. - */ - PermissionPatch.Operation = { - - /** - * Adds (grants) the specified permission. - */ - ADD : "add", - - /** - * Removes (revokes) the specified permission. - */ - REMOVE : "remove" - - }; - - return PermissionPatch; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/SharingProfile.js b/guacamole/src/main/frontend/src/app/rest/types/SharingProfile.js deleted file mode 100644 index 50f1307d59..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/SharingProfile.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the SharingProfile class. - */ -angular.module('rest').factory('SharingProfile', [function defineSharingProfile() { - - /** - * The object returned by REST API calls when representing the data - * associated with a sharing profile. - * - * @constructor - * @param {SharingProfile|Object} [template={}] - * The object whose properties should be copied within the new - * SharingProfile. - */ - var SharingProfile = function SharingProfile(template) { - - // Use empty object by default - template = template || {}; - - /** - * The unique identifier associated with this sharing profile. - * - * @type String - */ - this.identifier = template.identifier; - - /** - * The unique identifier of the connection that this sharing profile - * can be used to share. - * - * @type String - */ - this.primaryConnectionIdentifier = template.primaryConnectionIdentifier; - - /** - * The human-readable name of this sharing profile, which is not - * necessarily unique. - * - * @type String - */ - this.name = template.name; - - /** - * Connection configuration parameters, as dictated by the protocol in - * use by the primary connection, arranged as name/value pairs. This - * information may not be available until directly queried. If this - * information is unavailable, this property will be null or undefined. - * - * @type Object. - */ - this.parameters = template.parameters; - - /** - * Arbitrary name/value pairs which further describe this sharing - * profile. The semantics and validity of these attributes are dictated - * by the extension which defines them. - * - * @type Object. - */ - this.attributes = template.attributes || {}; - - }; - - return SharingProfile; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/TranslatableMessage.js b/guacamole/src/main/frontend/src/app/rest/types/TranslatableMessage.js deleted file mode 100644 index 454224840c..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/TranslatableMessage.js +++ /dev/null @@ -1,63 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the TranslatableMessage class. - */ -angular.module('rest').factory('TranslatableMessage', [function defineTranslatableMessage() { - - /** - * The object returned by REST API calls when representing a message which - * can be translated using the translation service, providing a translation - * key and optional set of values to be substituted into the translation - * string associated with that key. - * - * @constructor - * @param {TranslatableMessage|Object} [template={}] - * The object whose properties should be copied within the new - * TranslatableMessage. - */ - var TranslatableMessage = function TranslatableMessage(template) { - - // Use empty object by default - template = template || {}; - - /** - * The key associated with the translation string that used when - * displaying this message. - * - * @type String - */ - this.key = template.key; - - /** - * The object which should be passed through to the translation service - * for the sake of variable substitution. Each property of the provided - * object will be substituted for the variable of the same name within - * the translation string. - * - * @type Object - */ - this.variables = template.variables; - - }; - - return TranslatableMessage; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/rest/types/User.js b/guacamole/src/main/frontend/src/app/rest/types/User.js deleted file mode 100644 index c74d1cd8a1..0000000000 --- a/guacamole/src/main/frontend/src/app/rest/types/User.js +++ /dev/null @@ -1,125 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Service which defines the User class. - */ -angular.module('rest').factory('User', [function defineUser() { - - /** - * The object returned by REST API calls when representing the data - * associated with a user. - * - * @constructor - * @param {User|Object} [template={}] - * The object whose properties should be copied within the new - * User. - */ - var User = function User(template) { - - // Use empty object by default - template = template || {}; - - /** - * The name which uniquely identifies this user. - * - * @type String - */ - this.username = template.username; - - /** - * This user's password. Note that the REST API may not populate this - * property for the sake of security. In most cases, it's not even - * possible for the authentication layer to retrieve the user's true - * password. - * - * @type String - */ - this.password = template.password; - - /** - * The time that this user was last logged in, in milliseconds since - * 1970-01-01 00:00:00 UTC. If this information is unknown or - * unavailable, this will be null. - * - * @type Number - */ - this.lastActive = template.lastActive; - - /** - * True if this user account is disabled, otherwise false. - * - * @type boolean - */ - this.disabled = template.disabled; - - /** - * Arbitrary name/value pairs which further describe this user. The - * semantics and validity of these attributes are dictated by the - * extension which defines them. - * - * @type Object. - */ - this.attributes = template.attributes || {}; - - }; - - /** - * All standard attribute names with semantics defined by the Guacamole web - * application. Extensions may additionally define their own attributes - * with completely arbitrary names and semantics, so long as those names do - * not conflict with the names listed here. All standard attribute names - * have a "guac-" prefix to avoid such conflicts. - */ - User.Attributes = { - - /** - * The user's full name. - * - * @type String - */ - FULL_NAME : 'guac-full-name', - - /** - * The email address of the user. - * - * @type String - */ - EMAIL_ADDRESS : 'guac-email-address', - - /** - * The organization, company, group, etc. that the user belongs to. - * - * @type String - */ - ORGANIZATION : 'guac-organization', - - /** - * The role that the user has at the organization, company, group, etc. - * they belong to. - * - * @type String - */ - ORGANIZATIONAL_ROLE : 'guac-organizational-role' - - }; - - return User; - -}]); \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js b/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js deleted file mode 100644 index 12c368be32..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/controllers/connectionHistoryPlayerController.js +++ /dev/null @@ -1,50 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The controller for the session recording player page. - */ -angular.module('settings').controller('connectionHistoryPlayerController', ['$scope', '$injector', - function connectionHistoryPlayerController($scope, $injector) { - - // Required services - const authenticationService = $injector.get('authenticationService'); - const $routeParams = $injector.get('$routeParams'); - - /** - * The URL of the REST API resource exposing the requested session - * recording. - * - * @type {!string} - */ - const recordingURL = 'api/session/data/' + encodeURIComponent($routeParams.dataSource) - + '/history/connections/' + encodeURIComponent($routeParams.identifier) - + '/logs/' + encodeURIComponent($routeParams.name); - - /** - * The tunnel which should be used to download the Guacamole session - * recording. - * - * @type Guacamole.Tunnel - */ - $scope.tunnel = new Guacamole.StaticHTTPTunnel(recordingURL, false, { - 'Guacamole-Token' : authenticationService.getCurrentToken() - }); - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js deleted file mode 100644 index f52fc0f9dd..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnectionHistory.js +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for viewing connection history records. - */ -angular.module('settings').directive('guacSettingsConnectionHistory', [function guacSettingsConnectionHistory() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: { - }, - - templateUrl: 'app/settings/templates/settingsConnectionHistory.html', - controller: ['$scope', '$injector', function settingsConnectionHistoryController($scope, $injector) { - - // Get required types - var ConnectionHistoryEntryWrapper = $injector.get('ConnectionHistoryEntryWrapper'); - var FilterToken = $injector.get('FilterToken'); - var SortOrder = $injector.get('SortOrder'); - - // Get required services - var $filter = $injector.get('$filter'); - var $routeParams = $injector.get('$routeParams'); - var $translate = $injector.get('$translate'); - var csvService = $injector.get('csvService'); - var historyService = $injector.get('historyService'); - var requestService = $injector.get('requestService'); - - /** - * The identifier of the currently-selected data source. - * - * @type String - */ - $scope.dataSource = $routeParams.dataSource; - - /** - * All wrapped matching connection history entries, or null if these - * entries have not yet been retrieved. - * - * @type ConnectionHistoryEntryWrapper[] - */ - $scope.historyEntryWrappers = null; - - /** - * The search terms to use when filtering the history records. - * - * @type String - */ - $scope.searchString = ''; - - /** - * The date format for use for start/end dates. - * - * @type String - */ - $scope.dateFormat = null; - - /** - * SortOrder instance which stores the sort order of the history - * records. - * - * @type SortOrder - */ - $scope.order = new SortOrder([ - '-entry.startDate', - '-duration', - 'entry.username', - 'entry.connectionName', - 'entry.remoteHost' - ]); - - /** - * The names of sortable properties supported by the REST API that - * correspond to the properties that may be stored within - * $scope.order. - * - * @type {!Object.} - */ - const apiSortProperties = { - 'entry.startDate' : 'startDate', - '-entry.startDate' : '-startDate' - }; - - /** - * Converts the given sort predicate to a correponding array of - * sortable properties supported by the REST API. Any properties - * within the predicate that are not supported will be dropped. - * - * @param {!string[]} predicate - * The sort predicate to convert, as exposed by the predicate - * property of SortOrder. - * - * @returns {!string[]} - * A corresponding array of sortable properties, omitting any - * properties not supported by the REST API. - */ - var toAPISortPredicate = function toAPISortPredicate(predicate) { - return predicate - .map((name) => apiSortProperties[name]) - .filter((name) => !!name); - }; - - // Get session date format - $translate('SETTINGS_CONNECTION_HISTORY.FORMAT_DATE') - .then(function dateFormatReceived(retrievedDateFormat) { - - // Store received date format - $scope.dateFormat = retrievedDateFormat; - - }, angular.noop); - - /** - * Returns true if the connection history records have been loaded, - * indicating that information needed to render the page is fully - * loaded. - * - * @returns {Boolean} - * true if the history records have been loaded, false - * otherwise. - * - */ - $scope.isLoaded = function isLoaded() { - return $scope.historyEntryWrappers !== null - && $scope.dateFormat !== null; - }; - - /** - * Returns whether the search has completed but contains no history - * records. This function will return false if there are history - * records in the results OR if the search has not yet completed. - * - * @returns {Boolean} - * true if the search results have been loaded but no history - * records are present, false otherwise. - */ - $scope.isHistoryEmpty = function isHistoryEmpty() { - return $scope.isLoaded() && $scope.historyEntryWrappers.length === 0; - }; - - /** - * Query the API for the connection record history, filtered by - * searchString, and ordered by order. - */ - $scope.search = function search() { - - // Clear current results - $scope.historyEntryWrappers = null; - - // Tokenize search string - var tokens = FilterToken.tokenize($scope.searchString); - - // Transform tokens into list of required string contents - var requiredContents = []; - angular.forEach(tokens, function addRequiredContents(token) { - - // Transform depending on token type - switch (token.type) { - - // For string literals, use parsed token value - case 'LITERAL': - requiredContents.push(token.value); - - // Ignore whitespace - case 'WHITESPACE': - break; - - // For all other token types, use the relevant portion - // of the original search string - default: - requiredContents.push(token.consumed); - - } - - }); - - // Fetch history records - historyService.getConnectionHistory( - $scope.dataSource, - requiredContents, - toAPISortPredicate($scope.order.predicate) - ) - .then(function historyRetrieved(historyEntries) { - - // Wrap all history entries for sake of display - $scope.historyEntryWrappers = []; - angular.forEach(historyEntries, function wrapHistoryEntry(historyEntry) { - $scope.historyEntryWrappers.push(new ConnectionHistoryEntryWrapper($scope.dataSource, historyEntry)); - }); - - }, requestService.DIE); - - }; - - /** - * Initiates a download of a CSV version of the displayed history - * search results. - */ - $scope.downloadCSV = function downloadCSV() { - - // Translate CSV header - $translate([ - 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME', - 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE', - 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION', - 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME', - 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST', - 'SETTINGS_CONNECTION_HISTORY.FILENAME_HISTORY_CSV' - ]).then(function headerTranslated(translations) { - - // Initialize records with translated header row - var records = [[ - translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME'], - translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE'], - translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION'], - translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME'], - translations['SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST'] - ]]; - - // Add rows for all history entries, using the same sort - // order as the displayed table - angular.forEach( - $filter('orderBy')( - $scope.historyEntryWrappers, - $scope.order.predicate - ), - function pushRecord(historyEntryWrapper) { - records.push([ - historyEntryWrapper.entry.username, - $filter('date')(historyEntryWrapper.entry.startDate, $scope.dateFormat), - historyEntryWrapper.duration / 1000, - historyEntryWrapper.entry.connectionName, - historyEntryWrapper.entry.remoteHost - ]); - } - ); - - // Save the result - saveAs(csvService.toBlob(records), translations['SETTINGS_CONNECTION_HISTORY.FILENAME_HISTORY_CSV']); - - }, angular.noop); - - }; - - // Initialize search results - $scope.search(); - - }] - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnections.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnections.js deleted file mode 100644 index 38bb343b1b..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsConnections.js +++ /dev/null @@ -1,443 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for managing all connections and connection groups in the system. - */ -angular.module('settings').directive('guacSettingsConnections', [function guacSettingsConnections() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: { - }, - - templateUrl: 'app/settings/templates/settingsConnections.html', - controller: ['$scope', '$injector', function settingsConnectionsController($scope, $injector) { - - // Required types - var ConnectionGroup = $injector.get('ConnectionGroup'); - var GroupListItem = $injector.get('GroupListItem'); - var PermissionSet = $injector.get('PermissionSet'); - - // Required services - var $location = $injector.get('$location'); - var $routeParams = $injector.get('$routeParams'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var guacNotification = $injector.get('guacNotification'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - - /** - * The identifier of the current user. - * - * @type String - */ - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * The identifier of the currently-selected data source. - * - * @type String - */ - $scope.dataSource = $routeParams.dataSource; - - /** - * The root connection group of the connection group hierarchy. - * - * @type Object. - */ - $scope.rootGroups = null; - - /** - * All permissions associated with the current user, or null if the - * user's permissions have not yet been loaded. - * - * @type PermissionSet - */ - $scope.permissions = null; - - /** - * Array of all connection properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionProperties = [ - 'name', - 'protocol' - ]; - - /** - * Array of all connection group properties that are filterable. - * - * @type String[] - */ - $scope.filteredConnectionGroupProperties = [ - 'name' - ]; - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface - * to be useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.rootGroup !== null - && $scope.permissions !== null; - - }; - - /** - * Returns whether the current user has the ADMINISTER system - * permission (i.e. they are an administrator). - * - * @return {Boolean} - * true if the current user is an administrator. - */ - $scope.canAdminister = function canAdminister() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Return whether the current user is an administrator - return PermissionSet.hasSystemPermission( - $scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER); - }; - - /** - * Returns whether the current user can create new connections - * within the current data source. - * - * @return {Boolean} - * true if the current user can create new connections within - * the current data source, false otherwise. - */ - $scope.canCreateConnections = function canCreateConnections() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Can create connections if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)) - return true; - - // No data sources allow connection creation - return false; - - }; - - /** - * Returns whether the current user can create new connection - * groups within the current data source. - * - * @return {Boolean} - * true if the current user can create new connection groups - * within the current data source, false otherwise. - */ - $scope.canCreateConnectionGroups = function canCreateConnectionGroups() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Can create connections groups if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)) - return true; - - // No data sources allow connection group creation - return false; - - }; - - /** - * Returns whether the current user can create new sharing profiles - * within the current data source. - * - * @return {Boolean} - * true if the current user can create new sharing profiles - * within the current data source, false otherwise. - */ - $scope.canCreateSharingProfiles = function canCreateSharingProfiles() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Can create sharing profiles if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE)) - return true; - - // Current data source does not allow sharing profile creation - return false; - - }; - - /** - * Returns whether the current user can create new connections or - * connection groups or make changes to existing connections or - * connection groups within the current data source. The - * connection management interface as a whole is useless if this - * function returns false. - * - * @return {Boolean} - * true if the current user can create new connections/groups - * or make changes to existing connections/groups within the - * current data source, false otherwise. - */ - $scope.canManageConnections = function canManageConnections() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Creating connections/groups counts as management - if ($scope.canCreateConnections() - || $scope.canCreateConnectionGroups() - || $scope.canCreateSharingProfiles()) - return true; - - // Can manage connections if granted explicit update or delete - if (PermissionSet.hasConnectionPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionPermission($scope.permissions, PermissionSet.ObjectPermissionType.DELETE)) - return true; - - // Can manage connections groups if granted explicit update or delete - if (PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.DELETE)) - return true; - - // No data sources allow management of connections or groups - return false; - - }; - - /** - * Returns whether the current user can update the connection having - * the given identifier within the current data source. - * - * @param {String} identifier - * The identifier of the connection to check. - * - * @return {Boolean} - * true if the current user can update the connection having the - * given identifier within the current data source, false - * otherwise. - */ - $scope.canUpdateConnection = function canUpdateConnection(identifier) { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Can update the connection if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier)) - return true; - - // Current data sources does not allow the connection to be updated - return false; - - }; - - /** - * Returns whether the current user can update the connection group - * having the given identifier within the current data source. - * - * @param {String} identifier - * The identifier of the connection group to check. - * - * @return {Boolean} - * true if the current user can update the connection group - * having the given identifier within the current data source, - * false otherwise. - */ - $scope.canUpdateConnectionGroup = function canUpdateConnectionGroup(identifier) { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Can update the connection if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission($scope.permissions, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier)) - return true; - - // Current data sources does not allow the connection group to be updated - return false; - - }; - - /** - * Adds connection-group-specific contextual actions to the given - * array of GroupListItems. Each contextual action will be - * represented by a new GroupListItem. - * - * @param {GroupListItem[]} items - * The array of GroupListItems to which new GroupListItems - * representing connection-group-specific contextual actions - * should be added. - * - * @param {GroupListItem} [parent] - * The GroupListItem representing the connection group which - * contains the given array of GroupListItems, if known. - */ - var addConnectionGroupActions = function addConnectionGroupActions(items, parent) { - - // Do nothing if we lack permission to modify the parent at all - if (parent && !$scope.canUpdateConnectionGroup(parent.identifier)) - return; - - // Add action for creating a child connection, if the user has - // permission to do so - if ($scope.canCreateConnections()) - items.push(new GroupListItem({ - type : 'new-connection', - dataSource : $scope.dataSource, - weight : 1, - wrappedItem : parent - })); - - // Add action for creating a child connection group, if the user - // has permission to do so - if ($scope.canCreateConnectionGroups()) - items.push(new GroupListItem({ - type : 'new-connection-group', - dataSource : $scope.dataSource, - weight : 1, - wrappedItem : parent - })); - - }; - - /** - * Adds connection-specific contextual actions to the given array of - * GroupListItems. Each contextual action will be represented by a - * new GroupListItem. - * - * @param {GroupListItem[]} items - * The array of GroupListItems to which new GroupListItems - * representing connection-specific contextual actions should - * be added. - * - * @param {GroupListItem} [parent] - * The GroupListItem representing the connection which contains - * the given array of GroupListItems, if known. - */ - var addConnectionActions = function addConnectionActions(items, parent) { - - // Do nothing if we lack permission to modify the parent at all - if (parent && !$scope.canUpdateConnection(parent.identifier)) - return; - - // Add action for creating a child sharing profile, if the user - // has permission to do so - if ($scope.canCreateSharingProfiles()) - items.push(new GroupListItem({ - type : 'new-sharing-profile', - dataSource : $scope.dataSource, - weight : 1, - wrappedItem : parent - })); - - }; - - /** - * Decorates the given GroupListItem, including all descendants, - * adding contextual actions. - * - * @param {GroupListItem} item - * The GroupListItem which should be decorated with additional - * GroupListItems representing contextual actions. - */ - var decorateItem = function decorateItem(item) { - - // If the item is a connection group, add actions specific to - // connection groups - if (item.type === GroupListItem.Type.CONNECTION_GROUP) - addConnectionGroupActions(item.children, item); - - // If the item is a connection, add actions specific to - // connections - else if (item.type === GroupListItem.Type.CONNECTION) - addConnectionActions(item.children, item); - - // Decorate all children - angular.forEach(item.children, decorateItem); - - }; - - /** - * Callback which decorates all items within the given array of - * GroupListItems, including their descendants, adding contextual - * actions. - * - * @param {GroupListItem[]} items - * The array of GroupListItems which should be decorated with - * additional GroupListItems representing contextual actions. - */ - $scope.rootItemDecorator = function rootItemDecorator(items) { - - // Decorate each root-level item - angular.forEach(items, decorateItem); - - }; - - // Retrieve current permissions - permissionService.getEffectivePermissions($scope.dataSource, currentUsername) - .then(function permissionsRetrieved(permissions) { - - // Store retrieved permissions - $scope.permissions = permissions; - - // Ignore permission to update root group - PermissionSet.removeConnectionGroupPermission($scope.permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); - - // Return to home if there's nothing to do here - if (!$scope.canManageConnections()) - $location.path('/'); - - // Retrieve all connections for which we have UPDATE or DELETE permission - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - [$scope.dataSource], - ConnectionGroup.ROOT_IDENTIFIER, - [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] - ) - .then(function connectionGroupsReceived(rootGroups) { - $scope.rootGroups = rootGroups; - }, requestService.DIE); - - }, requestService.DIE); // end retrieve permissions - - }] - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js deleted file mode 100644 index 347c751774..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsPreferences.js +++ /dev/null @@ -1,269 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for managing preferences local to the current user. - */ -angular.module('settings').directive('guacSettingsPreferences', [function guacSettingsPreferences() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: {}, - - templateUrl: 'app/settings/templates/settingsPreferences.html', - controller: ['$scope', '$injector', function settingsPreferencesController($scope, $injector) { - - // Get required types - const Form = $injector.get('Form'); - const PermissionSet = $injector.get('PermissionSet'); - - // Required services - const $translate = $injector.get('$translate'); - const authenticationService = $injector.get('authenticationService'); - const guacNotification = $injector.get('guacNotification'); - const permissionService = $injector.get('permissionService'); - const preferenceService = $injector.get('preferenceService'); - const requestService = $injector.get('requestService'); - const schemaService = $injector.get('schemaService'); - const userService = $injector.get('userService'); - - /** - * An action to be provided along with the object sent to - * showStatus which closes the currently-shown status dialog. - */ - var ACKNOWLEDGE_ACTION = { - name : 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', - // Handle action - callback : function acknowledgeCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * An action which closes the current dialog, and refreshes - * the user data on dialog close. - */ - const ACKNOWLEDGE_ACTION_RELOAD = { - name : 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', - // Handle action - callback : function acknowledgeCallback() { - userService.getUser(dataSource, username) - .then(user => $scope.user = user) - .then(() => guacNotification.showStatus(false)); - } - }; - - /** - * The user being modified. - * - * @type User - */ - $scope.user = null; - - /** - * The username of the current user. - * - * @type String - */ - var username = authenticationService.getCurrentUsername(); - - /** - * The identifier of the data source which authenticated the - * current user. - * - * @type String - */ - var dataSource = authenticationService.getDataSource(); - - /** - * All currently-set preferences, or their defaults if not yet set. - * - * @type Object. - */ - $scope.preferences = preferenceService.preferences; - - /** - * All available user attributes. This is only the set of attribute - * definitions, organized as logical groupings of attributes, not attribute - * values. - * - * @type Form[] - */ - $scope.attributes = null; - - /** - * The fields which should be displayed for choosing locale - * preferences. Each field name must be a property on - * $scope.preferences. - * - * @type Field[] - */ - $scope.localeFields = [ - { 'type' : 'LANGUAGE', 'name' : 'language' }, - { 'type' : 'TIMEZONE', 'name' : 'timezone' } - ]; - - // Automatically update applied translation when language preference is changed - $scope.$watch('preferences.language', function changeLanguage(language) { - $translate.use(language); - }); - - /** - * The new password for the user. - * - * @type String - */ - $scope.newPassword = null; - - /** - * The password match for the user. The update password action will - * fail if $scope.newPassword !== $scope.passwordMatch. - * - * @type String - */ - $scope.newPasswordMatch = null; - - /** - * Whether the current user can edit themselves - i.e. update their - * password or change user preference attributes, or null if this - * is not yet known. - * - * @type Boolean - */ - $scope.canUpdateSelf = null; - - /** - * Update the current user's password to the password currently set within - * the password change dialog. - */ - $scope.updatePassword = function updatePassword() { - - // Verify passwords match - if ($scope.newPasswordMatch !== $scope.newPassword) { - guacNotification.showStatus({ - className : 'error', - title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', - text : { - key : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_MISMATCH' - }, - actions : [ ACKNOWLEDGE_ACTION ] - }); - return; - } - - // Verify that the new password is not blank - if (!$scope.newPassword) { - guacNotification.showStatus({ - className : 'error', - title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', - text : { - key : 'SETTINGS_PREFERENCES.ERROR_PASSWORD_BLANK' - }, - actions : [ ACKNOWLEDGE_ACTION ] - }); - return; - } - - // Save the user with the new password - userService.updateUserPassword(dataSource, username, $scope.oldPassword, $scope.newPassword) - .then(function passwordUpdated() { - - // Clear the password fields - $scope.oldPassword = null; - $scope.newPassword = null; - $scope.newPasswordMatch = null; - - // Indicate that the password has been changed - guacNotification.showStatus({ - text : { - key : 'SETTINGS_PREFERENCES.INFO_PASSWORD_CHANGED' - }, - actions : [ ACKNOWLEDGE_ACTION ] - }); - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - // Retrieve current permissions - permissionService.getEffectivePermissions(dataSource, username) - .then(function permissionsRetrieved(permissions) { - - // Add action for updaing password or user preferences if permission is granted - $scope.canUpdateSelf = ( - - // If permission is explicitly granted - PermissionSet.hasUserPermission(permissions, - PermissionSet.ObjectPermissionType.UPDATE, username) - - // Or if implicitly granted through being an administrator - || PermissionSet.hasSystemPermission(permissions, - PermissionSet.SystemPermissionType.ADMINISTER)); - - }) - ['catch'](requestService.createErrorCallback(function permissionsFailed(error) { - $scope.canUpdateSelf = false; - })); - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface to be - * useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.canUpdateSelf !== null - && $scope.languages !== null; - - }; - - - /** - * Saves the current user, displaying an acknowledgement message if - * saving was successful, or an error if the save failed. - */ - $scope.saveUser = function saveUser() { - return userService.saveUser(dataSource, $scope.user) - .then(() => guacNotification.showStatus({ - text : { - key : 'SETTINGS_PREFERENCES.INFO_PREFERENCE_ATTRIBUTES_CHANGED' - }, - - // Reload the user on successful save in case any attributes changed - actions : [ ACKNOWLEDGE_ACTION_RELOAD ] - }), - guacNotification.SHOW_REQUEST_ERROR); - }; - - // Fetch the user record - userService.getUser(dataSource, username).then(function saveUserData(user) { - $scope.user = user; - }); - - // Fetch all user preference attribute forms defined - schemaService.getUserPreferenceAttributes(dataSource).then(function saveAttributes(attributes) { - $scope.attributes = attributes; - }); - }] - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsSessions.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsSessions.js deleted file mode 100644 index 30b8bdeb0b..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsSessions.js +++ /dev/null @@ -1,416 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for managing all active Guacamole sessions. - */ -angular.module('settings').directive('guacSettingsSessions', [function guacSettingsSessions() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: { - }, - - templateUrl: 'app/settings/templates/settingsSessions.html', - controller: ['$scope', '$injector', function settingsSessionsController($scope, $injector) { - - // Required types - var ActiveConnectionWrapper = $injector.get('ActiveConnectionWrapper'); - var ClientIdentifier = $injector.get('ClientIdentifier'); - var ConnectionGroup = $injector.get('ConnectionGroup'); - var SortOrder = $injector.get('SortOrder'); - - // Required services - var $filter = $injector.get('$filter'); - var $translate = $injector.get('$translate'); - var $q = $injector.get('$q'); - var activeConnectionService = $injector.get('activeConnectionService'); - var authenticationService = $injector.get('authenticationService'); - var connectionGroupService = $injector.get('connectionGroupService'); - var dataSourceService = $injector.get('dataSourceService'); - var guacNotification = $injector.get('guacNotification'); - var requestService = $injector.get('requestService'); - - /** - * The identifiers of all data sources accessible by the current - * user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * The ActiveConnectionWrappers of all active sessions accessible - * by the current user, or null if the active sessions have not yet - * been loaded. - * - * @type ActiveConnectionWrapper[] - */ - $scope.wrappers = null; - - /** - * SortOrder instance which maintains the sort order of the visible - * connection wrappers. - * - * @type SortOrder - */ - $scope.wrapperOrder = new SortOrder([ - 'activeConnection.username', - 'startDate', - 'activeConnection.remoteHost', - 'name' - ]); - - /** - * Array of all wrapper properties that are filterable. - * - * @type String[] - */ - $scope.filteredWrapperProperties = [ - 'activeConnection.username', - 'startDate', - 'activeConnection.remoteHost', - 'name' - ]; - - /** - * All active connections, if known, grouped by corresponding data - * source identifier, or null if active connections have not yet - * been loaded. - * - * @type Object.> - */ - var allActiveConnections = null; - - /** - * Map of all visible connections by data source identifier and - * object identifier, or null if visible connections have not yet - * been loaded. - * - * @type Object.> - */ - var allConnections = null; - - /** - * The date format for use for session-related dates. - * - * @type String - */ - var sessionDateFormat = null; - - /** - * Map of all currently-selected active connection wrappers by - * data source and identifier. - * - * @type Object.> - */ - var allSelectedWrappers = {}; - - /** - * Adds the given connection to the internal set of visible - * connections. - * - * @param {String} dataSource - * The identifier of the data source associated with the given - * connection. - * - * @param {Connection} connection - * The connection to add to the internal set of visible - * connections. - */ - var addConnection = function addConnection(dataSource, connection) { - - // Add given connection to set of visible connections - allConnections[dataSource][connection.identifier] = connection; - - }; - - /** - * Adds all descendant connections of the given connection group to - * the internal set of connections. - * - * @param {String} dataSource - * The identifier of the data source associated with the given - * connection group. - * - * @param {ConnectionGroup} connectionGroup - * The connection group whose descendant connections should be - * added to the internal set of connections. - */ - var addDescendantConnections = function addDescendantConnections(dataSource, connectionGroup) { - - // Add all child connections - angular.forEach(connectionGroup.childConnections, function addConnectionForDataSource(connection) { - addConnection(dataSource, connection); - }); - - // Add all child connection groups - angular.forEach(connectionGroup.childConnectionGroups, function addConnectionGroupForDataSource(connectionGroup) { - addDescendantConnections(dataSource, connectionGroup); - }); - - }; - - /** - * Wraps all loaded active connections, storing the resulting array - * within the scope. If required data has not yet finished loading, - * this function has no effect. - */ - var wrapAllActiveConnections = function wrapAllActiveConnections() { - - // Abort if not all required data is available - if (!allActiveConnections || !allConnections || !sessionDateFormat) - return; - - // Wrap all active connections for sake of display - $scope.wrappers = []; - angular.forEach(allActiveConnections, function wrapActiveConnections(activeConnections, dataSource) { - angular.forEach(activeConnections, function wrapActiveConnection(activeConnection, identifier) { - - // Retrieve corresponding connection - var connection = allConnections[dataSource][activeConnection.connectionIdentifier]; - - // Add wrapper - if (activeConnection.username !== null) { - $scope.wrappers.push(new ActiveConnectionWrapper({ - dataSource : dataSource, - name : connection.name, - startDate : $filter('date')(activeConnection.startDate, sessionDateFormat), - activeConnection : activeConnection - })); - } - - }); - }); - - }; - - // Retrieve all connections - dataSourceService.apply( - connectionGroupService.getConnectionGroupTree, - dataSources, - ConnectionGroup.ROOT_IDENTIFIER - ) - .then(function connectionGroupsReceived(rootGroups) { - - allConnections = {}; - - // Load connections from each received root group - angular.forEach(rootGroups, function connectionGroupReceived(rootGroup, dataSource) { - allConnections[dataSource] = {}; - addDescendantConnections(dataSource, rootGroup); - }); - - // Attempt to produce wrapped list of active connections - wrapAllActiveConnections(); - - }, requestService.DIE); - - // Query active sessions - dataSourceService.apply( - activeConnectionService.getActiveConnections, - dataSources - ) - .then(function sessionsRetrieved(retrievedActiveConnections) { - - // Store received map of active connections - allActiveConnections = retrievedActiveConnections; - - // Attempt to produce wrapped list of active connections - wrapAllActiveConnections(); - - }, requestService.DIE); - - // Get session date format - $translate('SETTINGS_SESSIONS.FORMAT_STARTDATE').then(function sessionDateFormatReceived(retrievedSessionDateFormat) { - - // Store received date format - sessionDateFormat = retrievedSessionDateFormat; - - // Attempt to produce wrapped list of active connections - wrapAllActiveConnections(); - - }, angular.noop); - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface - * to be useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - return $scope.wrappers !== null; - }; - - /** - * An action to be provided along with the object sent to - * showStatus which closes the currently-shown status dialog. - */ - var CANCEL_ACTION = { - name : "SETTINGS_SESSIONS.ACTION_CANCEL", - // Handle action - callback : function cancelCallback() { - guacNotification.showStatus(false); - } - }; - - /** - * An action to be provided along with the object sent to - * showStatus which immediately deletes the currently selected - * sessions. - */ - var DELETE_ACTION = { - name : "SETTINGS_SESSIONS.ACTION_DELETE", - className : "danger", - // Handle action - callback : function deleteCallback() { - deleteAllSessionsImmediately(); - guacNotification.showStatus(false); - } - }; - - /** - * Immediately deletes the selected sessions, without prompting the - * user for confirmation. - */ - var deleteAllSessionsImmediately = function deleteAllSessionsImmediately() { - - var deletionRequests = []; - - // Perform deletion for each relevant data source - angular.forEach(allSelectedWrappers, function deleteSessionsImmediately(selectedWrappers, dataSource) { - - // Delete sessions, if any are selected - var identifiers = Object.keys(selectedWrappers); - if (identifiers.length) - deletionRequests.push(activeConnectionService.deleteActiveConnections(dataSource, identifiers)); - - }); - - // Update interface - $q.all(deletionRequests) - .then(function activeConnectionsDeleted() { - - // Remove deleted connections from wrapper array - $scope.wrappers = $scope.wrappers.filter(function activeConnectionStillExists(wrapper) { - return !(wrapper.activeConnection.identifier in (allSelectedWrappers[wrapper.dataSource] || {})); - }); - - // Clear selection - allSelectedWrappers = {}; - - }, guacNotification.SHOW_REQUEST_ERROR); - - }; - - /** - * Delete all selected sessions, prompting the user first to - * confirm that deletion is desired. - */ - $scope.deleteSessions = function deleteSessions() { - // Confirm deletion request - guacNotification.showStatus({ - 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_CONFIRM_DELETE', - 'text' : { - 'key' : 'SETTINGS_SESSIONS.TEXT_CONFIRM_DELETE' - }, - 'actions' : [ DELETE_ACTION, CANCEL_ACTION] - }); - }; - - /** - * Returns the relative URL of the client page which accesses the - * given active connection. If the active connection is not - * connectable, null is returned. - * - * @param {String} dataSource - * The unique identifier of the data source containing the - * active connection. - * - * @param {String} activeConnection - * The active connection to determine the relative URL of. - * - * @returns {String} - * The relative URL of the client page which accesses the given - * active connection, or null if the active connection is not - * connectable. - */ - $scope.getClientURL = function getClientURL(dataSource, activeConnection) { - - if (!activeConnection.connectable) - return null; - - return '#/client/' + encodeURIComponent(ClientIdentifier.toString({ - dataSource : dataSource, - type : ClientIdentifier.Types.ACTIVE_CONNECTION, - id : activeConnection.identifier - })); - - }; - - /** - * Returns whether the selected sessions can be deleted. - * - * @returns {Boolean} - * true if selected sessions can be deleted, false otherwise. - */ - $scope.canDeleteSessions = function canDeleteSessions() { - - // We can delete sessions if at least one is selected - for (var dataSource in allSelectedWrappers) { - for (var identifier in allSelectedWrappers[dataSource]) - return true; - } - - return false; - - }; - - /** - * Called whenever an active connection wrapper changes selected - * status. - * - * @param {ActiveConnectionWrapper} wrapper - * The wrapper whose selected status has changed. - */ - $scope.wrapperSelectionChange = function wrapperSelectionChange(wrapper) { - - // Get selection map for associated data source, creating if necessary - var selectedWrappers = allSelectedWrappers[wrapper.dataSource]; - if (!selectedWrappers) - selectedWrappers = allSelectedWrappers[wrapper.dataSource] = {}; - - // Add wrapper to map if selected - if (wrapper.checked) - selectedWrappers[wrapper.activeConnection.identifier] = wrapper; - - // Otherwise, remove wrapper from map - else - delete selectedWrappers[wrapper.activeConnection.identifier]; - - }; - - }] - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUserGroups.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUserGroups.js deleted file mode 100644 index a8f2dd37cb..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUserGroups.js +++ /dev/null @@ -1,272 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for managing all user groups in the system. - */ -angular.module('settings').directive('guacSettingsUserGroups', ['$injector', - function guacSettingsUserGroups($injector) { - - // Required types - var ManageableUserGroup = $injector.get('ManageableUserGroup'); - var PermissionSet = $injector.get('PermissionSet'); - var SortOrder = $injector.get('SortOrder'); - - // Required services - var $location = $injector.get('$location'); - var authenticationService = $injector.get('authenticationService'); - var dataSourceService = $injector.get('dataSourceService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var userGroupService = $injector.get('userGroupService'); - - var directive = { - restrict : 'E', - replace : true, - templateUrl : 'app/settings/templates/settingsUserGroups.html', - scope : {} - }; - - directive.controller = ['$scope', function settingsUserGroupsController($scope) { - - // Identifier of the current user - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * The identifiers of all data sources accessible by the current - * user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * Map of data source identifiers to all permissions associated - * with the current user within that data source, or null if the - * user's permissions have not yet been loaded. - * - * @type Object. - */ - var permissions = null; - - /** - * All visible user groups, along with their corresponding data - * sources. - * - * @type ManageableUserGroup[] - */ - $scope.manageableUserGroups = null; - - /** - * Array of all user group properties that are filterable. - * - * @type String[] - */ - $scope.filteredUserGroupProperties = [ - 'userGroup.identifier' - ]; - - /** - * SortOrder instance which stores the sort order of the listed - * user groups. - * - * @type SortOrder - */ - $scope.order = new SortOrder([ - 'userGroup.identifier' - ]); - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user group - * interface to be useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - return $scope.manageableUserGroups !== null; - }; - - /** - * Returns the identifier of the data source that should be used by - * default when creating a new user group. - * - * @return {String} - * The identifier of the data source that should be used by - * default when creating a new user group, or null if user group - * creation is not allowed. - */ - $scope.getDefaultDataSource = function getDefaultDataSource() { - - // Abort if permissions have not yet loaded - if (!permissions) - return null; - - // For each data source - var dataSources = _.keys(permissions).sort(); - for (var i = 0; i < dataSources.length; i++) { - - // Retrieve corresponding permission set - var dataSource = dataSources[i]; - var permissionSet = permissions[dataSource]; - - // Can create user groups if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER_GROUP)) - return dataSource; - - } - - // No data sources allow user group creation - return null; - - }; - - /** - * Returns whether the current user can create new user groups - * within at least one data source. - * - * @return {Boolean} - * true if the current user can create new user groups within at - * least one data source, false otherwise. - */ - $scope.canCreateUserGroups = function canCreateUserGroups() { - return $scope.getDefaultDataSource() !== null; - }; - - /** - * Returns whether the current user can create new user groups or - * make changes to existing user groups within at least one data - * source. The user group management interface as a whole is useless - * if this function returns false. - * - * @return {Boolean} - * true if the current user can create new user groups or make - * changes to existing user groups within at least one data - * source, false otherwise. - */ - var canManageUserGroups = function canManageUserGroups() { - - // Abort if permissions have not yet loaded - if (!permissions) - return false; - - // Creating user groups counts as management - if ($scope.canCreateUserGroups()) - return true; - - // For each data source - for (var dataSource in permissions) { - - // Retrieve corresponding permission set - var permissionSet = permissions[dataSource]; - - // Can manage user groups if granted explicit update or delete - if (PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) - return true; - - } - - // No data sources allow management of user groups - return false; - - }; - - /** - * Sets the displayed list of user groups. If any user groups are - * already shown within the interface, those user groups are replaced - * with the given user groups. - * - * @param {Object.} permissions - * A map of data source identifiers to all permissions associated - * with the current user within that data source. - * - * @param {Object.>} userGroups - * A map of all user groups which should be displayed, where each - * key is the data source identifier from which the user groups - * were retrieved and each value is a map of user group identifiers - * to their corresponding @link{UserGroup} objects. - */ - var setDisplayedUserGroups = function setDisplayedUserGroups(permissions, userGroups) { - - var addedUserGroups = {}; - $scope.manageableUserGroups = []; - - // For each user group in each data source - angular.forEach(dataSources, function addUserGroupList(dataSource) { - angular.forEach(userGroups[dataSource], function addUserGroup(userGroup) { - - // Do not add the same user group twice - if (addedUserGroups[userGroup.identifier]) - return; - - // Link to default creation data source if we cannot manage this user - if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.ADMINISTER) - && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, userGroup.identifier) - && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, userGroup.identifier)) - dataSource = $scope.getDefaultDataSource(); - - // Add user group to overall list - addedUserGroups[userGroup.identifier] = userGroup; - $scope.manageableUserGroups.push(new ManageableUserGroup ({ - 'dataSource' : dataSource, - 'userGroup' : userGroup - })); - - }); - }); - - }; - - // Retrieve current permissions - dataSourceService.apply( - permissionService.getEffectivePermissions, - dataSources, - currentUsername - ) - .then(function permissionsRetrieved(retrievedPermissions) { - - // Store retrieved permissions - permissions = retrievedPermissions; - - // Return to home if there's nothing to do here - if (!canManageUserGroups()) - $location.path('/'); - - // If user groups can be created, list all readable user groups - if ($scope.canCreateUserGroups()) - return dataSourceService.apply(userGroupService.getUserGroups, dataSources); - - // Otherwise, list only updateable/deletable users - return dataSourceService.apply(userGroupService.getUserGroups, dataSources, [ - PermissionSet.ObjectPermissionType.UPDATE, - PermissionSet.ObjectPermissionType.DELETE - ]); - - }) - .then(function userGroupsReceived(userGroups) { - setDisplayedUserGroups(permissions, userGroups); - }, requestService.WARN); - - }]; - - return directive; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUsers.js b/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUsers.js deleted file mode 100644 index 6b8c6e06a8..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/directives/guacSettingsUsers.js +++ /dev/null @@ -1,291 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive for managing all users in the system. - */ -angular.module('settings').directive('guacSettingsUsers', [function guacSettingsUsers() { - - return { - // Element only - restrict: 'E', - replace: true, - - scope: { - }, - - templateUrl: 'app/settings/templates/settingsUsers.html', - controller: ['$scope', '$injector', function settingsUsersController($scope, $injector) { - - // Required types - var ManageableUser = $injector.get('ManageableUser'); - var PermissionSet = $injector.get('PermissionSet'); - var SortOrder = $injector.get('SortOrder'); - - // Required services - var $location = $injector.get('$location'); - var $translate = $injector.get('$translate'); - var authenticationService = $injector.get('authenticationService'); - var dataSourceService = $injector.get('dataSourceService'); - var permissionService = $injector.get('permissionService'); - var requestService = $injector.get('requestService'); - var userService = $injector.get('userService'); - - // Identifier of the current user - var currentUsername = authenticationService.getCurrentUsername(); - - /** - * The identifiers of all data sources accessible by the current - * user. - * - * @type String[] - */ - var dataSources = authenticationService.getAvailableDataSources(); - - /** - * All visible users, along with their corresponding data sources. - * - * @type ManageableUser[] - */ - $scope.manageableUsers = null; - - /** - * The name of the new user to create, if any, when user creation - * is requested via newUser(). - * - * @type String - */ - $scope.newUsername = ""; - - /** - * Map of data source identifiers to all permissions associated - * with the current user within that data source, or null if the - * user's permissions have not yet been loaded. - * - * @type Object. - */ - $scope.permissions = null; - - /** - * Array of all user properties that are filterable. - * - * @type String[] - */ - $scope.filteredUserProperties = [ - 'user.attributes["guac-full-name"]', - 'user.attributes["guac-organization"]', - 'user.lastActive', - 'user.username' - ]; - - /** - * The date format for use for the last active date. - * - * @type String - */ - $scope.dateFormat = null; - - /** - * SortOrder instance which stores the sort order of the listed - * users. - * - * @type SortOrder - */ - $scope.order = new SortOrder([ - 'user.username', - '-user.lastActive', - 'user.attributes["guac-organization"]', - 'user.attributes["guac-full-name"]' - ]); - - // Get session date format - $translate('SETTINGS_USERS.FORMAT_DATE') - .then(function dateFormatReceived(retrievedDateFormat) { - - // Store received date format - $scope.dateFormat = retrievedDateFormat; - - }, angular.noop); - - /** - * Returns whether critical data has completed being loaded. - * - * @returns {Boolean} - * true if enough data has been loaded for the user interface - * to be useful, false otherwise. - */ - $scope.isLoaded = function isLoaded() { - - return $scope.dateFormat !== null - && $scope.manageableUsers !== null - && $scope.permissions !== null; - - }; - - /** - * Returns the identifier of the data source that should be used by - * default when creating a new user. - * - * @return {String} - * The identifier of the data source that should be used by - * default when creating a new user, or null if user creation - * is not allowed. - */ - $scope.getDefaultDataSource = function getDefaultDataSource() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return null; - - // For each data source - var dataSources = _.keys($scope.permissions).sort(); - for (var i = 0; i < dataSources.length; i++) { - - // Retrieve corresponding permission set - var dataSource = dataSources[i]; - var permissionSet = $scope.permissions[dataSource]; - - // Can create users if adminstrator or have explicit permission - if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) - || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER)) - return dataSource; - - } - - // No data sources allow user creation - return null; - - }; - - /** - * Returns whether the current user can create new users within at - * least one data source. - * - * @return {Boolean} - * true if the current user can create new users within at - * least one data source, false otherwise. - */ - $scope.canCreateUsers = function canCreateUsers() { - return $scope.getDefaultDataSource() !== null; - }; - - /** - * Returns whether the current user can create new users or make - * changes to existing users within at least one data source. The - * user management interface as a whole is useless if this function - * returns false. - * - * @return {Boolean} - * true if the current user can create new users or make - * changes to existing users within at least one data source, - * false otherwise. - */ - var canManageUsers = function canManageUsers() { - - // Abort if permissions have not yet loaded - if (!$scope.permissions) - return false; - - // Creating users counts as management - if ($scope.canCreateUsers()) - return true; - - // For each data source - for (var dataSource in $scope.permissions) { - - // Retrieve corresponding permission set - var permissionSet = $scope.permissions[dataSource]; - - // Can manage users if granted explicit update or delete - if (PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) - || PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) - return true; - - } - - // No data sources allow management of users - return false; - - }; - - // Retrieve current permissions - dataSourceService.apply( - permissionService.getEffectivePermissions, - dataSources, - currentUsername - ) - .then(function permissionsRetrieved(permissions) { - - // Store retrieved permissions - $scope.permissions = permissions; - - // Return to home if there's nothing to do here - if (!canManageUsers()) - $location.path('/'); - - var userPromise; - - // If users can be created, list all readable users - if ($scope.canCreateUsers()) - userPromise = dataSourceService.apply(userService.getUsers, dataSources); - - // Otherwise, list only updateable/deletable users - else - userPromise = dataSourceService.apply(userService.getUsers, dataSources, [ - PermissionSet.ObjectPermissionType.UPDATE, - PermissionSet.ObjectPermissionType.DELETE - ]); - - userPromise.then(function usersReceived(allUsers) { - - var addedUsers = {}; - $scope.manageableUsers = []; - - // For each user in each data source - angular.forEach(dataSources, function addUserList(dataSource) { - angular.forEach(allUsers[dataSource], function addUser(user) { - - // Do not add the same user twice - if (addedUsers[user.username]) - return; - - // Link to default creation data source if we cannot manage this user - if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.ADMINISTER) - && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, user.username) - && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, user.username)) - dataSource = $scope.getDefaultDataSource(); - - // Add user to overall list - addedUsers[user.username] = user; - $scope.manageableUsers.push(new ManageableUser ({ - 'dataSource' : dataSource, - 'user' : user - })); - - }); - }); - - }, requestService.DIE); - - }, requestService.DIE); - - }] - }; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/services/preferenceService.js b/guacamole/src/main/frontend/src/app/settings/services/preferenceService.js deleted file mode 100644 index f029790f84..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/services/preferenceService.js +++ /dev/null @@ -1,223 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for setting and retrieving browser-local preferences. Preferences - * may be any JSON-serializable type. - */ -angular.module('settings').provider('preferenceService', ['$injector', - function preferenceServiceProvider($injector) { - - // Required providers - var localStorageServiceProvider = $injector.get('localStorageServiceProvider'); - - /** - * Reference to the provider itself. - * - * @type preferenceServiceProvider - */ - var provider = this; - - /** - * The storage key of Guacamole preferences within local storage. - * - * @type String - */ - var GUAC_PREFERENCES_STORAGE_KEY = "GUAC_PREFERENCES"; - - /** - * All valid input method type names. - * - * @type Object. - */ - var inputMethods = { - - /** - * No input method is used. Keyboard events are generated from a - * physical keyboard. - * - * @constant - * @type String - */ - NONE : 'none', - - /** - * Keyboard events will be generated from the Guacamole on-screen - * keyboard. - * - * @constant - * @type String - */ - OSK : 'osk', - - /** - * Keyboard events will be generated by inferring the keys necessary to - * produce typed text from an IME (Input Method Editor) such as the - * native on-screen keyboard of a mobile device. - * - * @constant - * @type String - */ - TEXT : 'text' - - }; - - /** - * Returns the key of the language currently in use within the browser. - * This is not necessarily the user's desired language, but is rather the - * language user by the browser's interface. - * - * @returns {String} - * The key of the language currently in use within the browser. - */ - var getDefaultLanguageKey = function getDefaultLanguageKey() { - - // Pull browser language, falling back to US English - var language = (navigator.languages && navigator.languages[0]) - || navigator.language - || navigator.browserLanguage - || 'en'; - - // Convert to format used internally - return language.replace(/-/g, '_'); - - }; - - /** - * Return the timezone detected for the current browser session - * by the JSTZ timezone library. - * - * @returns String - * The name of the currently-detected timezone in IANA zone key - * format (Olson time zone database). - */ - var getDetectedTimezone = function getDetectedTimezone() { - return jstz.determine().name(); - }; - - /** - * All currently-set preferences, as name/value pairs. Each property name - * corresponds to the name of a preference. - * - * @type Object. - */ - this.preferences = { - - /** - * Whether translation of touch to mouse events should emulate an - * absolute pointer device, or a relative pointer device. - * - * @type Boolean - */ - emulateAbsoluteMouse : true, - - /** - * The default input method. This may be any of the values defined - * within preferenceService.inputMethods. - * - * @type String - */ - inputMethod : inputMethods.NONE, - - /** - * The key of the desired display language. - * - * @type String - */ - language : getDefaultLanguageKey(), - - /** - * The number of recent connections to display. - * - * @type {!number} - */ - numberOfRecentConnections: 6, - - /** - * Whether or not to show the "Recent Connections" section. - * - * @type {!boolean} - */ - showRecentConnections : true, - - /** - * The timezone set by the user, in IANA zone key format (Olson time - * zone database). - * - * @type String - */ - timezone : getDetectedTimezone() - - }; - - // Get stored preferences from localStorage - var storedPreferences = localStorageServiceProvider.getItem(GUAC_PREFERENCES_STORAGE_KEY); - if (storedPreferences) - angular.extend(provider.preferences, storedPreferences); - - // Factory method required by provider - this.$get = ['$injector', function preferenceServiceFactory($injector) { - - // Required services - var $rootScope = $injector.get('$rootScope'); - var $window = $injector.get('$window'); - var localStorageService = $injector.get('localStorageService'); - - var service = {}; - - /** - * All valid input method type names. - * - * @type Object. - */ - service.inputMethods = inputMethods; - - /** - * All currently-set preferences, as name/value pairs. Each property name - * corresponds to the name of a preference. - * - * @type Object. - */ - service.preferences = provider.preferences; - - /** - * Persists the current values of all preferences, if possible. - */ - service.save = function save() { - localStorageService.setItem(GUAC_PREFERENCES_STORAGE_KEY, service.preferences); - }; - - // Persist settings when window is unloaded - $window.addEventListener('unload', service.save); - - // Persist settings upon navigation - $rootScope.$on('$routeChangeSuccess', function handleNavigate() { - service.save(); - }); - - // Persist settings upon logout - $rootScope.$on('guacLogout', function handleLogout() { - service.save(); - }); - - return service; - - }]; - -}]); diff --git a/guacamole/src/main/frontend/src/app/settings/settingsModule.js b/guacamole/src/main/frontend/src/app/settings/settingsModule.js deleted file mode 100644 index 4eeb517af1..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/settingsModule.js +++ /dev/null @@ -1,32 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * The module for manipulation of general settings. This is distinct from the - * "manage" module, which deals only with administrator-level system management. - */ -angular.module('settings', [ - 'groupList', - 'list', - 'navigation', - 'notification', - 'player', - 'rest', - 'storage' -]); diff --git a/guacamole/src/main/frontend/src/app/settings/templates/connection.html b/guacamole/src/main/frontend/src/app/settings/templates/connection.html deleted file mode 100644 index ecf5e8eeec..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/connection.html +++ /dev/null @@ -1,16 +0,0 @@ - - - -
    - - - {{item.name}} - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/connectionGroup.html b/guacamole/src/main/frontend/src/app/settings/templates/connectionGroup.html deleted file mode 100644 index 79e37925ba..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/connectionGroup.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -
    - - - {{item.name}} - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/newConnection.html b/guacamole/src/main/frontend/src/app/settings/templates/newConnection.html deleted file mode 100644 index d5d8201382..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/newConnection.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | translate}} - diff --git a/guacamole/src/main/frontend/src/app/settings/templates/newConnectionGroup.html b/guacamole/src/main/frontend/src/app/settings/templates/newConnectionGroup.html deleted file mode 100644 index bd9735c3d2..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/newConnectionGroup.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION_GROUP' | translate}} - diff --git a/guacamole/src/main/frontend/src/app/settings/templates/newSharingProfile.html b/guacamole/src/main/frontend/src/app/settings/templates/newSharingProfile.html deleted file mode 100644 index ef0f8ca528..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/newSharingProfile.html +++ /dev/null @@ -1,4 +0,0 @@ - - {{'SETTINGS_CONNECTIONS.ACTION_NEW_SHARING_PROFILE' | translate}} - diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settings.html b/guacamole/src/main/frontend/src/app/settings/templates/settings.html deleted file mode 100644 index c0986ff453..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settings.html +++ /dev/null @@ -1,21 +0,0 @@ -
    - -
    -

    {{'SETTINGS.SECTION_HEADER_SETTINGS' | translate}}

    - -
    - - -
    - -
    - - - - - - - - - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html deleted file mode 100644 index 00c5c2e30d..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistory.html +++ /dev/null @@ -1,70 +0,0 @@ -
    - - -

    {{'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | translate}}

    - - - - - - - - - -
    - - - - - - - - - - - - - - - - - - - - - - - -
    - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | translate}} - - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | translate}} - - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | translate}} - - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} - - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} - - {{'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_LOGS' | translate}} -
    {{historyEntryWrapper.entry.startDate | date : dateFormat}}{{historyEntryWrapper.entry.connectionName}}{{historyEntryWrapper.entry.remoteHost}} - - {{'SETTINGS_CONNECTION_HISTORY.ACTION_VIEW_RECORDING' | translate}} - -
    - - -

    - {{'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | translate}} -

    - - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html deleted file mode 100644 index d986864d1c..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnectionHistoryPlayer.html +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html deleted file mode 100644 index 20f0f82689..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsConnections.html +++ /dev/null @@ -1,53 +0,0 @@ - diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html deleted file mode 100644 index bf9684295a..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsPreferences.html +++ /dev/null @@ -1,119 +0,0 @@ -
    - - -
    -

    {{'SETTINGS_PREFERENCES.HELP_LOCALE' | translate}}

    - -
    - - -

    {{'SETTINGS_PREFERENCES.SECTION_HEADER_APPEARANCE' | translate}}

    -
    -

    {{'SETTINGS_PREFERENCES.HELP_APPEARANCE' | translate}}

    -
    - - - - - - - - - -
    {{'SETTINGS_PREFERENCES.FIELD_HEADER_SHOW_RECENT_CONNECTIONS' | translate}}
    {{'SETTINGS_PREFERENCES.FIELD_HEADER_NUMBER_RECENT_CONNECTIONS' | translate}}
    -
    -
    - - -

    {{'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | translate}}

    -
    -

    {{'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | translate}}

    - - -
    - - - - - - - - - - - - - -
    {{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_OLD' | translate}}
    {{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW' | translate}}
    {{'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW_AGAIN' | translate}}
    -
    - - -
    - -
    -
    - - -

    {{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_INPUT_METHOD' | translate}}

    -
    -

    {{'SETTINGS_PREFERENCES.HELP_DEFAULT_INPUT_METHOD' | translate}}

    -
    - - -
    - -

    -
    - - -
    - -

    -
    - - -
    - -

    -
    - -
    -
    - - -

    {{'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_MOUSE_MODE' | translate}}

    -
    -

    {{'SETTINGS_PREFERENCES.HELP_DEFAULT_MOUSE_MODE' | translate}}

    -
    - - -
    - -
    - -

    -
    -
    - - -
    - -
    - -

    -
    -
    - -
    -
    - - -
    - - - - -
    - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsSessions.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsSessions.html deleted file mode 100644 index a2cd9b3d8e..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsSessions.html +++ /dev/null @@ -1,58 +0,0 @@ -
    - - -

    {{'SETTINGS_SESSIONS.HELP_SESSIONS' | translate}}

    - - -
    - -
    - - - - - - - - - - - - - - - - - - - - - - - - -
    - {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_USERNAME' | translate}} - - {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_STARTDATE' | translate}} - - {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_REMOTEHOST' | translate}} - - {{'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_CONNECTION_NAME' | translate}} -
    - - {{wrapper.startDate}}{{wrapper.activeConnection.remoteHost}}{{wrapper.name}}
    - - -

    - {{'SETTINGS_SESSIONS.INFO_NO_SESSIONS' | translate}} -

    - - - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsUserGroups.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsUserGroups.html deleted file mode 100644 index 4e7cd4c5a9..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsUserGroups.html +++ /dev/null @@ -1,48 +0,0 @@ -
    - - -

    {{'SETTINGS_USER_GROUPS.HELP_USER_GROUPS' | translate}}

    - - - - - - - - - - - - - - - - - -
    - {{'SETTINGS_USER_GROUPS.TABLE_HEADER_USER_GROUP_NAME' | translate}} -
    - -
    - {{manageableUserGroup.userGroup.identifier}} -
    -
    - - - - -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/templates/settingsUsers.html b/guacamole/src/main/frontend/src/app/settings/templates/settingsUsers.html deleted file mode 100644 index 4a178fe491..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/settingsUsers.html +++ /dev/null @@ -1,60 +0,0 @@ -
    - - -

    {{'SETTINGS_USERS.HELP_USERS' | translate}}

    - - - - - - - - - - - - - - - - - - - - - - - -
    - {{'SETTINGS_USERS.TABLE_HEADER_USERNAME' | translate}} - - {{'SETTINGS_USERS.TABLE_HEADER_ORGANIZATION' | translate}} - - {{'SETTINGS_USERS.TABLE_HEADER_FULL_NAME' | translate}} - - {{'SETTINGS_USERS.TABLE_HEADER_LAST_ACTIVE' | translate}} -
    - -
    - {{manageableUser.user.username}} -
    -
    {{manageableUser.user.attributes['guac-organization']}}{{manageableUser.user.attributes['guac-full-name']}}{{manageableUser.user.lastActive | date : dateFormat}}
    - - - - -
    \ No newline at end of file diff --git a/guacamole/src/main/frontend/src/app/settings/templates/sharingProfile.html b/guacamole/src/main/frontend/src/app/settings/templates/sharingProfile.html deleted file mode 100644 index 6c8ae4de40..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/templates/sharingProfile.html +++ /dev/null @@ -1,10 +0,0 @@ - - - -
    - - - {{item.name}} - -
    diff --git a/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js b/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js deleted file mode 100644 index e7db412d81..0000000000 --- a/guacamole/src/main/frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.js +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A service for defining the ConnectionHistoryEntryWrapper class. - */ -angular.module('settings').factory('ConnectionHistoryEntryWrapper', ['$injector', - function defineConnectionHistoryEntryWrapper($injector) { - - // Required types - const ActivityLog = $injector.get('ActivityLog'); - const ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); - - // Required services - const $translate = $injector.get('$translate'); - - /** - * Wrapper for ConnectionHistoryEntry which adds display-specific - * properties, such as a duration. - * - * @constructor - * @param {ConnectionHistoryEntry} historyEntry - * The ConnectionHistoryEntry that should be wrapped. - */ - const ConnectionHistoryEntryWrapper = function ConnectionHistoryEntryWrapper(dataSource, historyEntry) { - - /** - * The wrapped ConnectionHistoryEntry. - * - * @type ConnectionHistoryEntry - */ - this.entry = historyEntry; - - /** - * The total amount of time the connection associated with the wrapped - * history record was open, in seconds. - * - * @type Number - */ - this.duration = historyEntry.endDate - historyEntry.startDate; - - /** - * An object providing value and unit properties, denoting the duration - * and its corresponding units. - * - * @type ConnectionHistoryEntry.Duration - */ - this.readableDuration = null; - - // Set the duration if the necessary information is present - if (historyEntry.endDate && historyEntry.startDate) - this.readableDuration = new ConnectionHistoryEntry.Duration(this.duration); - - /** - * The string to display as the duration of this history entry. If a - * duration is available, its value and unit will be exposed to any - * given translation string as the VALUE and UNIT substitution - * variables respectively. - * - * @type String - */ - this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.TEXT_HISTORY_DURATION'; - - // Inform user if end date is not known - if (!historyEntry.endDate) - this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.INFO_CONNECTION_DURATION_UNKNOWN'; - - /** - * The graphical session recording associated with this history entry, - * if any. If no session recordings are associated with the entry, this - * will be null. If there are multiple session recordings, this will be - * the first such recording. - * - * @type {ConnectionHistoryEntryWrapper.Log} - */ - this.sessionRecording = (function getSessionRecording() { - - var identifier = historyEntry.identifier; - if (!identifier) - return null; - - var name = _.findKey(historyEntry.logs, log => log.type === ActivityLog.Type.GUACAMOLE_SESSION_RECORDING); - if (!name) - return null; - - var log = historyEntry.logs[name]; - return new ConnectionHistoryEntryWrapper.Log({ - - url : '#/settings/' + encodeURIComponent(dataSource) - + '/recording/' + encodeURIComponent(identifier) - + '/' + encodeURIComponent(name), - - description : $translate(log.description.key, log.description.variables) - - }); - - })(); - - }; - - /** - * Representation of the ActivityLog of a ConnectionHistoryEntry which adds - * display-specific properties, such as a URL for viewing the log. - * - * @param {ConnectionHistoryEntryWrapper.Log|Object} [template={}] - * The object whose properties should be copied within the new - * ConnectionHistoryEntryWrapper.Log. - */ - ConnectionHistoryEntryWrapper.Log = function Log(template) { - - // Use empty object by default - template = template || {}; - - /** - * The relative URL for a session recording player that loads the - * session recording represented by this log. - * - * @type {!string} - */ - this.url = template.url; - - /** - * A promise that resolves with a human-readable description of the log. - * - * @type {!Promise.} - */ - this.description = template.description; - - }; - - return ConnectionHistoryEntryWrapper; - -}]); diff --git a/guacamole/src/main/frontend/src/app/textInput/directives/guacKey.js b/guacamole/src/main/frontend/src/app/textInput/directives/guacKey.js deleted file mode 100644 index 45fa4b38f1..0000000000 --- a/guacamole/src/main/frontend/src/app/textInput/directives/guacKey.js +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays a button that controls the pressed state of a - * single keyboard key. - */ -angular.module('textInput').directive('guacKey', [function guacKey() { - - return { - restrict: 'E', - replace: true, - scope: { - - /** - * The text to display within the key. This will be run through the - * translation filter prior to display. - * - * @type String - */ - text : '=', - - /** - * The keysym to send within keyup and keydown events when this key - * is pressed or released. - * - * @type Number - */ - keysym : '=', - - /** - * Whether this key is sticky. Sticky keys toggle their pressed - * state with each click. - * - * @type Boolean - * @default false - */ - sticky : '=?', - - /** - * Whether this key is currently pressed. - * - * @type Boolean - * @default false - */ - pressed : '=?' - - }, - - templateUrl: 'app/textInput/templates/guacKey.html', - controller: ['$scope', '$rootScope', - function guacKey($scope, $rootScope) { - - // Not sticky by default - $scope.sticky = $scope.sticky || false; - - // Unpressed by default - $scope.pressed = $scope.pressed || false; - - /** - * Presses and releases this key, sending the corresponding keydown - * and keyup events. In the case of sticky keys, the pressed state - * is toggled, and only a single keydown/keyup event will be sent, - * depending on the current state. - * - * @param {MouseEvent} event - * The mouse event which resulted in this function being - * invoked. - */ - $scope.updateKey = function updateKey(event) { - - // If sticky, toggle pressed state - if ($scope.sticky) - $scope.pressed = !$scope.pressed; - - // For all non-sticky keys, press and release key immediately - else { - $rootScope.$broadcast('guacSyntheticKeydown', $scope.keysym); - $rootScope.$broadcast('guacSyntheticKeyup', $scope.keysym); - } - - // Prevent loss of focus due to interaction with buttons - event.preventDefault(); - - }; - - // Send keyup/keydown when pressed state is altered - $scope.$watch('pressed', function updatePressedState(isPressed, wasPressed) { - - // If the key is pressed now, send keydown - if (isPressed) - $rootScope.$broadcast('guacSyntheticKeydown', $scope.keysym); - - // If the key was pressed, but is not pressed any longer, send keyup - else if (wasPressed) - $rootScope.$broadcast('guacSyntheticKeyup', $scope.keysym); - - }); - - }] - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/textInput/directives/guacTextInput.js b/guacamole/src/main/frontend/src/app/textInput/directives/guacTextInput.js deleted file mode 100644 index c1a467727f..0000000000 --- a/guacamole/src/main/frontend/src/app/textInput/directives/guacTextInput.js +++ /dev/null @@ -1,341 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which displays the Guacamole text input method. - */ -angular.module('textInput').directive('guacTextInput', [function guacTextInput() { - - return { - restrict: 'E', - replace: true, - scope: {}, - - templateUrl: 'app/textInput/templates/guacTextInput.html', - controller: ['$scope', '$rootScope', '$element', '$timeout', - function guacTextInput($scope, $rootScope, $element, $timeout) { - - /** - * The number of characters to include on either side of text input - * content, to allow the user room to use backspace and delete. - * - * @type Number - */ - var TEXT_INPUT_PADDING = 4; - - /** - * The Unicode codepoint of the character to use for padding on - * either side of text input content. - * - * @type Number - */ - var TEXT_INPUT_PADDING_CODEPOINT = 0x200B; - - /** - * Keys which should be allowed through to the client when in text - * input mode, providing corresponding key events are received. - * Keys in this set will be allowed through to the server. - * - * @type Object. - */ - var ALLOWED_KEYS = { - 0xFE03: true, /* AltGr */ - 0xFF08: true, /* Backspace */ - 0xFF09: true, /* Tab */ - 0xFF0D: true, /* Enter */ - 0xFF1B: true, /* Escape */ - 0xFF50: true, /* Home */ - 0xFF51: true, /* Left */ - 0xFF52: true, /* Up */ - 0xFF53: true, /* Right */ - 0xFF54: true, /* Down */ - 0xFF57: true, /* End */ - 0xFF64: true, /* Insert */ - 0xFFBE: true, /* F1 */ - 0xFFBF: true, /* F2 */ - 0xFFC0: true, /* F3 */ - 0xFFC1: true, /* F4 */ - 0xFFC2: true, /* F5 */ - 0xFFC3: true, /* F6 */ - 0xFFC4: true, /* F7 */ - 0xFFC5: true, /* F8 */ - 0xFFC6: true, /* F9 */ - 0xFFC7: true, /* F10 */ - 0xFFC8: true, /* F11 */ - 0xFFC9: true, /* F12 */ - 0xFFE1: true, /* Left shift */ - 0xFFE2: true, /* Right shift */ - 0xFFE3: true, /* Left ctrl */ - 0xFFE4: true, /* Right ctrl */ - 0xFFE9: true, /* Left alt */ - 0xFFEA: true, /* Right alt */ - 0xFFFF: true /* Delete */ - }; - - /** - * Recently-sent text, ordered from oldest to most recent. - * - * @type String[] - */ - $scope.sentText = []; - - /** - * Whether the "Alt" key is currently pressed within the text input - * interface. - * - * @type Boolean - */ - $scope.altPressed = false; - - /** - * Whether the "Ctrl" key is currently pressed within the text - * input interface. - * - * @type Boolean - */ - $scope.ctrlPressed = false; - - /** - * The text area input target. - * - * @type Element - */ - var target = $element.find('.target')[0]; - - /** - * Whether the text input target currently has focus. Setting this - * attribute has no effect, but any bound property will be updated - * as focus is gained or lost. - * - * @type Boolean - */ - var hasFocus = false; - - target.onfocus = function targetFocusGained() { - hasFocus = true; - resetTextInputTarget(TEXT_INPUT_PADDING); - }; - - target.onblur = function targetFocusLost() { - hasFocus = false; - }; - - /** - * Whether composition is currently active within the text input - * target element, such as when an IME is in use. - * - * @type Boolean - */ - var composingText = false; - - target.addEventListener("compositionstart", function targetComposeStart(e) { - composingText = true; - }, false); - - target.addEventListener("compositionend", function targetComposeEnd(e) { - composingText = false; - }, false); - - /** - * Translates a given Unicode codepoint into the corresponding X11 - * keysym. - * - * @param {Number} codepoint - * The Unicode codepoint to translate. - * - * @returns {Number} - * The X11 keysym that corresponds to the given Unicode - * codepoint, or null if no such keysym exists. - */ - var keysymFromCodepoint = function keysymFromCodepoint(codepoint) { - - // Keysyms for control characters - if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F)) - return 0xFF00 | codepoint; - - // Keysyms for ASCII chars - if (codepoint >= 0x0000 && codepoint <= 0x00FF) - return codepoint; - - // Keysyms for Unicode - if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) - return 0x01000000 | codepoint; - - return null; - - }; - - /** - * Presses and releases the key corresponding to the given keysym, - * as if typed by the user. - * - * @param {Number} keysym The keysym of the key to send. - */ - var sendKeysym = function sendKeysym(keysym) { - $rootScope.$broadcast('guacSyntheticKeydown', keysym); - $rootScope.$broadcast('guacSyntheticKeyup', keysym); - }; - - /** - * Presses and releases the key having the keysym corresponding to - * the Unicode codepoint given, as if typed by the user. - * - * @param {Number} codepoint - * The Unicode codepoint of the key to send. - */ - var sendCodepoint = function sendCodepoint(codepoint) { - - if (codepoint === 10) { - sendKeysym(0xFF0D); - releaseStickyKeys(); - return; - } - - var keysym = keysymFromCodepoint(codepoint); - if (keysym) { - sendKeysym(keysym); - releaseStickyKeys(); - } - - }; - - /** - * Translates each character within the given string to keysyms and - * sends each, in order, as if typed by the user. - * - * @param {String} content - * The string to send. - */ - var sendString = function sendString(content) { - - var sentText = ""; - - // Send each codepoint within the string - for (var i=0; i - {{text | translate}} - diff --git a/guacamole/src/main/frontend/src/app/textInput/templates/guacTextInput.html b/guacamole/src/main/frontend/src/app/textInput/templates/guacTextInput.html deleted file mode 100644 index 7b8b728815..0000000000 --- a/guacamole/src/main/frontend/src/app/textInput/templates/guacTextInput.html +++ /dev/null @@ -1,6 +0,0 @@ -
    - - -
    {{text}}
    - -
    diff --git a/guacamole/src/main/frontend/src/app/touch/directives/guacTouchDrag.js b/guacamole/src/main/frontend/src/app/touch/directives/guacTouchDrag.js deleted file mode 100644 index 79ffd93e20..0000000000 --- a/guacamole/src/main/frontend/src/app/touch/directives/guacTouchDrag.js +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows handling of drag gestures on a particular element. - */ -angular.module('touch').directive('guacTouchDrag', [function guacTouchDrag() { - - return { - restrict: 'A', - - link: function linkGuacTouchDrag($scope, $element, $attrs) { - - /** - * Called during a drag gesture as the user's finger is placed upon - * the element, moves, and is lifted from the element. - * - * @event - * @param {Boolean} inProgress - * Whether the gesture is currently in progress. This will - * always be true except when the gesture has ended, at which - * point one final call will occur with this parameter set to - * false. - * - * @param {Number} startX - * The X location at which the drag gesture began. - * - * @param {Number} startY - * The Y location at which the drag gesture began. - * - * @param {Number} currentX - * The current X location of the user's finger. - * - * @param {Number} currentY - * The current Y location of the user's finger. - * - * @param {Number} deltaX - * The difference in X location relative to the start of the - * gesture. - * - * @param {Number} deltaY - * The difference in Y location relative to the start of the - * gesture. - * - * @return {Boolean} - * false if the default action of the touch event should be - * prevented, any other value otherwise. - */ - var guacTouchDrag = $scope.$eval($attrs.guacTouchDrag); - - /** - * The element which will register the drag gesture. - * - * @type Element - */ - var element = $element[0]; - - /** - * Whether a drag gesture is in progress. - * - * @type Boolean - */ - var inProgress = false; - - /** - * The starting X location of the drag gesture. - * - * @type Number - */ - var startX = null; - - /** - * The starting Y location of the drag gesture. - * - * @type Number - */ - var startY = null; - - /** - * The current X location of the drag gesture. - * - * @type Number - */ - var currentX = null; - - /** - * The current Y location of the drag gesture. - * - * @type Number - */ - var currentY = null; - - /** - * The change in X relative to drag start. - * - * @type Number - */ - var deltaX = 0; - - /** - * The change in X relative to drag start. - * - * @type Number - */ - var deltaY = 0; - - // When there is exactly one touch, monitor the change in location - element.addEventListener("touchmove", function dragTouchMove(e) { - if (e.touches.length === 1) { - - // Get touch location - var x = e.touches[0].clientX; - var y = e.touches[0].clientY; - - // Init start location and deltas if gesture is starting - if (!startX || !startY) { - startX = currentX = x; - startY = currentY = y; - deltaX = 0; - deltaY = 0; - inProgress = true; - } - - // Update deltas if gesture is in progress - else if (inProgress) { - deltaX = x - currentX; - deltaY = y - currentY; - currentX = x; - currentY = y; - } - - // Signal start/change in drag gesture - if (inProgress && guacTouchDrag) { - $scope.$apply(function dragChanged() { - if (guacTouchDrag(true, startX, startY, currentX, currentY, deltaX, deltaY) === false) - e.preventDefault(); - }); - } - - } - }, false); - - // Reset monitoring and fire end event when done - element.addEventListener("touchend", function dragTouchEnd(e) { - - if (startX && startY && e.touches.length === 0) { - - // Signal end of drag gesture - if (inProgress && guacTouchDrag) { - $scope.$apply(function dragComplete() { - if (guacTouchDrag(true, startX, startY, currentX, currentY, deltaX, deltaY) === false) - e.preventDefault(); - }); - } - - startX = currentX = null; - startY = currentY = null; - deltaX = 0; - deltaY = 0; - inProgress = false; - - } - - }, false); - - } - - }; -}]); diff --git a/guacamole/src/main/frontend/src/app/touch/directives/guacTouchPinch.js b/guacamole/src/main/frontend/src/app/touch/directives/guacTouchPinch.js deleted file mode 100644 index f754001047..0000000000 --- a/guacamole/src/main/frontend/src/app/touch/directives/guacTouchPinch.js +++ /dev/null @@ -1,206 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * A directive which allows handling of pinch gestures (pinch-to-zoom, for - * example) on a particular element. - */ -angular.module('touch').directive('guacTouchPinch', [function guacTouchPinch() { - - return { - restrict: 'A', - - link: function linkGuacTouchPinch($scope, $element, $attrs) { - - /** - * Called when a pinch gesture begins, changes, or ends. - * - * @event - * @param {Boolean} inProgress - * Whether the gesture is currently in progress. This will - * always be true except when the gesture has ended, at which - * point one final call will occur with this parameter set to - * false. - * - * @param {Number} startLength - * The initial distance between the two touches of the - * pinch gesture, in pixels. - * - * @param {Number} currentLength - * The current distance between the two touches of the - * pinch gesture, in pixels. - * - * @param {Number} centerX - * The current X coordinate of the center of the pinch gesture. - * - * @param {Number} centerY - * The current Y coordinate of the center of the pinch gesture. - * - * @return {Boolean} - * false if the default action of the touch event should be - * prevented, any other value otherwise. - */ - var guacTouchPinch = $scope.$eval($attrs.guacTouchPinch); - - /** - * The element which will register the pinch gesture. - * - * @type Element - */ - var element = $element[0]; - - /** - * The starting pinch distance, or null if the gesture has not yet - * started. - * - * @type Number - */ - var startLength = null; - - /** - * The current pinch distance, or null if the gesture has not yet - * started. - * - * @type Number - */ - var currentLength = null; - - /** - * The X coordinate of the current center of the pinch gesture. - * - * @type Number - */ - var centerX = 0; - - /** - * The Y coordinate of the current center of the pinch gesture. - * @type Number - */ - var centerY = 0; - - /** - * Given a touch event, calculates the distance between the first - * two touches in pixels. - * - * @param {TouchEvent} e - * The touch event to use when performing distance calculation. - * - * @return {Number} - * The distance in pixels between the first two touches. - */ - var pinchDistance = function pinchDistance(e) { - - var touchA = e.touches[0]; - var touchB = e.touches[1]; - - var deltaX = touchA.clientX - touchB.clientX; - var deltaY = touchA.clientY - touchB.clientY; - - return Math.sqrt(deltaX*deltaX + deltaY*deltaY); - - }; - - /** - * Given a touch event, calculates the center between the first two - * touches in pixels, returning the X coordinate of this center. - * - * @param {TouchEvent} e - * The touch event to use when performing center calculation. - * - * @return {Number} - * The X coordinate of the center of the first two touches. - */ - var pinchCenterX = function pinchCenterX(e) { - - var touchA = e.touches[0]; - var touchB = e.touches[1]; - - return (touchA.clientX + touchB.clientX) / 2; - - }; - - /** - * Given a touch event, calculates the center between the first two - * touches in pixels, returning the Y coordinate of this center. - * - * @param {TouchEvent} e - * The touch event to use when performing center calculation. - * - * @return {Number} - * The Y coordinate of the center of the first two touches. - */ - var pinchCenterY = function pinchCenterY(e) { - - var touchA = e.touches[0]; - var touchB = e.touches[1]; - - return (touchA.clientY + touchB.clientY) / 2; - - }; - - // When there are exactly two touches, monitor the distance between - // them, firing zoom events as appropriate - element.addEventListener("touchmove", function pinchTouchMove(e) { - if (e.touches.length === 2) { - - // Calculate current zoom level - currentLength = pinchDistance(e); - - // Calculate center - centerX = pinchCenterX(e); - centerY = pinchCenterY(e); - - // Init start length if pinch is not in progress - if (!startLength) - startLength = currentLength; - - // Notify of pinch status - if (guacTouchPinch) { - $scope.$apply(function pinchChanged() { - if (guacTouchPinch(true, startLength, currentLength, centerX, centerY) === false) - e.preventDefault(); - }); - } - - } - }, false); - - // Reset monitoring and fire end event when done - element.addEventListener("touchend", function pinchTouchEnd(e) { - - if (startLength && e.touches.length < 2) { - - // Notify of pinch end - if (guacTouchPinch) { - $scope.$apply(function pinchComplete() { - if (guacTouchPinch(false, startLength, currentLength, centerX, centerY) === false) - e.preventDefault(); - }); - } - - startLength = null; - - } - - }, false); - - } - - }; -}]); diff --git a/guacamole/src/main/frontend/src/index.html b/guacamole/src/main/frontend/src/index.html deleted file mode 100644 index 7a26c121f6..0000000000 --- a/guacamole/src/main/frontend/src/index.html +++ /dev/null @@ -1,127 +0,0 @@ - - - - - - - - - - - - - - - - - <% for (var index in htmlWebpackPlugin.files.css) { %> - - <% } %> - - - - - - - - - -
    - -
    -

    -

    - -

    -
    -
    -
    - - -
    - -
    -

    -

    -
    -
    -
    - - - - - - - - -
    - - - - - - -
    -
    - - -
    - -
    - -
    - - - - - - - - - - - - - <% for (var index in htmlWebpackPlugin.files.js) { %> - - <% } %> - - - - - - - diff --git a/guacamole/src/main/frontend/src/relocateParameters.js b/guacamole/src/main/frontend/src/relocateParameters.js deleted file mode 100644 index f3d0e00a2b..0000000000 --- a/guacamole/src/main/frontend/src/relocateParameters.js +++ /dev/null @@ -1,128 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Reformats the URL of the current page such that normal query parameters will - * be received by AngularJS. If possible, this reformatting operation will be - * performed using the HTML5 History API, thus avoiding reloading the page. - * - * For example, if a user visits the following URL: - * - * http://example.org/some/application/?foo=bar - * - * this script will reformat the URL as: - * - * http://example.org/some/application/#/?foo=bar - * - * If the URL does not contain query parameters, or the query parameters are - * already in a format which AngularJS can read, then the URL is left - * untouched. - * - * If query parameters are present both in the normal non-Angular format AND - * within the URL fragment identifier, the query parameters are merged such - * that AngularJS can read all parameters. - * - * @private - * @param {Location} location - * The Location object representing the URL of the current page. - */ -(function relocateParameters(location){ - - /** - * The default path, including leading '#' character, which should be used - * if the URL of the current page has no fragment identifier. - * - * @constant - * @type String - */ - var DEFAULT_ANGULAR_PATH = '#/'; - - /** - * The query parameters within the URL of the current page, including the - * leading '?' character. - * - * @type String - */ - var parameters = location.search; - - /** - * The base URL of the current page, containing only the protocol, hostname, - * and path. Query parameters and the fragment, if any, are excluded. - * - * @type String - */ - var baseUrl = location.origin + location.pathname; - - /** - * The Angular-specific path within the fragment identifier of the URL of - * the current page, including the leading '#' character of the fragment - * identifier. If no fragment identifier is present, the deafult path will - * be used. - * - * @type String - */ - var angularUrl = location.hash || DEFAULT_ANGULAR_PATH; - - /** - * Appends the given parameter string to the given URL. The URL may already - * contain parameters. - * - * @param {String} url - * The URL that the given parameters should be appended to, which may - * already contain parameters. - * - * @param {String} parameters - * The parameters which should be appended to the given URL, including - * leading '?' character. - * - * @returns {String} - * A properly-formatted URL consisting of the given URL and additional - * parameters. - */ - var appendParameters = function appendParameters(url, parameters) { - - // If URL already contains parameters, replace the leading '?' with an - // '&' prior to appending more parameters - if (url.indexOf('?') !== -1) - return url + '&' + parameters.substring(1); - - // Otherwise, the provided parameters already contains the necessary - // '?' character - just append - return url + parameters; - - }; - - // If non-Angular query parameters are present, reformat the URL such that - // they are after the path and thus visible to Angular - if (parameters) { - - // Reformat the URL such that query parameters are after Angular's path - var reformattedUrl = appendParameters(baseUrl + angularUrl, parameters); - - // Simply rewrite the visible URL if the HTML5 History API is supported - if (window.history && history.replaceState) - history.replaceState(null, document.title, reformattedUrl); - - // Otherwise, redirect to the reformatted URL - else - location.href = reformattedUrl; - - } - -})(window.location); diff --git a/guacamole/src/main/frontend/src/verifyCachedVersion.js b/guacamole/src/main/frontend/src/verifyCachedVersion.js deleted file mode 100644 index db9e14409d..0000000000 --- a/guacamole/src/main/frontend/src/verifyCachedVersion.js +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -/** - * Automatically reloads the current page and clears relevant browser cache if - * the build that produced index.html is different/older than the build that - * produced the JavaScript loaded by index.html. - * - * @private - * @param {Location} location - * The Location object representing the URL of the current page. - * - * @param {Storate} [sessionStorage] - * The Storage object that should optionally be used to avoid reloading the - * current page in a loop if it proves impossible to clear cache. - */ -(function verifyCachedVersion(location, sessionStorage) { - - /** - * The meta element containing the build identifier of the Guacamole build - * that produced index.html. - * - * @private - * @type {HTMLMetaElement} - */ - var buildMeta = document.head.querySelector('meta[name=build]'); - - // Verify that index.html came from the same build as this JavaScript file, - // forcing a reload if out-of-date - if (!buildMeta || buildMeta.content !== '${guacamole.build.identifier}') { - - if (sessionStorage) { - - // Bail out if we have already tried to automatically refresh the - // cache but were unsuccessful - if (sessionStorage.getItem('reloadedFor') === '${guacamole.build.identifier}') { - console.warn('The version of Guacamole cached by your ' - + 'browser does not match the version of Guacamole on the ' - + 'server. To avoid unexpected errors, please clear your ' - + 'browser cache.'); - return; - } - - sessionStorage.setItem('reloadedFor', '${guacamole.build.identifier}'); - - } - - // Force refresh of cache by issuing an HTTP request with headers that - // request revalidation of cached content - var xhr = new XMLHttpRequest(); - xhr.open('GET', '', true); - xhr.setRequestHeader('Cache-Control', 'no-cache'); - xhr.setRequestHeader('Pragma', 'no-cache'); - - xhr.onreadystatechange = function readyStateChanged() { - - // Reload current page when ready (this call to reload MAY be - // sufficient in itself to clear cache, but this is not - // guaranteed by any standard) - if (xhr.readyState === XMLHttpRequest.DONE) - location.reload(true); - - }; - - xhr.send(); - - } - -})(window.location, window.sessionStorage); diff --git a/guacamole/src/main/frontend/webpack.config.js b/guacamole/src/main/frontend/webpack.config.js deleted file mode 100644 index 9a96799119..0000000000 --- a/guacamole/src/main/frontend/webpack.config.js +++ /dev/null @@ -1,194 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you 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. - */ - -const AngularTemplateCacheWebpackPlugin = require('angular-templatecache-webpack-plugin'); -const { CleanWebpackPlugin } = require('clean-webpack-plugin'); -const ClosureWebpackPlugin = require('closure-webpack-plugin'); -const CopyPlugin = require('copy-webpack-plugin'); -const CssMinimizerPlugin = require('css-minimizer-webpack-plugin'); -const DependencyListPlugin = require('./plugins/dependency-list-plugin'); -const HtmlWebpackPlugin = require('html-webpack-plugin'); -const MiniCssExtractPlugin = require('mini-css-extract-plugin'); -const webpack = require('webpack'); - -module.exports = { - - bail: true, - mode: 'production', - stats: 'minimal', - - output: { - path: __dirname + '/dist', - filename: 'guacamole.[contenthash].js', - }, - - // Generate source maps - devtool: 'source-map', - - // Entry point for the Guacamole webapp is the "index" AngularJS module - entry: './src/app/index/indexModule.js', - - module: { - rules: [ - - // NOTE: This is required in order to parse ES2020 language features, - // like the optional chaining and nullish coalescing operators. It - // specifically needs to operate on the node-modules directory since - // Webpack 4 cannot handle such language features. - { - test: /\.js$/i, - use: { - loader: 'babel-loader', - options: { - presets: [ - ['@babel/preset-env'] - ] - } - } - }, - - // Automatically extract imported CSS for later reference within separate CSS file - { - test: /\.css$/i, - use: [ - MiniCssExtractPlugin.loader, - { - loader: 'css-loader', - options: { - import: false, - url: false - } - } - ] - }, - - /* - * Necessary to be able to use angular 1 with webpack as explained in https://github.com/webpack/webpack/issues/2049 - */ - { - test: require.resolve('angular'), - loader: 'exports-loader', - options: { - type: 'commonjs', - exports: 'single window.angular' - } - } - - ] - }, - optimization: { - minimizer: [ - - // Minify using Google Closure Compiler - new ClosureWebpackPlugin({ mode: 'STANDARD' }, { - languageIn: 'ECMASCRIPT_2020', - languageOut: 'ECMASCRIPT5', - compilationLevel: 'SIMPLE' - }), - - new CssMinimizerPlugin() - - ], - splitChunks: { - cacheGroups: { - - // Bundle CSS as one file - styles: { - name: 'styles', - test: /\.css$/, - chunks: 'all', - enforce: true - } - - } - } - }, - plugins: [ - - new AngularTemplateCacheWebpackPlugin({ - module: 'templates-main', - root: 'app/', - source: 'src/app/**/*.html', - standalone: true - }), - - // Automatically clean out dist/ directory - new CleanWebpackPlugin(), - - // Copy static files to dist/ - new CopyPlugin([ - { from: 'app/**/*' }, - { from: 'fonts/**/*' }, - { from: 'images/**/*' }, - { from: 'layouts/**/*' }, - { from: 'manifest.json' }, - { from: 'translations/**/*' }, - { from: 'verifyCachedVersion.js' } - ], { - context: 'src/' - }), - - // Copy core libraries for global inclusion - new CopyPlugin([ - { from: 'angular/angular.min.js' }, - { from: 'blob-polyfill/Blob.js' }, - { from: 'datalist-polyfill/datalist-polyfill.min.js' }, - { from: 'jquery/dist/jquery.min.js' }, - { from: 'lodash/lodash.min.js' } - ], { - context: 'node_modules/' - }), - - // Generate index.html from template - new HtmlWebpackPlugin({ - inject: false, - template: 'src/index.html' - }), - - // Extract CSS from Webpack bundle as separate file - new MiniCssExtractPlugin({ - filename: 'guacamole.[contenthash].css', - chunkFilename: '[id].guacamole.[contenthash].css' - }), - - // List all bundled node modules for sake of automatic LICENSE file - // generation / sanity checks - new DependencyListPlugin(), - - // Automatically require used modules - new webpack.ProvidePlugin({ - jstz: 'jstz', - Pickr: '@simonwep/pickr', - saveAs: 'file-saver' - }) - - ], - resolve: { - - // Include Node modules and base source tree within search path for - // import/resolve - modules: [ - 'src', - 'node_modules' - ] - - } - -}; - diff --git a/guacamole/src/main/guacamole-frontend/.editorconfig b/guacamole/src/main/guacamole-frontend/.editorconfig new file mode 100644 index 0000000000..1102d187b8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/.editorconfig @@ -0,0 +1,15 @@ +root = true + +[*] +charset = utf-8 +indent_style = space +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = true + +[*.ts] +quote_type = single + +[*.md] +max_line_length = off +trim_trailing_whitespace = false diff --git a/guacamole/src/main/guacamole-frontend/.gitignore b/guacamole/src/main/guacamole-frontend/.gitignore new file mode 100644 index 0000000000..2b4f545ddd --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/.gitignore @@ -0,0 +1,12 @@ +# Compiled output +dist/ + +# Node +node_modules/ + +# Miscellaneous +.angular/ +.idea/ + +# Build output guacamole-common-js +**/guacamole-common-js/all.min.js diff --git a/guacamole/src/main/guacamole-frontend/README.md b/guacamole/src/main/guacamole-frontend/README.md new file mode 100644 index 0000000000..1c528e4ecc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/README.md @@ -0,0 +1,5 @@ +# Guacamole Angular Frontend + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). diff --git a/guacamole/src/main/guacamole-frontend/angular.json b/guacamole/src/main/guacamole-frontend/angular.json new file mode 100644 index 0000000000..fea1eea7f6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/angular.json @@ -0,0 +1,274 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "guacamole-frontend": { + "projectType": "application", + "schematics": {}, + "root": "projects/guacamole-frontend", + "sourceRoot": "projects/guacamole-frontend/src", + "prefix": "guac", + "architect": { + "build": { + "builder": "@angular-architects/native-federation:build", + "options": {}, + "configurations": { + "production": { + "target": "guacamole-frontend:esbuild:production" + }, + "development": { + "target": "guacamole-frontend:esbuild:development", + "dev": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-architects/native-federation:build", + "options": { + "target": "guacamole-frontend:serve-original:development", + "rebuildDelay": 0, + "dev": true, + "port": 0 + } + }, + "extract-i18n": { + "builder": "ngx-build-plus:extract-i18n", + "options": { + "browserTarget": "guacamole-frontend:build", + "extraWebpackConfig": "projects/guacamole-frontend/webpack.config.js" + } + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "tsConfig": "projects/guacamole-frontend/tsconfig.spec.json", + "assets": [ + "projects/guacamole-frontend/src/assets" + ], + "styles": [ + "projects/guacamole-frontend/src/styles.css" + ], + "scripts": [ + "dist/guacamole-frontend-lib/assets/guacamole-common-js/all.min.js" + ], + "karmaConfig": "projects/guacamole-frontend/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/guacamole-frontend/**/*.ts", + "projects/guacamole-frontend/**/*.html" + ] + } + }, + "esbuild": { + "builder": "@angular-devkit/build-angular:application", + "options": { + "outputPath": { + "base": "dist/guacamole-frontend", + "browser": "" + }, + "index": "projects/guacamole-frontend/src/index.html", + "polyfills": [ + "zone.js", + "es-module-shims" + ], + "tsConfig": "projects/guacamole-frontend/tsconfig.app.json", + "assets": [ + { + "glob": "**/*", + "input": "projects/guacamole-frontend/src/images", + "output": "images" + }, + { + "glob": "**/*", + "input": "projects/guacamole-frontend/src/fonts", + "output": "fonts" + }, + { + "glob": "**/*.json", + "input": "projects/guacamole-frontend/src/translations", + "output": "translations" + }, + "projects/guacamole-frontend/src/manifest.json", + { + "glob": "**/*.json", + "input": "dist/guacamole-frontend-lib/assets/layouts", + "output": "layouts" + } + ], + "styles": [ + "projects/guacamole-frontend/src/styles.css", + "dist/guacamole-frontend-lib/assets/styles/element/styles/resize-sensor.css", + "dist/guacamole-frontend-lib/assets/styles/osk/styles/osk.css" + ], + "scripts": [ + "dist/guacamole-frontend-lib/assets/guacamole-common-js/all.min.js" + ], + "browser": "projects/guacamole-frontend/src/main.ts", + "allowedCommonJsDependencies": [ + "lodash", + "jquery", + "fuzzysort", + "@simonwep/pickr", + "angular-expressions", + "jstz", + "file-saver" + ] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "1.5mb", + "maximumError": "2mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "9kb" + } + ], + "outputHashing": "all", + "externalDependencies": [ + "images/*", + "fonts/*" + ] + }, + "development": { + "optimization": false, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true, + "externalDependencies": [ + "images/*", + "fonts/*" + ] + } + }, + "defaultConfiguration": "production" + }, + "serve-original": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "buildTarget": "guacamole-frontend:esbuild:production" + }, + "development": { + "buildTarget": "guacamole-frontend:esbuild:development" + } + }, + "defaultConfiguration": "development", + "options": { + "port": 4200 + } + } + } + }, + "guacamole-frontend-lib": { + "projectType": "library", + "root": "projects/guacamole-frontend-lib", + "sourceRoot": "projects/guacamole-frontend-lib/src", + "prefix": "guac", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/guacamole-frontend-lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/guacamole-frontend-lib/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/guacamole-frontend-lib/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "projects/guacamole-frontend-lib/tsconfig.spec.json", + "polyfills": [ + "zone.js", + "zone.js/testing" + ], + "scripts": [ + "projects/guacamole-frontend-lib/src/assets/guacamole-common-js/all.min.js" + ], + "karmaConfig": "projects/guacamole-frontend-lib/karma.conf.js" + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/guacamole-frontend-lib/**/*.ts", + "projects/guacamole-frontend-lib/**/*.html" + ], + "eslintConfig": "projects/guacamole-frontend-lib/eslint.config.js" + } + } + } + }, + "guacamole-frontend-ext-lib": { + "projectType": "library", + "root": "projects/guacamole-frontend-ext-lib", + "sourceRoot": "projects/guacamole-frontend-ext-lib/src", + "prefix": "guac", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:ng-packagr", + "options": { + "project": "projects/guacamole-frontend-ext-lib/ng-package.json" + }, + "configurations": { + "production": { + "tsConfig": "projects/guacamole-frontend-ext-lib/tsconfig.lib.prod.json" + }, + "development": { + "tsConfig": "projects/guacamole-frontend-ext-lib/tsconfig.lib.json" + } + }, + "defaultConfiguration": "production" + }, + "test": { + "builder": "@angular-devkit/build-angular:karma", + "options": { + "tsConfig": "projects/guacamole-frontend-ext-lib/tsconfig.spec.json", + "polyfills": [ + "zone.js", + "zone.js/testing" + ] + } + }, + "lint": { + "builder": "@angular-eslint/builder:lint", + "options": { + "lintFilePatterns": [ + "projects/guacamole-frontend-ext-lib/**/*.ts", + "projects/guacamole-frontend-ext-lib/**/*.html" + ], + "eslintConfig": "projects/guacamole-frontend-lib/eslint.config.js" + } + } + } + } + }, + "cli": { + "analytics": false, + "schematicCollections": [ + "@angular-eslint/schematics" + ] + } +} diff --git a/guacamole/src/main/guacamole-frontend/eslint.config.js b/guacamole/src/main/guacamole-frontend/eslint.config.js new file mode 100644 index 0000000000..a3c6097099 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/eslint.config.js @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// @ts-check +const eslint = require('@eslint/js'); +const tseslint = require('typescript-eslint'); +const angular = require('angular-eslint'); + +module.exports = tseslint.config( + { + files: ['**/*.ts'], + extends: [ + eslint.configs.recommended, + ...tseslint.configs.recommended, + ...tseslint.configs.stylistic, + ...angular.configs.tsRecommended, + ], + processor: angular.processInlineTemplates, + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + type: 'attribute', + prefix: 'guac', + style: 'camelCase', + }, + ], + '@angular-eslint/component-selector': [ + 'error', + { + type: 'element', + prefix: 'guac', + style: 'kebab-case', + }, + ], + '@angular-eslint/prefer-standalone': 'off' + } + }, + { + files: ['**/*.html'], + extends: [ + ...angular.configs.templateRecommended, + ...angular.configs.templateAccessibility, + ], + 'rules': { + '@angular-eslint/template/elements-content': 'off' + } + } +); diff --git a/guacamole/src/main/guacamole-frontend/package-lock.json b/guacamole/src/main/guacamole-frontend/package-lock.json new file mode 100644 index 0000000000..35af60c4e7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/package-lock.json @@ -0,0 +1,17633 @@ +{ + "name": "frontend", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frontend", + "version": "0.0.0", + "dependencies": { + "@angular-architects/native-federation": "^19.0.5", + "@angular/animations": "^19.0.6", + "@angular/common": "^19.0.6", + "@angular/compiler": "^19.0.6", + "@angular/core": "^19.0.6", + "@angular/forms": "^19.0.6", + "@angular/platform-browser": "^19.0.6", + "@angular/platform-browser-dynamic": "^19.0.6", + "@angular/router": "^19.0.6", + "@ngneat/transloco": "^4.2.2", + "@ngneat/transloco-messageformat": "^4.1.0", + "@simonwep/pickr": "^1.8.2", + "@softarc/native-federation-node": "^2.0.10", + "angular-expressions": "^1.1.9", + "csv": "^6.2.5", + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "es-module-shims": "^1.5.12", + "file-saver": "^2.0.5", + "fuzzysort": "^3.0.2", + "jquery": "^3.6.4", + "jstz": "^2.1.1", + "lodash": "^4.17.21", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "yaml": "^2.2.2", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.0.7", + "@angular/cli": "~19.0.7", + "@angular/compiler-cli": "^19.0.6", + "@types/d3-path": "^3.1.0", + "@types/d3-shape": "^3.1.7", + "@types/file-saver": "^2.0.5", + "@types/guacamole-common-js": "^1.5.3", + "@types/jasmine": "~5.1.0", + "@types/jquery": "^3.5.6", + "@types/lodash": "^4.14.194", + "angular-eslint": "19.0.2", + "eslint": "^9.16.0", + "jasmine-core": "~5.4.0", + "karma": "~6.4.0", + "karma-coverage": "~2.2.0", + "karma-firefox-launcher": "~2.1.2", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "ng-packagr": "^19.0.1", + "ngx-build-plus": "^19.0.0", + "typescript": "~5.6.3", + "typescript-eslint": "8.18.0" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@angular-architects/native-federation": { + "version": "19.0.5", + "resolved": "https://registry.npmjs.org/@angular-architects/native-federation/-/native-federation-19.0.5.tgz", + "integrity": "sha512-si09fH1WqXa0oRKqwpemsSF6iTlqvm91lRzdXTSxDQ1JnpKmn6eYYL1zeoVv2kOTp+nD/8vZxOW2G/NIvFjY6A==", + "license": "MIT", + "dependencies": { + "@babel/core": "^7.19.0", + "@chialab/esbuild-plugin-commonjs": "^0.18.0", + "@softarc/native-federation": "2.0.18", + "@softarc/native-federation-runtime": "2.0.18", + "@types/browser-sync": "^2.29.0", + "browser-sync": "^3.0.2", + "esbuild": "^0.19.5", + "mrmime": "^1.0.1", + "npmlog": "^6.0.2", + "process": "0.11.10" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/aix-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz", + "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz", + "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz", + "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/android-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz", + "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/darwin-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz", + "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/darwin-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz", + "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/freebsd-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz", + "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/freebsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz", + "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-arm": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz", + "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz", + "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz", + "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-loong64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz", + "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-mips64el": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz", + "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-ppc64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz", + "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-riscv64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz", + "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-s390x": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz", + "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/linux-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz", + "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/netbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz", + "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/openbsd-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz", + "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/sunos-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz", + "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-arm64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz", + "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-ia32": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz", + "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/@esbuild/win32-x64": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz", + "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/esbuild": { + "version": "0.19.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz", + "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.19.12", + "@esbuild/android-arm": "0.19.12", + "@esbuild/android-arm64": "0.19.12", + "@esbuild/android-x64": "0.19.12", + "@esbuild/darwin-arm64": "0.19.12", + "@esbuild/darwin-x64": "0.19.12", + "@esbuild/freebsd-arm64": "0.19.12", + "@esbuild/freebsd-x64": "0.19.12", + "@esbuild/linux-arm": "0.19.12", + "@esbuild/linux-arm64": "0.19.12", + "@esbuild/linux-ia32": "0.19.12", + "@esbuild/linux-loong64": "0.19.12", + "@esbuild/linux-mips64el": "0.19.12", + "@esbuild/linux-ppc64": "0.19.12", + "@esbuild/linux-riscv64": "0.19.12", + "@esbuild/linux-s390x": "0.19.12", + "@esbuild/linux-x64": "0.19.12", + "@esbuild/netbsd-x64": "0.19.12", + "@esbuild/openbsd-x64": "0.19.12", + "@esbuild/sunos-x64": "0.19.12", + "@esbuild/win32-arm64": "0.19.12", + "@esbuild/win32-ia32": "0.19.12", + "@esbuild/win32-x64": "0.19.12" + } + }, + "node_modules/@angular-architects/native-federation/node_modules/mrmime": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-1.0.1.tgz", + "integrity": "sha512-hzzEagAgDyoU1Q6yg5uI+AorQgdvMCur3FcKf7NhMKWsaYg+RnbTyHRa/9IlLF9rf455MOCtcqqrQQ83pPP7Uw==", + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/@angular-devkit/architect": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1900.7.tgz", + "integrity": "sha512-3dRV0IB+MbNYbAGbYEFMcABkMphqcTvn5MG79dQkwcf2a9QZxCq2slwf/rIleWoDUcFm9r1NnVPYrTYNYJaqQg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-devkit/build-angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.0.7.tgz", + "integrity": "sha512-R0vpJ+P5xBqF82zOMq2FvOP7pJz5NZ7PwHAIFuQ6z50SHLW/VcUA19ZoFKwxBX6A/Soyb66QXTcjZ5wbRqMm8w==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/build-webpack": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular/build": "19.0.7", + "@babel/core": "7.26.0", + "@babel/generator": "7.26.2", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-transform-async-generator-functions": "7.25.9", + "@babel/plugin-transform-async-to-generator": "7.25.9", + "@babel/plugin-transform-runtime": "7.25.9", + "@babel/preset-env": "7.26.0", + "@babel/runtime": "7.26.0", + "@discoveryjs/json-ext": "0.6.3", + "@ngtools/webpack": "19.0.7", + "@vitejs/plugin-basic-ssl": "1.1.0", + "ansi-colors": "4.1.3", + "autoprefixer": "10.4.20", + "babel-loader": "9.2.1", + "browserslist": "^4.21.5", + "copy-webpack-plugin": "12.0.2", + "css-loader": "7.1.2", + "esbuild-wasm": "0.24.0", + "fast-glob": "3.3.2", + "http-proxy-middleware": "3.0.3", + "istanbul-lib-instrument": "6.0.3", + "jsonc-parser": "3.3.1", + "karma-source-map-support": "1.4.0", + "less": "4.2.0", + "less-loader": "12.2.0", + "license-webpack-plugin": "4.0.2", + "loader-utils": "3.3.1", + "mini-css-extract-plugin": "2.9.2", + "open": "10.1.0", + "ora": "5.4.1", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "postcss": "8.4.49", + "postcss-loader": "8.1.1", + "resolve-url-loader": "5.0.0", + "rxjs": "7.8.1", + "sass": "1.80.7", + "sass-loader": "16.0.3", + "semver": "7.6.3", + "source-map-loader": "5.0.0", + "source-map-support": "0.5.21", + "terser": "5.36.0", + "tree-kill": "1.2.2", + "tslib": "2.8.1", + "webpack": "5.96.1", + "webpack-dev-middleware": "7.4.2", + "webpack-dev-server": "5.1.0", + "webpack-merge": "6.0.1", + "webpack-subresource-integrity": "5.1.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "esbuild": "0.24.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.0.7", + "@web/test-runner": "^0.19.0", + "browser-sync": "^3.0.2", + "jest": "^29.5.0", + "jest-environment-jsdom": "^29.5.0", + "karma": "^6.3.0", + "ng-packagr": "^19.0.0", + "protractor": "^7.0.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.5 <5.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "@web/test-runner": { + "optional": true + }, + "browser-sync": { + "optional": true + }, + "jest": { + "optional": true + }, + "jest-environment-jsdom": { + "optional": true + }, + "karma": { + "optional": true + }, + "ng-packagr": { + "optional": true + }, + "protractor": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-webpack": { + "version": "0.1900.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1900.7.tgz", + "integrity": "sha512-F0S0iyspo/9w9rP5F9wmL+ZkBr48YQIWiFu+PaQ0in/lcdRmY/FjVHTMa5BMnlew9VCtFHPvpoN9x4u8AIoWXA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1900.7", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^5.0.2" + } + }, + "node_modules/@angular-devkit/core": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.0.7.tgz", + "integrity": "sha512-VyuORSitT6LIaGUEF0KEnv2TwNaeWl6L3/4L4stok0BJ23B4joVca2DYVcrLC1hSzz8V4dwVgSlbNIgjgGdVpg==", + "dev": true, + "dependencies": { + "ajv": "8.17.1", + "ajv-formats": "3.0.1", + "jsonc-parser": "3.3.1", + "picomatch": "4.0.2", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "chokidar": "^4.0.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/schematics": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.0.7.tgz", + "integrity": "sha512-BHXQv6kMc9xo4TH9lhwMv8nrZXHkLioQvLun2qYjwvOsyzt3qd+sUM9wpHwbG6t+01+FIQ05iNN9ox+Cvpndgg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "jsonc-parser": "3.3.1", + "magic-string": "0.30.12", + "ora": "5.4.1", + "rxjs": "7.8.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular-eslint/builder": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-19.0.2.tgz", + "integrity": "sha512-BdmMSndQt2fSBiTVniskUcUpQaeweUapbsL0IDfQ7a13vL0NVXpc3K89YXuVE/xsb08uHtqphuwxPAAj6kX3OA==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": ">= 0.1900.0 < 0.2000.0", + "@angular-devkit/core": ">= 19.0.0 < 20.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/bundled-angular-compiler": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-19.0.2.tgz", + "integrity": "sha512-HPmp92r70SNO/0NdIaIhxrgVSpomqryuUk7jszvNRtu+OzYCJGcbLhQD38T3dbBWT/AV0QXzyzExn6/2ai9fEw==", + "dev": true + }, + "node_modules/@angular-eslint/eslint-plugin": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-19.0.2.tgz", + "integrity": "sha512-DLuNVVGGFicSThOcMSJyNje+FZSPdG0B3lCBRiqcgKH/16kfM4pV8MobPM7RGK2NhaOmmZ4zzJNwpwWPSgi+Lw==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2", + "@angular-eslint/utils": "19.0.2" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/eslint-plugin-template": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-19.0.2.tgz", + "integrity": "sha512-f/OCF9ThnxQ8m0eNYPwnCrySQPhYfCOF6STL7F9LnS8Bs3ZeW3/oT1yLaMIZ1Eg0ogIkgxksMAJZjrJPUPBD1Q==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2", + "@angular-eslint/utils": "19.0.2", + "aria-query": "5.3.2", + "axobject-query": "4.1.0" + }, + "peerDependencies": { + "@typescript-eslint/types": "^7.11.0 || ^8.0.0", + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/schematics": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-19.0.2.tgz", + "integrity": "sha512-wI4SyiAnUCrpigtK6PHRlVWMC9vWljqmlLhbsJV5O5yDajlmRdvgXvSHDefhJm0hSfvZYRXuiAARYv2+QVfnGA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": ">= 19.0.0 < 20.0.0", + "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "ignore": "6.0.2", + "semver": "7.6.3", + "strip-json-comments": "3.1.1" + } + }, + "node_modules/@angular-eslint/template-parser": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-19.0.2.tgz", + "integrity": "sha512-z3rZd2sBfuYcFf9rGDsB2zz2fbGX8kkF+0ftg9eocyQmzWrlZHFmuw9ha7oP/Mz8gpblyCS/aa1U/Srs6gz0UQ==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2", + "eslint-scope": "^8.0.2" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular-eslint/utils": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-19.0.2.tgz", + "integrity": "sha512-HotBT8OKr7zCaX1S9k27JuhRiTVIbbYVl6whlb3uwdMIPIWY8iOcEh1tjI4qDPUafpLfR72Dhwi5bO1E17F3/Q==", + "dev": true, + "dependencies": { + "@angular-eslint/bundled-angular-compiler": "19.0.2" + }, + "peerDependencies": { + "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*" + } + }, + "node_modules/@angular/animations": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.0.6.tgz", + "integrity": "sha512-dlXrFcw7RQNze1zjmrbwqcFd6zgEuqKwuExtEN1Fy26kQ+wqKIhYO6IG7PZGef53XpwN5DT16yve6UihJ2XeNg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6" + } + }, + "node_modules/@angular/build": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.0.7.tgz", + "integrity": "sha512-AFvhRa6sfXG8NmS8AN7TvE8q2kVcMw+zXMZzo981cqwnOwJy4VHU0htqm5OZQnohVJM0pP8SBAuROWO4yRrxCA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "2.3.0", + "@angular-devkit/architect": "0.1900.7", + "@babel/core": "7.26.0", + "@babel/helper-annotate-as-pure": "7.25.9", + "@babel/helper-split-export-declaration": "7.24.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@inquirer/confirm": "5.0.2", + "@vitejs/plugin-basic-ssl": "1.1.0", + "beasties": "0.1.0", + "browserslist": "^4.23.0", + "esbuild": "0.24.0", + "fast-glob": "3.3.2", + "https-proxy-agent": "7.0.5", + "istanbul-lib-instrument": "6.0.3", + "listr2": "8.2.5", + "magic-string": "0.30.12", + "mrmime": "2.0.0", + "parse5-html-rewriting-stream": "7.0.0", + "picomatch": "4.0.2", + "piscina": "4.7.0", + "rollup": "4.26.0", + "sass": "1.80.7", + "semver": "7.6.3", + "vite": "5.4.11", + "watchpack": "2.4.2" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "optionalDependencies": { + "lmdb": "3.1.5" + }, + "peerDependencies": { + "@angular/compiler": "^19.0.0", + "@angular/compiler-cli": "^19.0.0", + "@angular/localize": "^19.0.0", + "@angular/platform-server": "^19.0.0", + "@angular/service-worker": "^19.0.0", + "@angular/ssr": "^19.0.7", + "less": "^4.2.0", + "postcss": "^8.4.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "typescript": ">=5.5 <5.7" + }, + "peerDependenciesMeta": { + "@angular/localize": { + "optional": true + }, + "@angular/platform-server": { + "optional": true + }, + "@angular/service-worker": { + "optional": true + }, + "@angular/ssr": { + "optional": true + }, + "less": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/@angular/cli": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.0.7.tgz", + "integrity": "sha512-y6C4B4XdiZwe2+OADLWXyKqUVvW/XDzTuJ2mZ5PhTnSiiXDN4zRWId1F5wA8ve8vlbUKApPHXRQuaqiQJmA24g==", + "dev": true, + "dependencies": { + "@angular-devkit/architect": "0.1900.7", + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "@inquirer/prompts": "7.1.0", + "@listr2/prompt-adapter-inquirer": "2.0.18", + "@schematics/angular": "19.0.7", + "@yarnpkg/lockfile": "1.1.0", + "ini": "5.0.0", + "jsonc-parser": "3.3.1", + "listr2": "8.2.5", + "npm-package-arg": "12.0.0", + "npm-pick-manifest": "10.0.0", + "pacote": "20.0.0", + "resolve": "1.22.8", + "semver": "7.6.3", + "symbol-observable": "4.0.0", + "yargs": "17.7.2" + }, + "bin": { + "ng": "bin/ng.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@angular/common": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.0.6.tgz", + "integrity": "sha512-r9IDD0+UGkrQkjyX+pApeDmIJ9INpr1uYlgmmlWNBJCVNr9SKKIVZV60sssgadew6bGynKN9dW4mGsmEzzb5BA==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/compiler": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.0.6.tgz", + "integrity": "sha512-g8A6QOsiCJnRi5Hz0sASIpRQoAGxEgnjz0JanfrMNRedY4MpdIS1V0AeCSKTsMRlV7tQl3ng2Gse/tsb51HI3Q==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/core": "19.0.6" + }, + "peerDependenciesMeta": { + "@angular/core": { + "optional": true + } + } + }, + "node_modules/@angular/compiler-cli": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.0.6.tgz", + "integrity": "sha512-fHtwI5rCe3LmKDoaqlqLAPdNmLrbeCiMYVe+X1BHgApaqNCyAwcuJxuf8Q5R5su7nHiLmlmB74o1ZS/V+0cQ+g==", + "dev": true, + "dependencies": { + "@babel/core": "7.26.0", + "@jridgewell/sourcemap-codec": "^1.4.14", + "chokidar": "^4.0.0", + "convert-source-map": "^1.5.1", + "reflect-metadata": "^0.2.0", + "semver": "^7.0.0", + "tslib": "^2.3.0", + "yargs": "^17.2.1" + }, + "bin": { + "ng-xi18n": "bundles/src/bin/ng_xi18n.js", + "ngc": "bundles/src/bin/ngc.js", + "ngcc": "bundles/ngcc/index.js" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/compiler": "19.0.6", + "typescript": ">=5.5 <5.7" + } + }, + "node_modules/@angular/core": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.0.6.tgz", + "integrity": "sha512-9N7FmdRHtS7zfXC/wnyap/reX7fgiOrWpVivayHjWP4RkLYXJAzJIpLyew0jrx4vf8r3lZnC0Zmq0PW007Ngjw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "rxjs": "^6.5.3 || ^7.4.0", + "zone.js": "~0.15.0" + } + }, + "node_modules/@angular/forms": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.0.6.tgz", + "integrity": "sha512-HogauPvgDQHw2xxqKBaFgKTRRcc1xWeI/PByDCf3U6YsaqpF53Mz2CJh8X2bg2bY1RGKb67MZw7DBGFRvXx4bg==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@angular/platform-browser": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.0.6.tgz", + "integrity": "sha512-MWiToGy7Pa0rR61sgnEuu7dfZXpAw0g7nkSnw4xdjUf974OOOfI1LS9O9YevJibtdW8sPa1HaoXXwcb7N03B5A==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/animations": "19.0.6", + "@angular/common": "19.0.6", + "@angular/core": "19.0.6" + }, + "peerDependenciesMeta": { + "@angular/animations": { + "optional": true + } + } + }, + "node_modules/@angular/platform-browser-dynamic": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.0.6.tgz", + "integrity": "sha512-5TGLOwPlLHXJ1+Hs9b3dEmGdTpb7dfLYalVmiMUZOFBry1sMaRuw+nyqjmWn1GP3yD156hzt5QDzWA8A134AfQ==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/compiler": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6" + } + }, + "node_modules/@angular/router": { + "version": "19.0.6", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.0.6.tgz", + "integrity": "sha512-G1oz+TclPk48h6b6B4s5J3DfrDVJrrxKOA+KWeVQP4e1B8ld7/dCMf5nn3yqS4BGs4yLecxMxyvbOvOiZ//lxw==", + "dependencies": { + "tslib": "^2.3.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.0.6", + "@angular/core": "19.0.6", + "@angular/platform-browser": "19.0.6", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", + "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "dependencies": { + "@babel/helper-validator-identifier": "^7.25.9", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", + "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.26.0", + "@babel/generator": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helpers": "^7.26.0", + "@babel/parser": "^7.26.0", + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.26.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==" + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.26.2", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", + "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "dependencies": { + "@babel/parser": "^7.26.2", + "@babel/types": "^7.26.0", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.7", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", + "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", + "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", + "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "dev": true, + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.0.tgz", + "integrity": "sha512-tbhNuIxNcVb21pInl3ZSjksLCvgdZy9KwJ8brv993QtIVKJBBkYXz4q4ZbAv31GdnC+R90np23L5FbEBlthAEw==", + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.5.tgz", + "integrity": "sha512-SRJ4jYmXRqV1/Xc+TIVG84WjHBXKlxO9sHQnA2Pf12QQEAp1LOh6kDzNHXcUnbH1QI0FDoPPVOt+vyUDucxpaw==", + "dependencies": { + "@babel/types": "^7.26.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regexp-modifiers": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", + "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", + "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.25.9.tgz", + "integrity": "sha512-v61XqUMiueJROUv66BVIOi0Fv/CUuZuZMl5NkRoCVxLAnMexZ0A3kMe7vvZ0nulxMuMp0Mk6S5hNh48yki08ZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", + "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.26.0", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-option": "^7.25.9", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-import-assertions": "^7.26.0", + "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.9", + "@babel/plugin-transform-async-generator-functions": "^7.25.9", + "@babel/plugin-transform-async-to-generator": "^7.25.9", + "@babel/plugin-transform-block-scoped-functions": "^7.25.9", + "@babel/plugin-transform-block-scoping": "^7.25.9", + "@babel/plugin-transform-class-properties": "^7.25.9", + "@babel/plugin-transform-class-static-block": "^7.26.0", + "@babel/plugin-transform-classes": "^7.25.9", + "@babel/plugin-transform-computed-properties": "^7.25.9", + "@babel/plugin-transform-destructuring": "^7.25.9", + "@babel/plugin-transform-dotall-regex": "^7.25.9", + "@babel/plugin-transform-duplicate-keys": "^7.25.9", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-dynamic-import": "^7.25.9", + "@babel/plugin-transform-exponentiation-operator": "^7.25.9", + "@babel/plugin-transform-export-namespace-from": "^7.25.9", + "@babel/plugin-transform-for-of": "^7.25.9", + "@babel/plugin-transform-function-name": "^7.25.9", + "@babel/plugin-transform-json-strings": "^7.25.9", + "@babel/plugin-transform-literals": "^7.25.9", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", + "@babel/plugin-transform-member-expression-literals": "^7.25.9", + "@babel/plugin-transform-modules-amd": "^7.25.9", + "@babel/plugin-transform-modules-commonjs": "^7.25.9", + "@babel/plugin-transform-modules-systemjs": "^7.25.9", + "@babel/plugin-transform-modules-umd": "^7.25.9", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", + "@babel/plugin-transform-new-target": "^7.25.9", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", + "@babel/plugin-transform-numeric-separator": "^7.25.9", + "@babel/plugin-transform-object-rest-spread": "^7.25.9", + "@babel/plugin-transform-object-super": "^7.25.9", + "@babel/plugin-transform-optional-catch-binding": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9", + "@babel/plugin-transform-private-methods": "^7.25.9", + "@babel/plugin-transform-private-property-in-object": "^7.25.9", + "@babel/plugin-transform-property-literals": "^7.25.9", + "@babel/plugin-transform-regenerator": "^7.25.9", + "@babel/plugin-transform-regexp-modifiers": "^7.26.0", + "@babel/plugin-transform-reserved-words": "^7.25.9", + "@babel/plugin-transform-shorthand-properties": "^7.25.9", + "@babel/plugin-transform-spread": "^7.25.9", + "@babel/plugin-transform-sticky-regex": "^7.25.9", + "@babel/plugin-transform-template-literals": "^7.25.9", + "@babel/plugin-transform-typeof-symbol": "^7.25.9", + "@babel/plugin-transform-unicode-escapes": "^7.25.9", + "@babel/plugin-transform-unicode-property-regex": "^7.25.9", + "@babel/plugin-transform-unicode-regex": "^7.25.9", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", + "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", + "dev": true, + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/template": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", + "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "dependencies": { + "@babel/code-frame": "^7.25.9", + "@babel/parser": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.5.tgz", + "integrity": "sha512-rkOSPOw+AXbgtwUga3U4u8RpoK9FEFWBNAlTpcnkLFjL5CT+oyHNuUUC/xx6XefEJ16r38r8Bc/lfp6rYuHeJQ==", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.5", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.5.tgz", + "integrity": "sha512-L6mZmwFDK6Cjh1nRCLXpa6no13ZIioJDz7mdkzHv399pThrTa/k0nUlNaenOeh2kWu/iaOQYElEpKPUswUa9Vg==", + "dependencies": { + "@babel/helper-string-parser": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@chialab/cjs-to-esm": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/cjs-to-esm/-/cjs-to-esm-0.18.0.tgz", + "integrity": "sha512-fm8X9NhPO5pyUB7gxOZgwxb8lVq1UD4syDJCpqh6x4zGME6RTck7BguWZ4Zgv3GML4fQ4KZtyRwP5eoDgNGrmA==", + "license": "MIT", + "dependencies": { + "@chialab/estransform": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-plugin-commonjs": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-plugin-commonjs/-/esbuild-plugin-commonjs-0.18.0.tgz", + "integrity": "sha512-qZjIsNr1dVEJk6NLyza3pJLHeY7Fz0xjmYteKXElCnlFSKR7vVg6d18AsxVpRnP5qNbvx3XlOvs9U8j97ZQ6bw==", + "license": "MIT", + "dependencies": { + "@chialab/cjs-to-esm": "^0.18.0", + "@chialab/esbuild-rna": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/esbuild-rna": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/@chialab/esbuild-rna/-/esbuild-rna-0.18.2.tgz", + "integrity": "sha512-ckzskez7bxstVQ4c5cxbx0DRP2teldzrcSGQl2KPh1VJGdO2ZmRrb6vNkBBD5K3dx9tgTyvskWp4dV+Fbg07Ag==", + "license": "MIT", + "dependencies": { + "@chialab/estransform": "^0.18.0", + "@chialab/node-resolve": "^0.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/estransform": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@chialab/estransform/-/estransform-0.18.1.tgz", + "integrity": "sha512-W/WmjpQL2hndD0/XfR0FcPBAUj+aLNeoAVehOjV/Q9bSnioz0GVSAXXhzp59S33ZynxJBBfn8DNiMTVNJmk4Aw==", + "license": "MIT", + "dependencies": { + "@parcel/source-map": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@chialab/node-resolve": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/@chialab/node-resolve/-/node-resolve-0.18.0.tgz", + "integrity": "sha512-eV1m70Qn9pLY9xwFmZ2FlcOzwiaUywsJ7NB/ud8VB7DouvCQtIHkQ3Om7uPX0ojXGEG1LCyO96kZkvbNTxNu0Q==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", + "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", + "dev": true, + "engines": { + "node": ">=14.17.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.0.tgz", + "integrity": "sha512-WtKdFM7ls47zkKHFVzMz8opM7LkcsIp9amDUBIAWirg70RM71WRSjdILPsY5Uv1D42ZpUfaPILDlfactHgsRkw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.0.tgz", + "integrity": "sha512-arAtTPo76fJ/ICkXWetLCc9EwEHKaeya4vMrReVlEIUCAUncH7M4bhMQ+M9Vf+FFOZJdTNMXNBrWwW+OXWpSew==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.0.tgz", + "integrity": "sha512-Vsm497xFM7tTIPYK9bNTYJyF/lsP590Qc1WxJdlB6ljCbdZKU9SY8i7+Iin4kyhV/KV5J2rOKsBQbB77Ab7L/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.0.tgz", + "integrity": "sha512-t8GrvnFkiIY7pa7mMgJd7p8p8qqYIz1NYiAoKc75Zyv73L3DZW++oYMSHPRarcotTKuSs6m3hTOa5CKHaS02TQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.0.tgz", + "integrity": "sha512-CKyDpRbK1hXwv79soeTJNHb5EiG6ct3efd/FTPdzOWdbZZfGhpbcqIpiD0+vwmpu0wTIL97ZRPZu8vUt46nBSw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.0.tgz", + "integrity": "sha512-rgtz6flkVkh58od4PwTRqxbKH9cOjaXCMZgWD905JOzjFKW+7EiUObfd/Kav+A6Gyud6WZk9w+xu6QLytdi2OA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.0.tgz", + "integrity": "sha512-6Mtdq5nHggwfDNLAHkPlyLBpE5L6hwsuXZX8XNmHno9JuL2+bg2BX5tRkwjyfn6sKbxZTq68suOjgWqCicvPXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.0.tgz", + "integrity": "sha512-D3H+xh3/zphoX8ck4S2RxKR6gHlHDXXzOf6f/9dbFt/NRBDIE33+cVa49Kil4WUjxMGW0ZIYBYtaGCa2+OsQwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.0.tgz", + "integrity": "sha512-gJKIi2IjRo5G6Glxb8d3DzYXlxdEj2NlkixPsqePSZMhLudqPhtZ4BUrpIuTjJYXxvF9njql+vRjB2oaC9XpBw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.0.tgz", + "integrity": "sha512-TDijPXTOeE3eaMkRYpcy3LarIg13dS9wWHRdwYRnzlwlA370rNdZqbcp0WTyyV/k2zSxfko52+C7jU5F9Tfj1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.0.tgz", + "integrity": "sha512-K40ip1LAcA0byL05TbCQ4yJ4swvnbzHscRmUilrmP9Am7//0UjPreh4lpYzvThT2Quw66MhjG//20mrufm40mA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.0.tgz", + "integrity": "sha512-0mswrYP/9ai+CU0BzBfPMZ8RVm3RGAN/lmOMgW4aFUSOQBjA31UP8Mr6DDhWSuMwj7jaWOT0p0WoZ6jeHhrD7g==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.0.tgz", + "integrity": "sha512-hIKvXm0/3w/5+RDtCJeXqMZGkI2s4oMUGj3/jM0QzhgIASWrGO5/RlzAzm5nNh/awHE0A19h/CvHQe6FaBNrRA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.0.tgz", + "integrity": "sha512-HcZh5BNq0aC52UoocJxaKORfFODWXZxtBaaZNuN3PUX3MoDsChsZqopzi5UupRhPHSEHotoiptqikjN/B77mYQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.0.tgz", + "integrity": "sha512-bEh7dMn/h3QxeR2KTy1DUszQjUrIHPZKyO6aN1X4BCnhfYhuQqedHaa5MxSQA/06j3GpiIlFGSsy1c7Gf9padw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.0.tgz", + "integrity": "sha512-ZcQ6+qRkw1UcZGPyrCiHHkmBaj9SiCD8Oqd556HldP+QlpUIe2Wgn3ehQGVoPOvZvtHm8HPx+bH20c9pvbkX3g==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.0.tgz", + "integrity": "sha512-vbutsFqQ+foy3wSSbmjBXXIJ6PL3scghJoM8zCL142cGaZKAdCZHyf+Bpu/MmX9zT9Q0zFBVKb36Ma5Fzfa8xA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.0.tgz", + "integrity": "sha512-hjQ0R/ulkO8fCYFsG0FZoH+pWgTTDreqpqY7UnQntnaKv95uP5iW3+dChxnx7C3trQQU40S+OgWhUVwCjVFLvg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.0.tgz", + "integrity": "sha512-MD9uzzkPQbYehwcN583yx3Tu5M8EIoTD+tUgKF982WYL9Pf5rKy9ltgD0eUgs8pvKnmizxjXZyLt0z6DC3rRXg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.0.tgz", + "integrity": "sha512-4ir0aY1NGUhIC1hdoCzr1+5b43mw99uNwVzhIq1OY3QcEwPDO3B7WNXBzaKY5Nsf1+N11i1eOfFcq+D/gOS15Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.0.tgz", + "integrity": "sha512-jVzdzsbM5xrotH+W5f1s+JtUy1UWgjU0Cf4wMvffTB8m6wP5/kx0KiaLHlbJO+dMgtxKV8RQ/JvtlFcdZ1zCPA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.0.tgz", + "integrity": "sha512-iKc8GAslzRpBytO2/aN3d2yb2z8XTVfNV0PjGlCxKo5SgWmNXx82I/Q3aG1tFfS+A2igVCY97TJ8tnYwpUWLCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.0.tgz", + "integrity": "sha512-vQW36KZolfIudCcTnaTpmLQ24Ha1RjygBo39/aLkM2kmjkWmZGEJ5Gn9l5/7tzXA42QGIoWbICfg6KLLkIw6yw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.0.tgz", + "integrity": "sha512-7IAFPrjSQIJrGsK6flwg7NFmwBoSTyF3rl7If0hNUFQU4ilTsEPL6GuMuU9BfIWVVGuRnuIidkSMC+c0Otu8IA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", + "integrity": "sha512-s3O3waFUrMV8P/XaF/+ZTp1X9XBZW1a4B97ZnjQF2KYWaFD2A8KyFBsrsfSjEmjn3RGWAIuvlneuZm3CUK3jbA==", + "dev": true, + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.1.tgz", + "integrity": "sha512-fo6Mtm5mWyKjA/Chy1BYTdn5mGJoDNjC7C64ug20ADsRDGrA85bN3uK3MaKbeRkRuuIEAR5N33Jr1pbm411/PA==", + "dev": true, + "dependencies": { + "@eslint/object-schema": "^2.1.5", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/core": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.10.0.tgz", + "integrity": "sha512-gFHJ+xBOo4G3WRlR1e/3G8A6/KZAH6zcE/hkLRCZTi/B9avAG365QhFA8uOGzTMqgTghpn7/fSnscW++dpMSAw==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.2.0.tgz", + "integrity": "sha512-grOjVNN8P3hjJn/eIETF1wwd12DdnwFDoyceUJLYYdkpbwq3nLi+4fqrTAONx7XDALqlL220wC/RHSC/QTI/0w==", + "dev": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.18.0.tgz", + "integrity": "sha512-fK6L7rxcq6/z+AaQMtiFTkvbHkBLNlwyRxHpKawP0x3u9+NC6MQTnFW+AdpwC6gfHTW0051cokQgtTN2FqlxQA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.5.tgz", + "integrity": "sha512-o0bhxnL89h5Bae5T318nFoFzGy+YE5i/gGkoPAgkmTVdRKTiv3p8JHevPiPaMwoloKfEiiaHlawCqaZMqRm+XQ==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.5.tgz", + "integrity": "sha512-lB05FkqEdUg2AA0xEbUz0SnkXT1LcCTa438W4IWTUh4hdOnVbQyOJ81OrDXsJk/LSiJHubgGEFoR5EHq1NsH1A==", + "dev": true, + "dependencies": { + "@eslint/core": "^0.10.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.1.tgz", + "integrity": "sha512-c7hNEllBlenFTHBky65mhq8WD2kbN9Q6gk0bTk8lSBvc554jpXSkST1iePudpt7+A/AQvuHs9EMqjHDXMY1lrA==", + "dev": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@inquirer/checkbox": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", + "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/confirm": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.0.2.tgz", + "integrity": "sha512-KJLUHOaKnNCYzwVbryj3TNBxyZIrr56fR5N45v6K9IPrbT6B7DcudBMfylkV1A8PUdJE15mybkEQyp2/ZUpxUA==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.0", + "@inquirer/type": "^3.0.1" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/core": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", + "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "dev": true, + "dependencies": { + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "cli-width": "^4.1.0", + "mute-stream": "^2.0.0", + "signal-exit": "^4.1.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^6.2.0", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/editor": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", + "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "external-editor": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/expand": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", + "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/figures": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", + "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/@inquirer/input": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", + "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/number": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", + "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/password": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", + "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/prompts": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.1.0.tgz", + "integrity": "sha512-5U/XiVRH2pp1X6gpNAjWOglMf38/Ys522ncEHIKT1voRUvSj/DQnR22OVxHnwu5S+rCFaUiPQ57JOtMFQayqYA==", + "dev": true, + "dependencies": { + "@inquirer/checkbox": "^4.0.2", + "@inquirer/confirm": "^5.0.2", + "@inquirer/editor": "^4.1.0", + "@inquirer/expand": "^4.0.2", + "@inquirer/input": "^4.0.2", + "@inquirer/number": "^3.0.2", + "@inquirer/password": "^4.0.2", + "@inquirer/rawlist": "^4.0.2", + "@inquirer/search": "^3.0.2", + "@inquirer/select": "^4.0.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/rawlist": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", + "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/search": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", + "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/select": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", + "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "dev": true, + "dependencies": { + "@inquirer/core": "^10.1.4", + "@inquirer/figures": "^1.0.9", + "@inquirer/type": "^3.0.2", + "ansi-escapes": "^4.3.2", + "yoctocolors-cjs": "^2.1.2" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@inquirer/type": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", + "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "dev": true, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", + "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "dependencies": { + "@jridgewell/set-array": "^1.2.1", + "@jridgewell/sourcemap-codec": "^1.4.10", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz", + "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@jsonjoy.com/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/json-pack": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", + "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", + "dev": true, + "dependencies": { + "@jsonjoy.com/base64": "^1.1.1", + "@jsonjoy.com/util": "^1.1.2", + "hyperdyperid": "^1.2.0", + "thingies": "^1.20.0" + }, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@jsonjoy.com/util": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", + "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", + "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", + "dev": true + }, + "node_modules/@listr2/prompt-adapter-inquirer": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", + "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "dev": true, + "dependencies": { + "@inquirer/type": "^1.5.5" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@inquirer/prompts": ">= 3 < 8" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/@inquirer/type": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.5.tgz", + "integrity": "sha512-MzICLu4yS7V8AA61sANROZ9vT1H3ooca5dSmI1FjZkzq7o/koMsRfQSzRtFo+F3Ao4Sf1C0bpLKejpKB/+j6MA==", + "dev": true, + "dependencies": { + "mute-stream": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@listr2/prompt-adapter-inquirer/node_modules/mute-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-1.0.0.tgz", + "integrity": "sha512-avsJQhyd+680gKXyG/sQc0nXaC6rBkPOfyHYcFb9+hdkqQkR9bdnkJ0AMZhke0oesPqIO+mFFJ+IdBc7mst4IA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/@lmdb/lmdb-darwin-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.1.5.tgz", + "integrity": "sha512-ue5PSOzHMCIYrfvPP/MRS6hsKKLzqqhcdAvJCO8uFlDdj598EhgnacuOTuqA6uBK5rgiZXfDWyb7DVZSiBKxBA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-darwin-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.1.5.tgz", + "integrity": "sha512-CGhsb0R5vE6mMNCoSfxHFD8QTvBHM51gs4DBeigTYHWnYv2V5YpJkC4rMo5qAAFifuUcc0+a8a3SIU0c9NrfNw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.1.5.tgz", + "integrity": "sha512-3WeW328DN+xB5PZdhSWmqE+t3+44xWXEbqQ+caWJEZfOFdLp9yklBZEbVqVdqzznkoaXJYxTCp996KD6HmANeg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-arm64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.1.5.tgz", + "integrity": "sha512-LAjaoOcBHGj6fiYB8ureiqPoph4eygbXu4vcOF+hsxiY74n8ilA7rJMmGUT0K0JOB5lmRQHSmor3mytRjS4qeQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-linux-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.1.5.tgz", + "integrity": "sha512-k/IklElP70qdCXOQixclSl2GPLFiopynGoKX1FqDd1/H0E3Fo1oPwjY2rEVu+0nS3AOw1sryStdXk8CW3cVIsw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@lmdb/lmdb-win32-x64": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.1.5.tgz", + "integrity": "sha512-KYar6W8nraZfSJspcK7Kp7hdj238X/FNauYbZyrqPBrtsXI1hvI4/KcRcRGP50aQoV7fkKDyJERlrQGMGTZUsA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@messageformat/core": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/@messageformat/core/-/core-3.4.0.tgz", + "integrity": "sha512-NgCFubFFIdMWJGN5WuQhHCNmzk7QgiVfrViFxcS99j7F5dDS5EP6raR54I+2ydhe4+5/XTn/YIEppFaqqVWHsw==", + "dependencies": { + "@messageformat/date-skeleton": "^1.0.0", + "@messageformat/number-skeleton": "^1.0.0", + "@messageformat/parser": "^5.1.0", + "@messageformat/runtime": "^3.0.1", + "make-plural": "^7.0.0", + "safe-identifier": "^0.4.1" + } + }, + "node_modules/@messageformat/date-skeleton": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@messageformat/date-skeleton/-/date-skeleton-1.1.0.tgz", + "integrity": "sha512-rmGAfB1tIPER+gh3p/RgA+PVeRE/gxuQ2w4snFWPF5xtb5mbWR7Cbw7wCOftcUypbD6HVoxrVdyyghPm3WzP5A==" + }, + "node_modules/@messageformat/number-skeleton": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@messageformat/number-skeleton/-/number-skeleton-1.2.0.tgz", + "integrity": "sha512-xsgwcL7J7WhlHJ3RNbaVgssaIwcEyFkBqxHdcdaiJzwTZAWEOD8BuUFxnxV9k5S0qHN3v/KzUpq0IUpjH1seRg==" + }, + "node_modules/@messageformat/parser": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@messageformat/parser/-/parser-5.1.1.tgz", + "integrity": "sha512-3p0YRGCcTUCYvBKLIxtDDyrJ0YijGIwrTRu1DT8gIviIDZru8H23+FkY6MJBzM1n9n20CiM4VeDYuBsrrwnLjg==", + "dependencies": { + "moo": "^0.5.1" + } + }, + "node_modules/@messageformat/runtime": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@messageformat/runtime/-/runtime-3.0.1.tgz", + "integrity": "sha512-6RU5ol2lDtO8bD9Yxe6CZkl0DArdv0qkuoZC+ZwowU+cdRlVE1157wjCmlA5Rsf1Xc/brACnsZa5PZpEDfTFFg==", + "dependencies": { + "make-plural": "^7.0.0" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", + "integrity": "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.3.tgz", + "integrity": "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.3.tgz", + "integrity": "sha512-fg0uy/dG/nZEXfYilKoRe7yALaNmHoYeIoJuJ7KJ+YyU2bvY8vPv27f7UKhGRpY6euFYqEVhxCFZgAUNQBM3nw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.3.tgz", + "integrity": "sha512-YxQL+ax0XqBJDZiKimS2XQaf+2wDGVa1enVRGzEvLLVFeqa5kx2bWbtcSXgsxjQB7nRqqIGFIcLteF/sHeVtQg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.3.tgz", + "integrity": "sha512-cvwNfbP07pKUfq1uH+S6KJ7dT9K8WOE4ZiAcsrSes+UY55E/0jLYc+vq+DO7jlmqRb5zAggExKm0H7O/CBaesg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.3.tgz", + "integrity": "sha512-x0fWaQtYp4E6sktbsdAqnehxDgEc/VwM7uLsRCYWaiGu0ykYdZPiS8zCWdnjHwyiumousxfBm4SO31eXqwEZhQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@napi-rs/nice": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", + "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/nice-android-arm-eabi": "1.0.1", + "@napi-rs/nice-android-arm64": "1.0.1", + "@napi-rs/nice-darwin-arm64": "1.0.1", + "@napi-rs/nice-darwin-x64": "1.0.1", + "@napi-rs/nice-freebsd-x64": "1.0.1", + "@napi-rs/nice-linux-arm-gnueabihf": "1.0.1", + "@napi-rs/nice-linux-arm64-gnu": "1.0.1", + "@napi-rs/nice-linux-arm64-musl": "1.0.1", + "@napi-rs/nice-linux-ppc64-gnu": "1.0.1", + "@napi-rs/nice-linux-riscv64-gnu": "1.0.1", + "@napi-rs/nice-linux-s390x-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-gnu": "1.0.1", + "@napi-rs/nice-linux-x64-musl": "1.0.1", + "@napi-rs/nice-win32-arm64-msvc": "1.0.1", + "@napi-rs/nice-win32-ia32-msvc": "1.0.1", + "@napi-rs/nice-win32-x64-msvc": "1.0.1" + } + }, + "node_modules/@napi-rs/nice-android-arm-eabi": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm-eabi/-/nice-android-arm-eabi-1.0.1.tgz", + "integrity": "sha512-5qpvOu5IGwDo7MEKVqqyAxF90I6aLj4n07OzpARdgDRfz8UbBztTByBp0RC59r3J1Ij8uzYi6jI7r5Lws7nn6w==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-android-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-android-arm64/-/nice-android-arm64-1.0.1.tgz", + "integrity": "sha512-GqvXL0P8fZ+mQqG1g0o4AO9hJjQaeYG84FRfZaYjyJtZZZcMjXW5TwkL8Y8UApheJgyE13TQ4YNUssQaTgTyvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-arm64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-arm64/-/nice-darwin-arm64-1.0.1.tgz", + "integrity": "sha512-91k3HEqUl2fsrz/sKkuEkscj6EAj3/eZNCLqzD2AA0TtVbkQi8nqxZCZDMkfklULmxLkMxuUdKe7RvG/T6s2AA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-darwin-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-darwin-x64/-/nice-darwin-x64-1.0.1.tgz", + "integrity": "sha512-jXnMleYSIR/+TAN/p5u+NkCA7yidgswx5ftqzXdD5wgy/hNR92oerTXHc0jrlBisbd7DpzoaGY4cFD7Sm5GlgQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-freebsd-x64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-freebsd-x64/-/nice-freebsd-x64-1.0.1.tgz", + "integrity": "sha512-j+iJ/ezONXRQsVIB/FJfwjeQXX7A2tf3gEXs4WUGFrJjpe/z2KB7sOv6zpkm08PofF36C9S7wTNuzHZ/Iiccfw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm-gnueabihf": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm-gnueabihf/-/nice-linux-arm-gnueabihf-1.0.1.tgz", + "integrity": "sha512-G8RgJ8FYXYkkSGQwywAUh84m946UTn6l03/vmEXBYNJxQJcD+I3B3k5jmjFG/OPiU8DfvxutOP8bi+F89MCV7Q==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-gnu/-/nice-linux-arm64-gnu-1.0.1.tgz", + "integrity": "sha512-IMDak59/W5JSab1oZvmNbrms3mHqcreaCeClUjwlwDr0m3BoR09ZiN8cKFBzuSlXgRdZ4PNqCYNeGQv7YMTjuA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-arm64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-arm64-musl/-/nice-linux-arm64-musl-1.0.1.tgz", + "integrity": "sha512-wG8fa2VKuWM4CfjOjjRX9YLIbysSVV1S3Kgm2Fnc67ap/soHBeYZa6AGMeR5BJAylYRjnoVOzV19Cmkco3QEPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-ppc64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-ppc64-gnu/-/nice-linux-ppc64-gnu-1.0.1.tgz", + "integrity": "sha512-lxQ9WrBf0IlNTCA9oS2jg/iAjQyTI6JHzABV664LLrLA/SIdD+I1i3Mjf7TsnoUbgopBcCuDztVLfJ0q9ubf6Q==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-riscv64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-riscv64-gnu/-/nice-linux-riscv64-gnu-1.0.1.tgz", + "integrity": "sha512-3xs69dO8WSWBb13KBVex+yvxmUeEsdWexxibqskzoKaWx9AIqkMbWmE2npkazJoopPKX2ULKd8Fm9veEn0g4Ig==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-s390x-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-s390x-gnu/-/nice-linux-s390x-gnu-1.0.1.tgz", + "integrity": "sha512-lMFI3i9rlW7hgToyAzTaEybQYGbQHDrpRkg+1gJWEpH0PLAQoZ8jiY0IzakLfNWnVda1eTYYlxxFYzW8Rqczkg==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-gnu": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-gnu/-/nice-linux-x64-gnu-1.0.1.tgz", + "integrity": "sha512-XQAJs7DRN2GpLN6Fb+ZdGFeYZDdGl2Fn3TmFlqEL5JorgWKrQGRUrpGKbgZ25UeZPILuTKJ+OowG2avN8mThBA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-linux-x64-musl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-linux-x64-musl/-/nice-linux-x64-musl-1.0.1.tgz", + "integrity": "sha512-/rodHpRSgiI9o1faq9SZOp/o2QkKQg7T+DK0R5AkbnI/YxvAIEHf2cngjYzLMQSQgUhxym+LFr+UGZx4vK4QdQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-arm64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-arm64-msvc/-/nice-win32-arm64-msvc-1.0.1.tgz", + "integrity": "sha512-rEcz9vZymaCB3OqEXoHnp9YViLct8ugF+6uO5McifTedjq4QMQs3DHz35xBEGhH3gJWEsXMUbzazkz5KNM5YUg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-ia32-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-ia32-msvc/-/nice-win32-ia32-msvc-1.0.1.tgz", + "integrity": "sha512-t7eBAyPUrWL8su3gDxw9xxxqNwZzAqKo0Szv3IjVQd1GpXXVkb6vBBQUuxfIYaXMzZLwlxRQ7uzM2vdUE9ULGw==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@napi-rs/nice-win32-x64-msvc": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@napi-rs/nice-win32-x64-msvc/-/nice-win32-x64-msvc-1.0.1.tgz", + "integrity": "sha512-JlF+uDcatt3St2ntBG8H02F1mM45i5SF9W+bIKiReVE6wiy3o16oBP/yxt+RZ+N6LbCImJXJ6bXNO2kn9AXicg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@ngneat/transloco": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco/-/transloco-4.3.0.tgz", + "integrity": "sha512-KUhGvp1ki+jvrM2PO27Tgzme1HkFmvDgS+7VyGxHta35wZEyoH6/r/EAXvfurPeYgaP6IaEMhUvAVT1WDgYwUg==", + "deprecated": "NOTICE: Transloco has moved to a new scope, this package will no longer receive updates. please use @jsverse/transloco instead.", + "dependencies": { + "@ngneat/transloco-utils": "3.0.5", + "flat": "5.0.2", + "lodash.kebabcase": "^4.1.1", + "ora": "^5.4.1", + "replace-in-file": "^6.2.0", + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "fs-extra": ">=9.1.0", + "glob": ">=7.1.7", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@ngneat/transloco-messageformat": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-messageformat/-/transloco-messageformat-4.1.0.tgz", + "integrity": "sha512-4Q9tMri97YVB46L6l0M2NJNycP5bplJLbSlm9ySIkm2M5m+iIfpJo5CCJh6iI0V2GplhMIle0SsfRlaFatazmQ==", + "deprecated": "NOTICE: Transloco has moved to a new scope, this package will no longer receive updates. please use @jsverse/transloco-messageformat instead.", + "dependencies": { + "@messageformat/core": "^3.0.0", + "tslib": "^2.2.0" + }, + "peerDependencies": { + "@angular/core": ">=13.0.0", + "@ngneat/transloco": ">=4.2.0", + "rxjs": ">=6.0.0" + } + }, + "node_modules/@ngneat/transloco-utils": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@ngneat/transloco-utils/-/transloco-utils-3.0.5.tgz", + "integrity": "sha512-Xn9GaLUocXSPMhErNHbUyoloDm9sb+JaYszZJFL9F8em6frPQDSJxcYk9pV0caWpAU8INlksJSYgx1LXAH18mw==", + "deprecated": "NOTICE: Transloco has moved to a new scope, this package will no longer receive updates. please use @jsverse/transloco-utils instead.", + "dependencies": { + "cosmiconfig": "^8.1.3", + "tslib": "^2.3.0" + } + }, + "node_modules/@ngtools/webpack": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.0.7.tgz", + "integrity": "sha512-jWyMuqtLKZB8Jnuqo27mG2cCQdl71lhM1oEdq3x7Z/QOrm2I+8EfyAzOLxB1f1vXt85O1bz3nf66CkuVCVGGTQ==", + "dev": true, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0", + "typescript": ">=5.5 <5.7", + "webpack": "^5.54.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@npmcli/agent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", + "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/fs": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", + "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", + "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "dev": true, + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-inflight": "^1.0.1", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/git/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/git/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/git/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", + "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", + "dev": true, + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", + "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.0.tgz", + "integrity": "sha512-t6G+6ZInT4X+tqj2i+wlLIeCKnKOTuz9/VFYDtj+TGTur5q7sp/OYrQA19LdBbWfXDOi0Y4jtedV6xtB8zQ9ug==", + "dev": true, + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "normalize-package-data": "^7.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/package-json/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/@npmcli/package-json/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/package-json/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", + "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", + "dev": true, + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/promise-spawn/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/redact": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.0.0.tgz", + "integrity": "sha512-/1uFzjVcfzqrgCeGW7+SZ4hv0qLWmKXVzFahZGJ6QuJBj6Myt9s17+JL86i76NV9YSnJRcGXJYQbAU0rn1YTCQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.0.2.tgz", + "integrity": "sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw==", + "dev": true, + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@npmcli/run-script/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/@npmcli/run-script/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@parcel/source-map": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@parcel/source-map/-/source-map-2.1.1.tgz", + "integrity": "sha512-Ejx1P/mj+kMjQb8/y5XxDUn4reGdr+WyKYloBljpppUy8gs42T+BNoEOuRYqDVdgPc6NxduzIDoJS9pOFfV5Ew==", + "license": "MIT", + "dependencies": { + "detect-libc": "^1.0.3" + }, + "engines": { + "node": "^12.18.3 || >=14" + } + }, + "node_modules/@parcel/source-map/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "license": "Apache-2.0", + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/@parcel/watcher/node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/@parcel/watcher/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "optional": true + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rollup/plugin-json": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-json/-/plugin-json-6.1.0.tgz", + "integrity": "sha512-EGI2te5ENk1coGeADSIwZ7G2Q8CJS2sF120T7jLw4xFw9n7wIOXHo+kIYRAoVpJAN+kmqZSoO3Fp4JtoNF4ReA==", + "dev": true, + "dependencies": { + "@rollup/pluginutils": "^5.1.0" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.1.4.tgz", + "integrity": "sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ==", + "dev": true, + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.26.0.tgz", + "integrity": "sha512-gJNwtPDGEaOEgejbaseY6xMFu+CPltsc8/T+diUTTbOQLqD+bnrJq9ulH6WD69TqwqWmrfRAtUv30cCFZlbGTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.26.0.tgz", + "integrity": "sha512-YJa5Gy8mEZgz5JquFruhJODMq3lTHWLm1fOy+HIANquLzfIOzE9RA5ie3JjCdVb9r46qfAQY/l947V0zfGJ0OQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.26.0.tgz", + "integrity": "sha512-ErTASs8YKbqTBoPLp/kA1B1Um5YSom8QAc4rKhg7b9tyyVqDBlQxy7Bf2wW7yIlPGPg2UODDQcbkTlruPzDosw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.26.0.tgz", + "integrity": "sha512-wbgkYDHcdWW+NqP2mnf2NOuEbOLzDblalrOWcPyY6+BRbVhliavon15UploG7PpBRQ2bZJnbmh8o3yLoBvDIHA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.26.0.tgz", + "integrity": "sha512-Y9vpjfp9CDkAG4q/uwuhZk96LP11fBz/bYdyg9oaHYhtGZp7NrbkQrj/66DYMMP2Yo/QPAsVHkV891KyO52fhg==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.26.0.tgz", + "integrity": "sha512-A/jvfCZ55EYPsqeaAt/yDAG4q5tt1ZboWMHEvKAH9Zl92DWvMIbnZe/f/eOXze65aJaaKbL+YeM0Hz4kLQvdwg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.26.0.tgz", + "integrity": "sha512-paHF1bMXKDuizaMODm2bBTjRiHxESWiIyIdMugKeLnjuS1TCS54MF5+Y5Dx8Ui/1RBPVRE09i5OUlaLnv8OGnA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.26.0.tgz", + "integrity": "sha512-cwxiHZU1GAs+TMxvgPfUDtVZjdBdTsQwVnNlzRXC5QzIJ6nhfB4I1ahKoe9yPmoaA/Vhf7m9dB1chGPpDRdGXg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.26.0.tgz", + "integrity": "sha512-4daeEUQutGRCW/9zEo8JtdAgtJ1q2g5oHaoQaZbMSKaIWKDQwQ3Yx0/3jJNmpzrsScIPtx/V+1AfibLisb3AMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.26.0.tgz", + "integrity": "sha512-eGkX7zzkNxvvS05ROzJ/cO/AKqNvR/7t1jA3VZDi2vRniLKwAWxUr85fH3NsvtxU5vnUUKFHKh8flIBdlo2b3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.26.0.tgz", + "integrity": "sha512-Odp/lgHbW/mAqw/pU21goo5ruWsytP7/HCC/liOt0zcGG0llYWKrd10k9Fj0pdj3prQ63N5yQLCLiE7HTX+MYw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.26.0.tgz", + "integrity": "sha512-MBR2ZhCTzUgVD0OJdTzNeF4+zsVogIR1U/FsyuFerwcqjZGvg2nYe24SAHp8O5sN8ZkRVbHwlYeHqcSQ8tcYew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.26.0.tgz", + "integrity": "sha512-YYcg8MkbN17fMbRMZuxwmxWqsmQufh3ZJFxFGoHjrE7bv0X+T6l3glcdzd7IKLiwhT+PZOJCblpnNlz1/C3kGQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.26.0.tgz", + "integrity": "sha512-ZuwpfjCwjPkAOxpjAEjabg6LRSfL7cAJb6gSQGZYjGhadlzKKywDkCUnJ+KEfrNY1jH5EEoSIKLCb572jSiglA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.26.0.tgz", + "integrity": "sha512-+HJD2lFS86qkeF8kNu0kALtifMpPCZU80HvwztIKnYwym3KnA1os6nsX4BGSTLtS2QVAGG1P3guRgsYyMA0Yhg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.26.0.tgz", + "integrity": "sha512-WUQzVFWPSw2uJzX4j6YEbMAiLbs0BUysgysh8s817doAYhR5ybqTI1wtKARQKo6cGop3pHnrUJPFCsXdoFaimQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.26.0.tgz", + "integrity": "sha512-D4CxkazFKBfN1akAIY6ieyOqzoOoBV1OICxgUblWxff/pSjCA2khXlASUx7mK6W1oP4McqhgcCsu6QaLj3WMWg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.26.0.tgz", + "integrity": "sha512-2x8MO1rm4PGEP0xWbubJW5RtbNLk3puzAMaLQd3B3JHVw4KcHlmXcO+Wewx9zCoo7EUFiMlu/aZbCJ7VjMzAag==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/wasm-node": { + "version": "4.30.1", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.30.1.tgz", + "integrity": "sha512-xiaHQoTkjKS60ouuSzJGPgjMp5R6TGGShLFQIhsmD8w06z7pfmJDiZPF0G953P2ruU5LSSsVI+UWmvtlzSFInA==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@rspack/binding": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding/-/binding-1.1.8.tgz", + "integrity": "sha512-+/JzXx1HctfgPj+XtsCTbRkxiaOfAXGZZLEvs7jgp04WgWRSZ5u97WRCePNPvy+sCfOEH/2zw2ZK36Z7oQRGhQ==", + "dev": true, + "optional": true, + "peer": true, + "optionalDependencies": { + "@rspack/binding-darwin-arm64": "1.1.8", + "@rspack/binding-darwin-x64": "1.1.8", + "@rspack/binding-linux-arm64-gnu": "1.1.8", + "@rspack/binding-linux-arm64-musl": "1.1.8", + "@rspack/binding-linux-x64-gnu": "1.1.8", + "@rspack/binding-linux-x64-musl": "1.1.8", + "@rspack/binding-win32-arm64-msvc": "1.1.8", + "@rspack/binding-win32-ia32-msvc": "1.1.8", + "@rspack/binding-win32-x64-msvc": "1.1.8" + } + }, + "node_modules/@rspack/binding-darwin-arm64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-arm64/-/binding-darwin-arm64-1.1.8.tgz", + "integrity": "sha512-I7avr471ghQ3LAqKm2fuXuJPLgQ9gffn5Q4nHi8rsukuZUtiLDPfYzK1QuupEp2JXRWM1gG5lIbSUOht3cD6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-darwin-x64": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-darwin-x64/-/binding-darwin-x64-1.1.8.tgz", + "integrity": "sha512-vfqf/c+mcx8rr1M8LnqKmzDdnrgguflZnjGerBLjNerAc+dcUp3lCvNxRIvZ2TkSZZBW8BpCMgjj3n70CZ4VLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.8.tgz", + "integrity": "sha512-lZlO/rAJSeozi+qtVLkGSXfe+riPawCwM4FsrflELfNlvvEXpANwtrdJ+LsaNVXcgvhh50ZX2KicTdmx9G2b6Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-arm64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.8.tgz", + "integrity": "sha512-bX7exULSZwy8xtDh6Z65b6sRC4uSxGuyvSLCEKyhmG6AnJkg0gQMxk3hoO0hWnyGEZgdJEn+jEhk0fjl+6ZRAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-gnu": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.8.tgz", + "integrity": "sha512-2Prw2USgTJ3aLdLExfik8pAwAHbX4MZrACBGEmR7Vbb56kLjC+++fXkciRc50pUDK4JFr1VQ7eNZrJuDR6GG6Q==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-linux-x64-musl": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.8.tgz", + "integrity": "sha512-bnVGB/mQBKEdzOU/CPmcOE3qEXxGOGGW7/i6iLl2MamVOykJq8fYjL9j86yi6L0r009ja16OgWckykQGc4UqGw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-arm64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.8.tgz", + "integrity": "sha512-u+na3gxhzeksm4xZyAzn1+XWo5a5j7hgWA/KcFPDQ8qQNkRknx4jnQMxVtcZ9pLskAYV4AcOV/AIximx7zvv8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-ia32-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-ia32-msvc/-/binding-win32-ia32-msvc-1.1.8.tgz", + "integrity": "sha512-FijUxym1INd5fFHwVCLuVP8XEAb4Sk1sMwEEQUlugiDra9ZsLaPw4OgPGxbxkD6SB0DeUz9Zq46Xbcf6d3OgfA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/binding-win32-x64-msvc": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.8.tgz", + "integrity": "sha512-SBzIcND4qpDt71jlu1MCDxt335tqInT3YID9V4DoQ4t8wgM/uad7EgKOWKTK6vc2RRaOIShfS2XzqjNUxPXh4w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "peer": true + }, + "node_modules/@rspack/core": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@rspack/core/-/core-1.1.8.tgz", + "integrity": "sha512-pcZtcj5iXLCuw9oElTYC47bp/RQADm/MMEb3djHdwJuSlFWfWPQi5QFgJ/lJAxIW9UNHnTFrYtytycfjpuoEcA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime-tools": "0.5.1", + "@rspack/binding": "1.1.8", + "@rspack/lite-tapable": "1.0.1", + "caniuse-lite": "^1.0.30001616" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "@swc/helpers": ">=0.5.1" + }, + "peerDependenciesMeta": { + "@swc/helpers": { + "optional": true + } + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime/-/runtime-0.5.1.tgz", + "integrity": "sha512-xgiMUWwGLWDrvZc9JibuEbXIbhXg6z2oUkemogSvQ4LKvrl/n0kbqP1Blk669mXzyWbqtSp6PpvNdwaE1aN5xQ==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/sdk": "0.5.1" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/runtime-tools": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/runtime-tools/-/runtime-tools-0.5.1.tgz", + "integrity": "sha512-nfBedkoZ3/SWyO0hnmaxuz0R0iGPSikHZOAZ0N/dVSQaIzlffUo35B5nlC2wgWIc0JdMZfkwkjZRrnuuDIJbzg==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.5.1", + "@module-federation/webpack-bundler-runtime": "0.5.1" + } + }, + "node_modules/@rspack/core/node_modules/@module-federation/sdk": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/sdk/-/sdk-0.5.1.tgz", + "integrity": "sha512-exvchtjNURJJkpqjQ3/opdbfeT2wPKvrbnGnyRkrwW5o3FH1LaST1tkiNviT6OXTexGaVc2DahbdniQHVtQ7pA==", + "dev": true, + "optional": true, + "peer": true + }, + "node_modules/@rspack/core/node_modules/@module-federation/webpack-bundler-runtime": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@module-federation/webpack-bundler-runtime/-/webpack-bundler-runtime-0.5.1.tgz", + "integrity": "sha512-mMhRFH0k2VjwHt3Jol9JkUsmI/4XlrAoBG3E0o7HoyoPYv1UFOWyqAflfANcUPgbYpvqmyLzDcO+3IT36LXnrA==", + "dev": true, + "optional": true, + "peer": true, + "dependencies": { + "@module-federation/runtime": "0.5.1", + "@module-federation/sdk": "0.5.1" + } + }, + "node_modules/@rspack/lite-tapable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rspack/lite-tapable/-/lite-tapable-1.0.1.tgz", + "integrity": "sha512-VynGOEsVw2s8TAlLf/uESfrgfrq2+rcXB1muPJYBWbsm1Oa6r5qVQhjA5ggM6z/coYPrsVMgovl3Ff7Q7OCp1w==", + "dev": true, + "optional": true, + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@schematics/angular": { + "version": "19.0.7", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.0.7.tgz", + "integrity": "sha512-1WtTqKFPuEaV99VIP+y/gf/XW3TVJh/NbJbbEF4qYpp7qQiJ4ntF4klVZmsJcQzFucZSzlg91QVMPQKev5WZGA==", + "dev": true, + "dependencies": { + "@angular-devkit/core": "19.0.7", + "@angular-devkit/schematics": "19.0.7", + "jsonc-parser": "3.3.1" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + } + }, + "node_modules/@sigstore/bundle": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", + "integrity": "sha512-XDUYX56iMPAn/cdgh/DTJxz5RWmqKV4pwvUAEKEWJl+HzKdCd/24wUa9JYNMlDSCb7SUHAdtksxYX779Nne/Zg==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", + "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/protobuf-specs": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", + "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/sign": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.0.0.tgz", + "integrity": "sha512-UjhDMQOkyDoktpXoc5YPJpJK6IooF2gayAr5LvXI4EL7O0vd58okgfRcxuaH+YTdhvb5aa1Q9f+WJ0c2sVuYIw==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "make-fetch-happen": "^14.0.1", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/tuf": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.0.0.tgz", + "integrity": "sha512-9Xxy/8U5OFJu7s+OsHzI96IX/OzjF/zj0BSSaWhgJgTqtlBhQIV2xdrQI5qxLD7+CWWDepadnXAxzaZ3u9cvRw==", + "dev": true, + "dependencies": { + "@sigstore/protobuf-specs": "^0.3.2", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@sigstore/verify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.0.0.tgz", + "integrity": "sha512-Ggtq2GsJuxFNUvQzLoXqRwS4ceRfLAJnrIHUDrzAD0GgnOhwujJkKkxM/s5Bako07c3WtAs/sZo5PJq7VHjeDg==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@simonwep/pickr": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@simonwep/pickr/-/pickr-1.9.1.tgz", + "integrity": "sha512-fR3qmfAcPf/HSFS7GEnTmZLM3+xERv1+jyMBbzT63ilRRM8veYjI7ELvkHHKk0/du3lHp7uh/FqatjM3646X1g==", + "dependencies": { + "core-js": "3.37.0", + "nanopop": "2.4.2" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", + "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" + }, + "node_modules/@softarc/native-federation": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation/-/native-federation-2.0.18.tgz", + "integrity": "sha512-2QhZZjm3YGTKi7VHlYC4GyBwx+fuc+SM2djl5CAionap/WuBzsucxOmo3ECbLS6tfoc0kGeYSVMX1EcwWa+F5A==", + "license": "MIT", + "dependencies": { + "@softarc/native-federation-runtime": "2.0.18", + "json5": "^2.2.0", + "npmlog": "^6.0.2" + } + }, + "node_modules/@softarc/native-federation-node": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-node/-/native-federation-node-2.0.18.tgz", + "integrity": "sha512-2bV/oB2BpGibs62/PmNxH6VIngsAWdZGLUl4oLjzrwgxmEw+rvIW9EhJ+S6zZz0bmz/YZCFxEowBETpP779fxQ==" + }, + "node_modules/@softarc/native-federation-runtime": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/@softarc/native-federation-runtime/-/native-federation-runtime-2.0.18.tgz", + "integrity": "sha512-kCfegbei6FVbGkCZfWe5IrNuRcNZGU517HeRwaoCtXvk2/2zudGRD9ftWmhDAV1N85N3tvXEsO65YFSI7Ccr5g==", + "dependencies": { + "tslib": "^2.3.0" + } + }, + "node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", + "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", + "dev": true, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@tufjs/models": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", + "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", + "dev": true, + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", + "dev": true, + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/browser-sync": { + "version": "2.29.0", + "resolved": "https://registry.npmjs.org/@types/browser-sync/-/browser-sync-2.29.0.tgz", + "integrity": "sha512-d2V8FDX/LbDCSm343N2VChzDxvll0h76I8oSigYpdLgPDmcdcR6fywTggKBkUiDM3qAbHOq7NZvepj/HJM5e2g==", + "license": "MIT", + "dependencies": { + "@types/micromatch": "^2", + "@types/node": "*", + "@types/serve-static": "*", + "chokidar": "^3.0.0" + } + }, + "node_modules/@types/browser-sync/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/@types/browser-sync/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@types/browser-sync/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@types/browser-sync/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", + "dev": true, + "dependencies": { + "@types/express-serve-static-core": "*", + "@types/node": "*" + } + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-P2dlU/q51fkOc/Gfl3Ul9kicV7l+ra934qBFXCFhrZMOL6du1TM0pm1ThYvENukyOn5h9v+yMJ9Fn5JK4QozrQ==", + "dev": true + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dev": true, + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", + "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==", + "dev": true + }, + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.5.tgz", + "integrity": "sha512-GLZPrd9ckqEBFMcVM/qRFAP0Hg3qiVEojgEFsx/N/zKXsBzbGF6z5FBDpZ0+Xhp1xr+qRZYjfGr1cWHB9oFHSA==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/express/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/file-saver": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/file-saver/-/file-saver-2.0.7.tgz", + "integrity": "sha512-dNKVfHd/jk0SkR/exKGj2ggkB45MAkzvWCaqLUUgkyjITkGNzH8H+yUwr+BLJUBjZOe9w8X3wgmXhZDRg1ED6A==", + "dev": true + }, + "node_modules/@types/guacamole-common-js": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@types/guacamole-common-js/-/guacamole-common-js-1.5.3.tgz", + "integrity": "sha512-PDW2kRwwIgzw0ys82X65g13+OHRPW4Ek/919vIoacWGEUU8jGGfULmH+6TuufLDGMO0cqXR03nxer8ceRDmy3g==", + "dev": true + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" + }, + "node_modules/@types/http-proxy": { + "version": "1.17.15", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.15.tgz", + "integrity": "sha512-25g5atgiVNTIv0LBDTg1H74Hvayx0ajtJPLLcYE3whFv75J0pWNtOBzaXJQgDTmrX1bx5U9YC2w/n65BN1HwRQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/jasmine": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/@types/jasmine/-/jasmine-5.1.5.tgz", + "integrity": "sha512-SaCZ3kM5NjOiJqMRYwHpLbTfUC2Dyk1KS3QanNFsUYPGTk70CWVK/J9ueun6zNhw/UkgV7xl8V4ZLQZNRbfnNw==", + "dev": true + }, + "node_modules/@types/jquery": { + "version": "3.5.32", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.32.tgz", + "integrity": "sha512-b9Xbf4CkMqS02YH8zACqN1xzdxc3cO735Qe5AbSUFmyOiaWAbcpqh9Wna+Uk0vgACvoQHpWDg2rGdHkYPLmCiQ==", + "dev": true, + "dependencies": { + "@types/sizzle": "*" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true + }, + "node_modules/@types/lodash": { + "version": "4.17.14", + "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.14.tgz", + "integrity": "sha512-jsxagdikDiDBeIRaPYtArcT8my4tN1og7MtMRquFT3XNA6axxyHDRUemqDz/taRDdOUn0GnGHRCuff4q48sW9A==", + "dev": true + }, + "node_modules/@types/micromatch": { + "version": "2.3.35", + "resolved": "https://registry.npmjs.org/@types/micromatch/-/micromatch-2.3.35.tgz", + "integrity": "sha512-J749bHo/Zu56w0G0NI/IGHLQPiSsjx//0zJhfEVAN95K/xM5C8ZDmhkXtU3qns0sBOao7HuQzr8XV1/2o5LbXA==", + "license": "MIT", + "dependencies": { + "@types/parse-glob": "*" + } + }, + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" + }, + "node_modules/@types/node": { + "version": "22.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.6.tgz", + "integrity": "sha512-qNiuwC4ZDAUNcY47xgaSuS92cjf8JbSUoaKS77bmLG1rU7MlATVSiw/IlrjtIyyskXBZ8KkNfjK/P5na7rgXbQ==", + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/parse-glob": { + "version": "3.0.32", + "resolved": "https://registry.npmjs.org/@types/parse-glob/-/parse-glob-3.0.32.tgz", + "integrity": "sha512-n4xmml2WKR12XeQprN8L/sfiVPa8FHS3k+fxp4kSr/PA2GsGUgFND+bvISJxM0y5QdvzNEGjEVU3eIrcKks/pA==", + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.9.18", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.18.tgz", + "integrity": "sha512-kK7dgTYDyGqS+e2Q4aK9X3D7q234CIZ1Bv0q/7Z5IwRDoADNU81xXJK/YVyLbLTZCoIwUoDoffFeF+p/eIklAA==", + "dev": true + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "dev": true + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", + "dependencies": { + "@types/mime": "^1", + "@types/node": "*" + } + }, + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", + "dev": true, + "dependencies": { + "@types/express": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "1.15.7", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", + "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*", + "@types/send": "*" + } + }, + "node_modules/@types/sizzle": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.9.tgz", + "integrity": "sha512-xzLEyKB50yqCUPUJkIsrVvoWNfFUbIZI+RspLWt8u+tIW/BetMBZtgV2LY/2o+tYH8dRvQ+eoPf3NdhQCcLE2w==", + "dev": true + }, + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.13", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.13.tgz", + "integrity": "sha512-osM/gWBTPKgHV8XkTunnegTRIsvF6owmf5w+JtAfOw472dptdm0dlGv4xCt6GwQRcC2XVOvvRE/0bAoQcL2QkA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.0.tgz", + "integrity": "sha512-NR2yS7qUqCL7AIxdJUQf2MKKNDVNaig/dEB0GBLU7D+ZdHgK1NoH/3wsgO3OnPVipn51tG3MAwaODEGil70WEw==", + "dev": true, + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/type-utils": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "graphemer": "^1.4.0", + "ignore": "^5.3.1", + "natural-compare": "^1.4.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.0.tgz", + "integrity": "sha512-hgUZ3kTEpVzKaK3uNibExUYm6SKKOmTU2BOxBSvOYwtJEPdVQ70kZJpPjstlnhCHcuc2WGfSbpKlb/69ttyN5Q==", + "dev": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/parser/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.20.0.tgz", + "integrity": "sha512-J7+VkpeGzhOt3FeG1+SzhiMj9NzGD/M6KoGn9f4dbz3YzK9hvbhVTmLj/HiTp9DazIzJ8B4XcM80LrR9Dm1rJw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.0.tgz", + "integrity": "sha512-er224jRepVAVLnMF2Q7MZJCq5CsdH2oqjP4dT7K6ij09Kyd+R21r7UVJrF0buMVdZS5QRhDzpvzAxHxabQadow==", + "dev": true, + "dependencies": { + "@typescript-eslint/typescript-estree": "8.18.0", + "@typescript-eslint/utils": "8.18.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@typescript-eslint/type-utils/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.20.0.tgz", + "integrity": "sha512-cqaMiY72CkP+2xZRrFt3ExRBu0WmVitN/rYPZErA80mHjHx/Svgp8yfbzkJmDoQ/whcytOPO9/IZXnOc+wigRA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.20.0.tgz", + "integrity": "sha512-Y7ncuy78bJqHI35NwzWol8E0X7XkRVS4K4P4TCyzWkOJih5NDvtoRDW4Ba9YJJoB2igm9yXDdYI/+fkiiAxPzA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/visitor-keys": "8.20.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.20.0.tgz", + "integrity": "sha512-dq70RUw6UK9ei7vxc4KQtBRk7qkHZv447OUZ6RPQMQl71I3NZxQJX/f32Smr+iqWrB02pHKn2yAdHBb0KNrRMA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.20.0", + "@typescript-eslint/types": "8.20.0", + "@typescript-eslint/typescript-estree": "8.20.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.20.0.tgz", + "integrity": "sha512-v/BpkeeYAsPkKCkR8BDwcno0llhzWVqPOamQrAEMdpZav2Y9OVjd9dwJyBLJWwf335B5DmlifECIkZRJCaGaHA==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.20.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitejs/plugin-basic-ssl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.1.0.tgz", + "integrity": "sha512-wO4Dk/rm8u7RNhOf95ZzcEmC9rYOncYgvq4z3duaJrCgjN8BxAnDVyndanfcJZ0O6XZzHz6Q0hTimxTg8Y9g/A==", + "dev": true, + "engines": { + "node": ">=14.6.0" + }, + "peerDependencies": { + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0" + } + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true + }, + "node_modules/@yarnpkg/lockfile": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@yarnpkg/lockfile/-/lockfile-1.1.0.tgz", + "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", + "dev": true + }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/acorn": { + "version": "8.14.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.0.tgz", + "integrity": "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==", + "dev": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/adjust-sourcemap-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/adjust-sourcemap-loader/-/adjust-sourcemap-loader-4.0.0.tgz", + "integrity": "sha512-OXwN5b9pCUXNQHJpwwD2qP40byEmSgzj8B4ydSN0uMNYWiFmJ6x6KwUllMmfk8Rwu/HJDFR7U8ubsWBoN0Xp0A==", + "dev": true, + "dependencies": { + "loader-utils": "^2.0.0", + "regex-parser": "^2.2.11" + }, + "engines": { + "node": ">=8.9" + } + }, + "node_modules/adjust-sourcemap-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "dev": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/angular-eslint": { + "version": "19.0.2", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-19.0.2.tgz", + "integrity": "sha512-d8P/Y5+QXOOko1x5W3Pp/p4cr7arXKGHdMAv6jtrqHjsIrlBqZSZY18apKRdTysFjYuKa5G9M3hejtzwXXHNhg==", + "dev": true, + "dependencies": { + "@angular-devkit/core": ">= 19.0.0 < 20.0.0", + "@angular-devkit/schematics": ">= 19.0.0 < 20.0.0", + "@angular-eslint/builder": "19.0.2", + "@angular-eslint/eslint-plugin": "19.0.2", + "@angular-eslint/eslint-plugin-template": "19.0.2", + "@angular-eslint/schematics": "19.0.2", + "@angular-eslint/template-parser": "19.0.2", + "@typescript-eslint/types": "^8.0.0", + "@typescript-eslint/utils": "^8.0.0" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": "*", + "typescript-eslint": "^8.0.0" + } + }, + "node_modules/angular-expressions": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/angular-expressions/-/angular-expressions-1.4.3.tgz", + "integrity": "sha512-r7j+dqOuHy0OYiR5AazDixU/Us3TDN2FfuxGX4Dq6d61Y2MhBQHMdUNBfkkLPjDqVm2Is394h31gC3bcBwy9zw==" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", + "dev": true, + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/anymatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC" + }, + "node_modules/are-we-there-yet": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-3.0.1.tgz", + "integrity": "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + }, + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", + "dev": true + }, + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/async-each-series": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/async-each-series/-/async-each-series-0.1.1.tgz", + "integrity": "sha512-p4jj6Fws4Iy2m0iCmI2am2ZNZCgbdgE+P8F/8csmn2vx7ixXrO2zGcuNsD46X5uZSVecmkEy/M06X2vG8KD6dQ==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/autoprefixer": { + "version": "10.4.20", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", + "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "browserslist": "^4.23.3", + "caniuse-lite": "^1.0.30001646", + "fraction.js": "^4.3.7", + "normalize-range": "^0.1.2", + "picocolors": "^1.0.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, + "dependencies": { + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" + } + }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==" + }, + "node_modules/beasties": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.1.0.tgz", + "integrity": "sha512-+Ssscd2gVG24qRNC+E2g88D+xsQW4xwakWtKAiGEQ3Pw54/FGdyo9RrfxhGhEv6ilFVbB7r3Lgx+QnAxnSpECw==", + "dev": true, + "dependencies": { + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "htmlparser2": "^9.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.4.47", + "postcss-media-query-parser": "^0.2.3" + } + }, + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/bonjour-service": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", + "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-sync": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync/-/browser-sync-3.0.3.tgz", + "integrity": "sha512-91hoBHKk1C4pGeD+oE9Ld222k2GNQEAsI5AElqR8iLLWNrmZR2LPP8B0h8dpld9u7kro5IEUB3pUb0DJ3n1cRQ==", + "license": "Apache-2.0", + "dependencies": { + "browser-sync-client": "^3.0.3", + "browser-sync-ui": "^3.0.3", + "bs-recipes": "1.3.4", + "chalk": "4.1.2", + "chokidar": "^3.5.1", + "connect": "3.6.6", + "connect-history-api-fallback": "^1", + "dev-ip": "^1.0.1", + "easy-extender": "^2.3.4", + "eazy-logger": "^4.0.1", + "etag": "^1.8.1", + "fresh": "^0.5.2", + "fs-extra": "3.0.1", + "http-proxy": "^1.18.1", + "immutable": "^3", + "micromatch": "^4.0.8", + "opn": "5.3.0", + "portscanner": "2.2.0", + "raw-body": "^2.3.2", + "resp-modifier": "6.0.2", + "rx": "4.1.0", + "send": "^0.19.0", + "serve-index": "^1.9.1", + "serve-static": "^1.16.2", + "server-destroy": "1.0.1", + "socket.io": "^4.4.1", + "ua-parser-js": "^1.0.33", + "yargs": "^17.3.1" + }, + "bin": { + "browser-sync": "dist/bin.js" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/browser-sync-client": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-client/-/browser-sync-client-3.0.3.tgz", + "integrity": "sha512-TOEXaMgYNjBYIcmX5zDlOdjEqCeCN/d7opf/fuyUD/hhGVCfP54iQIDhENCi012AqzYZm3BvuFl57vbwSTwkSQ==", + "license": "ISC", + "dependencies": { + "etag": "1.8.1", + "fresh": "0.5.2", + "mitt": "^1.1.3" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/browser-sync-ui": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/browser-sync-ui/-/browser-sync-ui-3.0.3.tgz", + "integrity": "sha512-FcGWo5lP5VodPY6O/f4pXQy5FFh4JK0f2/fTBsp0Lx1NtyBWs/IfPPJbW8m1ujTW/2r07oUXKTF2LYZlCZktjw==", + "license": "Apache-2.0", + "dependencies": { + "async-each-series": "0.1.1", + "chalk": "4.1.2", + "connect-history-api-fallback": "^1", + "immutable": "^3", + "server-destroy": "1.0.1", + "socket.io-client": "^4.4.1", + "stream-throttle": "^0.1.3" + } + }, + "node_modules/browser-sync-ui/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync-ui/node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/browser-sync-ui/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browser-sync/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/browser-sync/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/browser-sync/node_modules/connect": { + "version": "3.6.6", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.6.6.tgz", + "integrity": "sha512-OO7axMmPpu/2XuX1+2Yrg0ddju31B6xLZMWkJ5rYBu4YRmRVlOjvlY6kw2FJKiAzyxGwnrDUAG4s1Pf0sbBMCQ==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.0", + "parseurl": "~1.3.2", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/browser-sync/node_modules/connect-history-api-fallback": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", + "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/browser-sync/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/browser-sync/node_modules/finalhandler": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.0.tgz", + "integrity": "sha512-ejnvM9ZXYzp6PUPUyQBMBf0Co5VX2gr5H2VQe2Ui2jWXNlxv+PYZo8wpAymJNJdLsG1R4p+M4aynF8KuoUEwRw==", + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.1", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.2", + "statuses": "~1.3.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/browser-sync/node_modules/fs-extra": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-3.0.1.tgz", + "integrity": "sha512-V3Z3WZWVUYd8hoCL5xfXJCaHWYzmtwW5XWYSlLgERi8PWd8bx1kUHUk8L1BT57e49oKnDDD180mjfrHc1yA9rg==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "jsonfile": "^3.0.0", + "universalify": "^0.1.0" + } + }, + "node_modules/browser-sync/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/browser-sync/node_modules/immutable": { + "version": "3.8.2", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-3.8.2.tgz", + "integrity": "sha512-15gZoQ38eYjEjxkorfbcgBKBL6R7T459OuK+CpcWt7O3KF4uPCx2tD0uFETlUDIyo+1789crbMhTvQBSR5yBMg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/browser-sync/node_modules/jsonfile": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-3.0.1.tgz", + "integrity": "sha512-oBko6ZHlubVB5mRFkur5vgYR1UyqX+S6Y/oCfLhqNdcc2fYFlDpIoNc7AfKS1KOGcnNAkvsr0grLck9ANM815w==", + "license": "MIT", + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/browser-sync/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/browser-sync/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/browser-sync/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/browser-sync/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "license": "MIT", + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/browser-sync/node_modules/statuses": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.3.1.tgz", + "integrity": "sha512-wuTCPGlJONk/a1kqZ4fQM2+908lC7fa7nPYpTC1EhnvqLX/IICbeP1OZGDtA374trpSq68YubKUMo8oRhN46yg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/browser-sync/node_modules/ua-parser-js": { + "version": "1.0.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.40.tgz", + "integrity": "sha512-z6PJ8Lml+v3ichVojCiB8toQJBuwR42ySM4ezjXIqXK3M0HczmKQ3LF4rhU55PfD99KEEXQG6yb7iOMyvYuHew==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/browser-sync/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.24.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", + "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.1" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-recipes": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/bs-recipes/-/bs-recipes-1.3.4.tgz", + "integrity": "sha512-BXvDkqhDNxXEjeGM8LFkSbR+jzmP/CYpCiVKYn+soB1dDldeU15EBNDkwVXndKuX35wnNUaPd0qSoQEAkmQtMw==", + "license": "ISC" + }, + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true + }, + "node_modules/bundle-name": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", + "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", + "dev": true, + "dependencies": { + "run-applescript": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cacache": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", + "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001692", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001692.tgz", + "integrity": "sha512-A95VKan0kdtrsnMubMKxEKUKImOPSuCpYgxSQBo036P5YYgVIcOYJEgt/txJWqObiRQeISNCfef9nvlQ0vbV7A==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/chardet": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.7.0.tgz", + "integrity": "sha512-mT8iDcrh03qDGRRmoA2hmBJnxpllMR+0/0qlzjqZES6NdiWDcZkCNAk4rPFZ9Q85r27unkiNNg8ZOiwZXBHwcA==", + "dev": true + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/cli-cursor": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", + "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", + "dev": true, + "dependencies": { + "restore-cursor": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-spinners": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.9.2.tgz", + "integrity": "sha512-ywqV+5MmyL4E7ybXgKys4DugZbX0FC6LnwrhjuykIjnK9k8OQacQ7axGKnjDXWNhns0xot3bZI5h55H8yo9cJg==", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", + "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", + "dev": true, + "dependencies": { + "slice-ansi": "^5.0.0", + "string-width": "^7.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/cli-truncate/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/cli-truncate/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/cli-truncate/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/cli-width": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", + "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", + "dev": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", + "integrity": "sha512-JQHZ2QMW6l3aH/j6xCqQThY/9OH4D/9ls34cgkUBiEeocRTU04tHfKPBsUK1PqZCUQM7GiA0IIXJSuXHI64Kbg==", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "bin": { + "color-support": "bin.js" + } + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", + "dev": true + }, + "node_modules/commondir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", + "integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg==", + "dev": true + }, + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "dev": true, + "dependencies": { + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/compression": { + "version": "1.7.5", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.5.tgz", + "integrity": "sha512-bQJ0YRck5ak3LgtnpKkiabX5pNF7tMUh1BSy2ZBOTh0Dim0BUu6aPPwByIns6/A5Prh8PufSPerMDUklpzes2Q==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.0.2", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", + "dev": true, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/connect/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/connect/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC" + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", + "dev": true + }, + "node_modules/copy-anything": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/copy-anything/-/copy-anything-2.0.6.tgz", + "integrity": "sha512-1j20GZTsvKNkc4BY3NpMOM8tt///wY3FpIzozTOFO2ffuZcV61nojHXVKIy3WM+7ADCy5FVhdZYHYDdgTU0yJw==", + "dev": true, + "dependencies": { + "is-what": "^3.14.1" + }, + "funding": { + "url": "https://github.com/sponsors/mesqueeb" + } + }, + "node_modules/copy-webpack-plugin": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", + "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", + "dev": true, + "dependencies": { + "fast-glob": "^3.3.2", + "glob-parent": "^6.0.1", + "globby": "^14.0.0", + "normalize-path": "^3.0.0", + "schema-utils": "^4.2.0", + "serialize-javascript": "^6.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + } + }, + "node_modules/core-js": { + "version": "3.37.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.37.0.tgz", + "integrity": "sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==", + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", + "dev": true, + "dependencies": { + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", + "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", + "dev": true, + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csv": { + "version": "6.3.11", + "resolved": "https://registry.npmjs.org/csv/-/csv-6.3.11.tgz", + "integrity": "sha512-a8bhT76Q546jOElHcTrkzWY7Py925mfLO/jqquseH61ThOebYwOjLbWHBqdRB4K1VpU36sTyIei6Jwj7QdEZ7g==", + "dependencies": { + "csv-generate": "^4.4.2", + "csv-parse": "^5.6.0", + "csv-stringify": "^6.5.2", + "stream-transform": "^3.3.3" + }, + "engines": { + "node": ">= 0.1.90" + } + }, + "node_modules/csv-generate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/csv-generate/-/csv-generate-4.4.2.tgz", + "integrity": "sha512-W6nVsf+rz0J3yo9FOjeer7tmzBJKaTTxf7K0uw6GZgRocZYPVpuSWWa5/aoWWrjQZj4/oNIKTYapOM7hiNjVMA==" + }, + "node_modules/csv-parse": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-5.6.0.tgz", + "integrity": "sha512-l3nz3euub2QMg5ouu5U09Ew9Wf6/wQ8I++ch1loQ0ljmzhmfZYrH9fflS22i/PQEvsPvxCwxgz5q7UB8K1JO4Q==" + }, + "node_modules/csv-stringify": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-6.5.2.tgz", + "integrity": "sha512-RFPahj0sXcmUyjrObAK+DOWtMvMIFV328n4qZJhgX3x2RqkQgOTU2mCUmiFR0CzM6AzChlRSUErjiJeEt8BaQA==" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true + }, + "node_modules/default-browser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", + "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", + "dev": true, + "dependencies": { + "bundle-name": "^4.1.0", + "default-browser-id": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-browser-id": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", + "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/defaults": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.4.tgz", + "integrity": "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A==", + "dependencies": { + "clone": "^1.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", + "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-libc": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", + "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", + "dev": true + }, + "node_modules/dev-ip": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dev-ip/-/dev-ip-1.0.1.tgz", + "integrity": "sha512-LmVkry/oDShEgSZPNgqCIp2/TlqtExeGmymru3uCELnfyjY11IzpAproLYs+1X88fXO6DBoYP3ul2Xo2yz2j6A==", + "bin": { + "dev-ip": "lib/dev-ip.js" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" + }, + "node_modules/easy-extender": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/easy-extender/-/easy-extender-2.3.4.tgz", + "integrity": "sha512-8cAwm6md1YTiPpOvDULYJL4ZS6WfM5/cTeVVh4JsvyYZAoqlRVUpHL9Gr5Fy7HA6xcSZicUia3DeAgO3Us8E+Q==", + "dependencies": { + "lodash": "^4.17.10" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/eazy-logger": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/eazy-logger/-/eazy-logger-4.0.1.tgz", + "integrity": "sha512-2GSFtnnC6U4IEKhEI7+PvdxrmjJ04mdsj3wHZTFiw0tUtG4HCWzTr13ZYTk8XOGnA1xQMaDljoBOYlk3D/MMSw==", + "dependencies": { + "chalk": "4.1.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/eazy-logger/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.82", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.82.tgz", + "integrity": "sha512-Zq16uk1hfQhyGx5GpwPAYDwddJuSGhtRhgOA2mCxANYaDT79nAeGnaXogMGng4KqLaJUVnOnuL0+TDop9nLOiA==" + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", + "integrity": "sha512-gmNvsYi9C8iErnZdVcJnvCpSKbWTt1E8+JZo8b+daLninywUWi5NQ5STSHZ9rFjFO7imNcvb8Pc5pe/wMR5xEw==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.2.tgz", + "integrity": "sha512-TAr+NKeoVTjEVW8P3iHguO1LO6RlUz9O5Y8o7EY0fU+gY1NYqas7NN3slpFtbXEsLMHk0h90fJMfKjRkQ0qUIw==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1", + "xmlhttprequest-ssl": "~2.1.1" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/enhanced-resolve": { + "version": "5.18.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.0.tgz", + "integrity": "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.2.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, + "node_modules/errno": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", + "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "dev": true, + "optional": true, + "dependencies": { + "prr": "~1.0.1" + }, + "bin": { + "errno": "cli.js" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.6.0.tgz", + "integrity": "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ==", + "dev": true + }, + "node_modules/es-module-shims": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/es-module-shims/-/es-module-shims-1.10.1.tgz", + "integrity": "sha512-HSSkRLkqFEyX6GrCAHrSOR5iz/QzQJRqZUF7bFJOZ4aoSw0WoSggfsTIGN2yFbF8v6xjQFhT4HGP6b+0qQZmEQ==", + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.0.tgz", + "integrity": "sha512-FuLPevChGDshgSicjisSooU0cemp/sGXR841D5LHMB7mTVOmsEHcAxaH3irL53+8YDIeVNQEySh4DaYU/iuPqQ==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.24.0", + "@esbuild/android-arm": "0.24.0", + "@esbuild/android-arm64": "0.24.0", + "@esbuild/android-x64": "0.24.0", + "@esbuild/darwin-arm64": "0.24.0", + "@esbuild/darwin-x64": "0.24.0", + "@esbuild/freebsd-arm64": "0.24.0", + "@esbuild/freebsd-x64": "0.24.0", + "@esbuild/linux-arm": "0.24.0", + "@esbuild/linux-arm64": "0.24.0", + "@esbuild/linux-ia32": "0.24.0", + "@esbuild/linux-loong64": "0.24.0", + "@esbuild/linux-mips64el": "0.24.0", + "@esbuild/linux-ppc64": "0.24.0", + "@esbuild/linux-riscv64": "0.24.0", + "@esbuild/linux-s390x": "0.24.0", + "@esbuild/linux-x64": "0.24.0", + "@esbuild/netbsd-x64": "0.24.0", + "@esbuild/openbsd-arm64": "0.24.0", + "@esbuild/openbsd-x64": "0.24.0", + "@esbuild/sunos-x64": "0.24.0", + "@esbuild/win32-arm64": "0.24.0", + "@esbuild/win32-ia32": "0.24.0", + "@esbuild/win32-x64": "0.24.0" + } + }, + "node_modules/esbuild-wasm": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.0.tgz", + "integrity": "sha512-xhNn5tL1AhkPg4ft59yXT6FkwKXiPSYyz1IeinJHUJpjvOHOIPvdmFQc0pGdjxlKSbzZc2mNmtVOWAR1EF/JAg==", + "dev": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.18.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.18.0.tgz", + "integrity": "sha512-+waTfRWQlSbpt3KWE+CjrPPYnbq9kfZIYUqapc0uBXyjTp8aYXZDsUH16m39Ryq3NjAVP4tjuF7KaukeqoCoaA==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.19.0", + "@eslint/core": "^0.10.0", + "@eslint/eslintrc": "^3.2.0", + "@eslint/js": "9.18.0", + "@eslint/plugin-kit": "^0.2.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.1", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.2.0", + "eslint-visitor-keys": "^4.2.0", + "espree": "^10.3.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.2.0.tgz", + "integrity": "sha512-PHlWUfG6lvPc3yvP5A4PNyBL1W8fkDUccmI21JUu/+GKZBoH/W5u6usENXUrWFRsyoW5ACUjFGgAFQp5gUlb/A==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", + "integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", + "dev": true, + "dependencies": { + "acorn": "^8.14.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, + "node_modules/express": { + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", + "dev": true, + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.3", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.7.1", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.3.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.12", + "proxy-addr": "~2.0.7", + "qs": "6.13.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.19.0", + "serve-static": "1.16.2", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/external-editor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-3.1.0.tgz", + "integrity": "sha512-hMQ4CX1p1izmuLYyZqLMO/qGNw10wSv9QDCPfzXfyFrOaCSSoRfqE1Kf1s5an66J5JZC62NewG+mK49jOCtQew==", + "dev": true, + "dependencies": { + "chardet": "^0.7.0", + "iconv-lite": "^0.4.24", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.2.tgz", + "integrity": "sha512-oX2ruAFQwf/Orj8m737Y5adxDQO0LAB7/S5MnxCdTNDd4p6BsyIVsv9JQsATbTSq8KHRpLwIHbVlUNatxd+1Ow==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.4" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true + }, + "node_modules/fast-uri": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.5.tgz", + "integrity": "sha512-5JnBCWpFlMo0a3ciDy/JckMzzv1U9coZrIhedq+HXxxUfDTAiS0LA8OKVao4G9BxmCVck/jtA5r3KAtRWEyD8Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ] + }, + "node_modules/fastq": { + "version": "1.18.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.18.0.tgz", + "integrity": "sha512-QKHXPW0hD8g4UET03SdOdunzSouc9N4AuHdsX8XNcTsuz+yYFILVNIX4l9yHABMhiEI9Db0JTTIpu0wB+Y1QQw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/file-saver": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/file-saver/-/file-saver-2.0.5.tgz", + "integrity": "sha512-P9bmyZ3h/PRG+Nzga+rbdI4OEpNDzAVyy74uVO9ATgzLK6VtAsYybF/+TOCvrc0MO793d6+42lLyZTw7/ArVzA==" + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.0.tgz", + "integrity": "sha512-Ld2g8rrAyMYFXBhEqMz8ZAHBi4J4uS1i/CxGMDnjyFWddMXLVcDp051DZfu+t7+ab7Wv6SMqpWmyFIj5UbfFvg==", + "dependencies": { + "cross-spawn": "^7.0.0", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", + "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", + "dev": true, + "engines": { + "node": "*" + }, + "funding": { + "type": "patreon", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs-extra": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.2.0.tgz", + "integrity": "sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/fuzzysort": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fuzzysort/-/fuzzysort-3.1.0.tgz", + "integrity": "sha512-sR9BNCjBg6LNgwvxlBd0sBABvQitkLzoVY9MYYROQVX/FvfJ4Mai9LsGhDgd8qYdds0bY77VzYd5iuB+v5rwQQ==" + }, + "node_modules/gauge": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-4.0.4.tgz", + "integrity": "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.3", + "console-control-strings": "^1.1.0", + "has-unicode": "^2.0.1", + "signal-exit": "^3.0.7", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.5" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC" + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", + "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.7.tgz", + "integrity": "sha512-VW6Pxhsrk0KAOqs3WEd0klDiF/+V7gQOpAvY1jVU/LHmaD/kQO4523aiJuikX/QAKYiW6x8Jh+RJej1almdtCA==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "function-bind": "^1.1.2", + "get-proto": "^1.0.0", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/glob": { + "version": "11.0.1", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.1.tgz", + "integrity": "sha512-zrQDm8XPnYEKawJScsnM0QzobJxlT/kHOOlRTio8IH/GrmxRE5fjllkzdaHclIuNjUQTJYH2xHNIGfdpJkDJUw==", + "peer": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^2.0.0" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/glob/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "peer": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/globby": { + "version": "14.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", + "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", + "dev": true, + "dependencies": { + "@sindresorhus/merge-streams": "^2.1.0", + "fast-glob": "^3.3.2", + "ignore": "^5.2.4", + "path-type": "^5.0.0", + "slash": "^5.1.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/globby/node_modules/path-type": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", + "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC" + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hosted-git-info": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", + "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "dev": true, + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/hosted-git-info/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/hpack.js/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/hpack.js/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true + }, + "node_modules/hpack.js/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ] + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/htmlparser2": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", + "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "dev": true, + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.1.0", + "entities": "^4.5.0" + } + }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.9.tgz", + "integrity": "sha512-n1XsPy3rXVxlqxVioEWdC+0+M+SQw0DpJynwtOPo1X+ZlvdzTLtDBIJJlDQTnwZIFJrZSzSGmIOUdP8tu+SgLw==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", + "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.15", + "debug": "^4.3.6", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.3", + "is-plain-object": "^5.0.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.5.tgz", + "integrity": "sha512-1e4Wqeblerz+tMKPIq2EMGiiWW1dIjZOksyHWSUm1rmuvw/how9hBHZ38lAGj5ID4Ik6EdkOw7NmWPy6LAwalw==", + "dev": true, + "dependencies": { + "agent-base": "^7.0.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/hyperdyperid": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", + "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", + "dev": true, + "engines": { + "node": ">=10.18" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-6.0.2.tgz", + "integrity": "sha512-InwqeHHN2XpumIkMvpl/DCJVrAHgCsG5+cn1XlnLWGwtZBm8QJfSusItfrwx81CTp5agNZqpKU2J/ccC5nGT4A==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", + "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", + "dev": true, + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/image-size": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-0.5.5.tgz", + "integrity": "sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==", + "dev": true, + "optional": true, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/immutable": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.0.3.tgz", + "integrity": "sha512-P8IdPQHq3lA1xVeBRi5VPqUm5HDgKnx0Ru51wZz5mjxHr5n3RWhjIpOFU7ybkUxfB+5IToy+OLaHYDBIWsv+uw==", + "dev": true + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", + "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/injection-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/injection-js/-/injection-js-2.4.0.tgz", + "integrity": "sha512-6jiJt0tCAo9zjHbcwLiPL+IuNe9SQ6a9g0PEzafThW3fOQi0mrmiJGBJvDD6tmhPh8cQHIQtCOrJuBfQME4kPA==", + "dev": true, + "dependencies": { + "tslib": "^2.0.0" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", + "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-inside-container": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", + "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", + "dev": true, + "dependencies": { + "is-docker": "^3.0.0" + }, + "bin": { + "is-inside-container": "cli.js" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-inside-container/node_modules/is-docker": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", + "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-interactive": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-network-error": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", + "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", + "dev": true, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-number-like": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/is-number-like/-/is-number-like-1.0.8.tgz", + "integrity": "sha512-6rZi3ezCyFcn5L71ywzz2bS5b2Igl1En3eTlZlvKjpz1n3IZLAYMbKYAIQgFmEu0GENg92ziU/faEOA/aixjbA==", + "license": "ISC", + "dependencies": { + "lodash.isfinite": "^3.3.2" + } + }, + "node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-what": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/is-what/-/is-what-3.14.1.tgz", + "integrity": "sha512-sNxgpk9793nzSs7bA6JQJGeIuRBQhAaNGG77kzYQgMkrID+lS6SlK07K5LaptscDlSaIgH+GPFzf+d75FVxozA==", + "dev": true + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.7.tgz", + "integrity": "sha512-BewmUXImeuRk2YY0PVbxgKAysvhRPUQE0h5QRM++nVWyubKGV0l8qQ5op8+B2DOmwSe63Jivj0BjkPQVf8fP5g==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.2.tgz", + "integrity": "sha512-bZsjR/iRjl1Nk1UkjGpAzLNfQtzuijhn2g+pbZb98HQ1Gk8vM9hfbxeMBP+M2/UUdwj0RqGG3mlvk2MsAqwvEw==", + "peer": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/jasmine-core": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-5.4.0.tgz", + "integrity": "sha512-T4fio3W++llLd7LGSGsioriDHgWyhoL6YTu4k37uwJLF7DzOzspz7mNxRoM3cQdLWtL/ebazQpIf/yZGJx/gzg==", + "dev": true + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jiti": { + "version": "1.21.7", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", + "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", + "dev": true, + "bin": { + "jiti": "bin/jiti.js" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", + "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "peer": true, + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ] + }, + "node_modules/jstz": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/jstz/-/jstz-2.1.1.tgz", + "integrity": "sha512-8hfl5RD6P7rEeIbzStBz3h4f+BQHfq/ABtoU6gXKQv5OcZhnmrIpG7e1pYaZ8hS9e0mp+bxUj08fnDUbKctYyA==", + "engines": { + "node": ">=0.10" + } + }, + "node_modules/karma": { + "version": "6.4.4", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.4.tgz", + "integrity": "sha512-LrtUxbdvt1gOpo3gxG+VAJlJAEMhbWlM4YrFQgql98FwF7+K8K12LYO4hnDdUkNjeztYrOXEMqgTajSWgmtI/w==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.7.2", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-coverage": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/karma-coverage/-/karma-coverage-2.2.1.tgz", + "integrity": "sha512-yj7hbequkQP2qOSb20GuNSIyE//PgJWHwC2IydLE6XRtsnaflv+/OSGNssPjobYUlhVVagy99TQpqUt3vAUG7A==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.2.0", + "istanbul-lib-instrument": "^5.1.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.1", + "istanbul-reports": "^3.0.5", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/karma-coverage/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma-coverage/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma-coverage/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma-coverage/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/karma-firefox-launcher": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/karma-firefox-launcher/-/karma-firefox-launcher-2.1.3.tgz", + "integrity": "sha512-LMM2bseebLbYjODBOVt7TCPP9OI2vZIXCavIXhkO9m+10Uj5l7u/SKoeRmYx8FYHTVGZSpk6peX+3BMHC1WwNw==", + "dev": true, + "dependencies": { + "is-wsl": "^2.2.0", + "which": "^3.0.0" + } + }, + "node_modules/karma-firefox-launcher/node_modules/which": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/which/-/which-3.0.1.tgz", + "integrity": "sha512-XA1b62dzQzLfaEOSQFTCOd5KFf/1VSzZo7/7TUjnya6u0vGGKzU96UQBZTAThCb2j4/xjBAyii1OhRLJEivHvg==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/karma-jasmine": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-5.1.0.tgz", + "integrity": "sha512-i/zQLFrfEpRyQoJF9fsCdTMOF5c2dK7C7OmsuKg2D0YSsuZSfQDiLuaiktbuio6F2wiCsZSnSnieIQ0ant/uzQ==", + "dev": true, + "dependencies": { + "jasmine-core": "^4.1.0" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "karma": "^6.0.0" + } + }, + "node_modules/karma-jasmine-html-reporter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/karma-jasmine-html-reporter/-/karma-jasmine-html-reporter-2.1.0.tgz", + "integrity": "sha512-sPQE1+nlsn6Hwb5t+HHwyy0A1FNCVKuL1192b+XNauMYWThz2kweiBVW1DqloRpVvZIJkIoHVB7XRpK78n1xbQ==", + "dev": true, + "peerDependencies": { + "jasmine-core": "^4.0.0 || ^5.0.0", + "karma": "^6.0.0", + "karma-jasmine": "^5.0.0" + } + }, + "node_modules/karma-jasmine/node_modules/jasmine-core": { + "version": "4.6.1", + "resolved": "https://registry.npmjs.org/jasmine-core/-/jasmine-core-4.6.1.tgz", + "integrity": "sha512-VYz/BjjmC3klLJlLwA4Kw8ytk0zDSmbbDLNs794VnWmkcCB7I9aAL/D48VNQtmITyPvea2C3jdUMfc3kAoy0PQ==", + "dev": true + }, + "node_modules/karma-source-map-support": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/karma-source-map-support/-/karma-source-map-support-1.4.0.tgz", + "integrity": "sha512-RsBECncGO17KAoJCYXjv+ckIz+Ii9NCi+9enk+rq6XC81ezYkb4/RHE6CTXdA7IOJqoF3wcaLfVG0CPmE5ca6A==", + "dev": true, + "dependencies": { + "source-map-support": "^0.5.5" + } + }, + "node_modules/karma/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/karma/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/karma/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/karma/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/karma/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/karma/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/karma/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/karma/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/karma/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/karma/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/karma/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/karma/node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/karma/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/karma/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/karma/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/launch-editor": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/less": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.2.0.tgz", + "integrity": "sha512-P3b3HJDBtSzsXUl0im2L7gTO5Ubg8mEN6G8qoTS77iXxXX4Hvu4Qj540PZDvQ8V6DmX6iXo98k7Md0Cm1PrLaA==", + "dev": true, + "dependencies": { + "copy-anything": "^2.0.1", + "parse-node-version": "^1.0.1", + "tslib": "^2.3.0" + }, + "bin": { + "lessc": "bin/lessc" + }, + "engines": { + "node": ">=6" + }, + "optionalDependencies": { + "errno": "^0.1.1", + "graceful-fs": "^4.1.2", + "image-size": "~0.5.0", + "make-dir": "^2.1.0", + "mime": "^1.4.1", + "needle": "^3.1.0", + "source-map": "~0.6.0" + } + }, + "node_modules/less-loader": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", + "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", + "dev": true, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "less": "^3.5.0 || ^4.0.0", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/less/node_modules/make-dir": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", + "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^4.0.1", + "semver": "^5.6.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/less/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/less/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/less/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/license-webpack-plugin": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/license-webpack-plugin/-/license-webpack-plugin-4.0.2.tgz", + "integrity": "sha512-771TFWFD70G1wLTC4oU2Cw4qvtmNrIw+wRvBtn+okgHl7slJVi7zfNcdmqDL72BojM30VNJ2UHylr1o77U37Jw==", + "dev": true, + "dependencies": { + "webpack-sources": "^3.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-sources": { + "optional": true + } + } + }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==" + }, + "node_modules/listr2": { + "version": "8.2.5", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", + "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "dev": true, + "dependencies": { + "cli-truncate": "^4.0.0", + "colorette": "^2.0.20", + "eventemitter3": "^5.0.1", + "log-update": "^6.1.0", + "rfdc": "^1.4.1", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/listr2/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/listr2/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/listr2/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/listr2/node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "dev": true + }, + "node_modules/listr2/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/listr2/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/listr2/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/lmdb": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.1.5.tgz", + "integrity": "sha512-46Mch5Drq+A93Ss3gtbg+Xuvf5BOgIuvhKDWoGa3HcPHI6BL2NCOkRdSx1D4VfzwrxhnsjbyIVsLRlQHu6URvw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "msgpackr": "^1.11.2", + "node-addon-api": "^6.1.0", + "node-gyp-build-optional-packages": "5.2.2", + "ordered-binary": "^1.5.3", + "weak-lru-cache": "^1.2.2" + }, + "bin": { + "download-lmdb-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@lmdb/lmdb-darwin-arm64": "3.1.5", + "@lmdb/lmdb-darwin-x64": "3.1.5", + "@lmdb/lmdb-linux-arm": "3.1.5", + "@lmdb/lmdb-linux-arm64": "3.1.5", + "@lmdb/lmdb-linux-x64": "3.1.5", + "@lmdb/lmdb-win32-x64": "3.1.5" + } + }, + "node_modules/loader-runner": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", + "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", + "dev": true, + "engines": { + "node": ">=6.11.5" + } + }, + "node_modules/loader-utils": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", + "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "dev": true, + "engines": { + "node": ">= 12.13.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.isfinite": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lodash.isfinite/-/lodash.isfinite-3.3.2.tgz", + "integrity": "sha512-7FGG40uhC8Mm633uKW1r58aElFlBlxCrg9JfSi3P6aYiWmfiWF0PgMd86ZUsxE5GwWPdHoS2+48bwTh2VPkIQA==", + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-update": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", + "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", + "dev": true, + "dependencies": { + "ansi-escapes": "^7.0.0", + "cli-cursor": "^5.0.0", + "slice-ansi": "^7.1.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-escapes": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", + "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", + "dev": true, + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/log-update/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-update/node_modules/emoji-regex": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", + "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", + "dev": true + }, + "node_modules/log-update/node_modules/is-fullwidth-code-point": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", + "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", + "dev": true, + "dependencies": { + "get-east-asian-width": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/slice-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", + "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "is-fullwidth-code-point": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-update/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/log-update/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/magic-string": { + "version": "0.30.12", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.12.tgz", + "integrity": "sha512-Ea8I3sQMVXr8JhN4z+H/d8zwo+tYDgHE9+5G4Wnrwhs0gaK9fXTKx0Tw5Xwsd/bCPTTZNRAdpyzvoeORe9LYpw==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-fetch-happen": { + "version": "14.0.3", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", + "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", + "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/make-plural": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/make-plural/-/make-plural-7.4.0.tgz", + "integrity": "sha512-4/gC9KVNTV6pvYg2gFeQYTW3mWaoJt7WZE5vrp1KnQDgW92JtYZnzmZT81oj/dUTqAIu0ufI2x3dkgu3bB1tYg==" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "4.17.0", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", + "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", + "dev": true, + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/micromatch/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-function": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", + "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.0.tgz", + "integrity": "sha512-2v6aXUXwLP1Epd/gc32HAMIWoczx+fZwEPRHm/VwtrJzRGwR1qGZXEYV3Zp8ZjjbwaZhMrM6uHV4KVkk+XCc2w==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-flush/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/minizlib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", + "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.4", + "rimraf": "^5.0.5" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/minizlib/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/minizlib/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/minizlib/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minizlib/node_modules/rimraf": { + "version": "5.0.10", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", + "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "dev": true, + "dependencies": { + "glob": "^10.3.7" + }, + "bin": { + "rimraf": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/mitt": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-1.2.0.tgz", + "integrity": "sha512-r6lj77KlwqLhIUku9UWYes7KJtsczvolZkzp8hbaDPPaE24OmWl5s539Mytlj22siEQKosZ26qCBgda2PKwoJw==", + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/moo": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.2.tgz", + "integrity": "sha512-iSAJLHYKnX41mKcJKjqvnAN9sf0LMDTXDEvFv+ffuRR9a1MIuXLjMNL6EsnDHSkKLTWNqQQ5uo61P4EbU4NU+Q==" + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/msgpackr": { + "version": "1.11.2", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", + "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "dev": true, + "optional": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.2" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.3.tgz", + "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" + } + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", + "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/nanoid": { + "version": "3.3.8", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz", + "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanopop": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/nanopop/-/nanopop-2.4.2.tgz", + "integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/needle": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/needle/-/needle-3.3.1.tgz", + "integrity": "sha512-6k0YULvhpw+RoLNiQCRKOl09Rv1dPLr8hHnVjHqdolKwDrdNyk+Hmrthi4lIGPPz3r39dLx0hsF5s40sZ3Us4Q==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "sax": "^1.2.4" + }, + "bin": { + "needle": "bin/needle" + }, + "engines": { + "node": ">= 4.4.x" + } + }, + "node_modules/needle/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/ng-packagr": { + "version": "19.0.1", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.0.1.tgz", + "integrity": "sha512-PnXa/y3ce3v4bKJNtUBS7qcNoyv5g/tSthoMe23NyMV5kjNY4+hJT7h64zK+8tnJWTelCbIpoep7tmSPsOifBA==", + "dev": true, + "dependencies": { + "@rollup/plugin-json": "^6.1.0", + "@rollup/wasm-node": "^4.24.0", + "ajv": "^8.17.1", + "ansi-colors": "^4.1.3", + "browserslist": "^4.22.1", + "chokidar": "^4.0.1", + "commander": "^12.1.0", + "convert-source-map": "^2.0.0", + "dependency-graph": "^1.0.0", + "esbuild": "^0.24.0", + "fast-glob": "^3.3.2", + "find-cache-dir": "^3.3.2", + "injection-js": "^2.4.0", + "jsonc-parser": "^3.3.1", + "less": "^4.2.0", + "ora": "^5.1.0", + "piscina": "^4.7.0", + "postcss": "^8.4.47", + "rxjs": "^7.8.1", + "sass": "^1.79.5" + }, + "bin": { + "ng-packagr": "cli/main.js" + }, + "engines": { + "node": "^18.19.1 || >=20.11.1" + }, + "optionalDependencies": { + "rollup": "^4.24.0" + }, + "peerDependencies": { + "@angular/compiler-cli": "^19.0.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.5 <5.7" + }, + "peerDependenciesMeta": { + "tailwindcss": { + "optional": true + } + } + }, + "node_modules/ng-packagr/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/ng-packagr/node_modules/find-cache-dir": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", + "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", + "dev": true, + "dependencies": { + "commondir": "^1.0.1", + "make-dir": "^3.0.2", + "pkg-dir": "^4.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/avajs/find-cache-dir?sponsor=1" + } + }, + "node_modules/ng-packagr/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ng-packagr/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ng-packagr/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ng-packagr/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ng-packagr/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ng-packagr/node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ng-packagr/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/ngx-build-plus": { + "version": "19.0.0", + "resolved": "https://registry.npmjs.org/ngx-build-plus/-/ngx-build-plus-19.0.0.tgz", + "integrity": "sha512-JXzRvDZH1gQ2xGiePduHoMeR9kaK1cHHhT6d6npVBq8aWGFHs1iAg/5/gkieCsz2QfrcaoSF8ZBb9ZhcGX4Z2g==", + "dev": true, + "dependencies": { + "webpack-merge": "^5.0.0" + }, + "peerDependencies": { + "@angular-devkit/build-angular": ">=19.0.0", + "@schematics/angular": ">=19.0.0", + "rxjs": ">= 6.0.0" + } + }, + "node_modules/ngx-build-plus/node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/node-addon-api": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-6.1.0.tgz", + "integrity": "sha512-+eawOlIgy680F0kBzPUNFhMZGtJ1YmqM6l4+Crf4IkImjYrO/mqPwRMh352g23uIaQKFItcQ64I7KMaJxHgAVA==", + "dev": true, + "optional": true + }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "dev": true, + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-gyp": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.0.0.tgz", + "integrity": "sha512-zQS+9MTTeCMgY0F3cWPyJyRFAkVltQ1uXm+xXu/ES6KFgC6Czo1Seb9vQW2wNxSX2OrDTiqL0ojtkFxBQ0ypIw==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", + "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/node-gyp/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", + "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", + "dev": true, + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", + "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", + "dev": true, + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", + "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", + "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==" + }, + "node_modules/nopt": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.0.0.tgz", + "integrity": "sha512-1L/fTJ4UmV/lUxT2Uf006pfZKTvAgCF+chz+0OgBHO8u2Z67pE7AaAUUj7CJy0lXqHmymUvGFt6NE9R3HER0yw==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-package-data": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-7.0.0.tgz", + "integrity": "sha512-k6U0gKRIuNCTkwHGZqblCfLfBRh+w1vI6tBo+IeJwq2M8FUiOqhX7GH+GArQGScA7azd1WfyRCvxoXDO3hQDIA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/normalize-range": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", + "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", + "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-install-checks": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", + "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", + "dev": true, + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", + "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-package-arg": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.0.tgz", + "integrity": "sha512-ZTE0hbwSdTNL+Stx2zxSqdu2KZfNDcrtrLdIk7XGnQFYBWYDho/ORvXtn5XEePcL3tFpGjHCV3X3xrtDh7eZ+A==", + "dev": true, + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-packlist": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", + "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "dev": true, + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-pick-manifest": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", + "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", + "dev": true, + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm-registry-fetch": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", + "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", + "dev": true, + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npmlog": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-6.0.2.tgz", + "integrity": "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "dependencies": { + "are-we-there-yet": "^3.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^4.0.3", + "set-blocking": "^2.0.0" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.3", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", + "integrity": "sha512-kDCGIbxkDSXE3euJZZXzc6to7fCrKHNI/hSRQnRuQ+BWjFNzZwiFF8fj/6o2t2G9/jTj8PSIYTfCLelLZEeRpA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", + "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", + "dev": true, + "dependencies": { + "mimic-function": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", + "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "dev": true, + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "dev": true, + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opn": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/opn/-/opn-5.3.0.tgz", + "integrity": "sha512-bYJHo/LOmoTd+pfiYhfZDnf9zekVJrY+cnS2a5F2x+w5ppvTqObojTP7WiFG+kVZs9Inw+qQ/lw7TroWwhdd2g==", + "license": "MIT", + "dependencies": { + "is-wsl": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/opn/node_modules/is-wsl": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", + "integrity": "sha512-gfygJYZ2gLTDlmbWMI0CE2MwnFzSN/2SZfkMlItC4K/JBlsWVDB0bO6XhqcY13YXE7iMcAJnzTCJjPiTeJJ0Mw==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/ora": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", + "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dependencies": { + "bl": "^4.1.0", + "chalk": "^4.1.0", + "cli-cursor": "^3.1.0", + "cli-spinners": "^2.5.0", + "is-interactive": "^1.0.0", + "is-unicode-supported": "^0.1.0", + "log-symbols": "^4.1.0", + "strip-ansi": "^6.0.0", + "wcwidth": "^1.0.1" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/ora/node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ora/node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ora/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + }, + "node_modules/ordered-binary": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", + "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", + "dev": true, + "optional": true + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha512-D2FR03Vir7FIu45XBY20mTb+/ZSWB00sjU9jdQXt83gDrI4Ztz5Fs7/yy74g2N5SVQY4xY1qDr4rNddwYRVX0g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", + "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-retry/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==" + }, + "node_modules/pacote": { + "version": "20.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", + "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "dev": true, + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-json/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + }, + "node_modules/parse-node-version": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-node-version/-/parse-node-version-1.0.1.tgz", + "integrity": "sha512-3YHlOa/JgH6Mnpr05jP9eDG254US9ek25LyIxZlDItp2iJtwyaXQb57lBYLdT3MowkUFYEV2XXNAYIPlESvJlA==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parse5": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", + "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", + "dev": true, + "dependencies": { + "entities": "^4.5.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-html-rewriting-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.0.0.tgz", + "integrity": "sha512-mazCyGWkmCRWDI15Zp+UiCqMp/0dgEmkZRvhlsqqKYr4SsVm/TvnSpD9fCvqCA2zoWJcfRym846ejWBBHRiYEg==", + "dev": true, + "dependencies": { + "entities": "^4.3.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-sax-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-sax-parser/-/parse5-sax-parser-7.0.0.tgz", + "integrity": "sha512-5A+v2SNsq8T6/mG3ahcz8ZtQ0OUFTatxPbeidoMB7tkJSGDY3tdfl4MHovtLQHkEn5CGxijNWRQHhRQ6IRpXKg==", + "dev": true, + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.2.tgz", + "integrity": "sha512-123qHRfJBmo2jXDbo/a5YOQrJoHF/GNQTLzQ5+IdK5pWpceK17yRc6ozlWd25FxvGKQbIUs91fDFkXmDHTKcyA==", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "engines": { + "node": ">=8" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==" + }, + "node_modules/picomatch": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.2.tgz", + "integrity": "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/piscina": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.7.0.tgz", + "integrity": "sha512-b8hvkpp9zS0zsfa939b/jXbe64Z2gZv0Ha7FYPNUiDIB1y2AtxcOZdfP8xN8HFjUaqQiT9gRlfjAsoL8vdJ1Iw==", + "dev": true, + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" + } + }, + "node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/pkg-dir/node_modules/yocto-queue": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", + "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", + "dev": true, + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/portscanner": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/portscanner/-/portscanner-2.2.0.tgz", + "integrity": "sha512-IFroCz/59Lqa2uBvzK3bKDbDDIEaAY8XJ1jFxcLWTqosrsc32//P4VuSB2vZXoHiHqOmx8B5L5hnKOxL/7FlPw==", + "license": "MIT", + "dependencies": { + "async": "^2.6.0", + "is-number-like": "^1.0.3" + }, + "engines": { + "node": ">=0.4", + "npm": ">=1.0.0" + } + }, + "node_modules/postcss": { + "version": "8.4.49", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", + "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "nanoid": "^3.3.7", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-loader": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", + "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "dev": true, + "dependencies": { + "cosmiconfig": "^9.0.0", + "jiti": "^1.20.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/postcss-loader/node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.0.0.tgz", + "integrity": "sha512-9RbEr1Y7FFfptd/1eEdntyjMwLeghW1bHX9GWjXo19vx4ytPQhANltvVxDggzJl7mnWM+dX28kb6cyS/4iQjlQ==", + "dev": true, + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/proc-log": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", + "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true + }, + "node_modules/promise-inflight": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", + "integrity": "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g==", + "dev": true + }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dev": true, + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/prr": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", + "integrity": "sha512-yPw4Sng1gWghHQWj0B3ZggWUm4qVbPwPFcRG8KyxiU7J2OHFSoEHKS+EZ3fv5l1t9CyCiop6l/ZYeWbrgoQejw==", + "dev": true, + "optional": true + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dev": true, + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/readdirp": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", + "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "dev": true, + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/reflect-metadata": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", + "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", + "dev": true + }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", + "dev": true + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, + "node_modules/regex-parser": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", + "integrity": "sha512-TVILVSz2jY5D47F4mA4MppkBrafEaiUWJO/TcZHEIuI13AqoZMkK1WMA4Om1YkYbTx+9Ki1/tSUXbceyr9saRg==", + "dev": true + }, + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/replace-in-file": { + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/replace-in-file/-/replace-in-file-6.3.5.tgz", + "integrity": "sha512-arB9d3ENdKva2fxRnSjwBEXfK1npgyci7ZZuwysgAp7ORjHSyxz6oqIjTEv8R0Ydl4Ll7uOAZXL4vbkhGIizCg==", + "dependencies": { + "chalk": "^4.1.2", + "glob": "^7.2.0", + "yargs": "^17.2.1" + }, + "bin": { + "replace-in-file": "bin/cli.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/replace-in-file/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/replace-in-file/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/replace-in-file/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/replace-in-file/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==" + }, + "node_modules/resolve": { + "version": "1.22.8", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", + "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "dev": true, + "dependencies": { + "is-core-module": "^2.13.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-url-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-url-loader/-/resolve-url-loader-5.0.0.tgz", + "integrity": "sha512-uZtduh8/8srhBoMx//5bwqjQ+rfYOUq8zC9NrMUGtjBiGTtFJM42s58/36+hTqeqINcnYe08Nj3LkK9lW4N8Xg==", + "dev": true, + "dependencies": { + "adjust-sourcemap-loader": "^4.0.0", + "convert-source-map": "^1.7.0", + "loader-utils": "^2.0.0", + "postcss": "^8.2.14", + "source-map": "0.6.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/resolve-url-loader/node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/resolve-url-loader/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resp-modifier": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/resp-modifier/-/resp-modifier-6.0.2.tgz", + "integrity": "sha512-U1+0kWC/+4ncRFYqQWTx/3qkfE6a4B/h3XXgmXypfa0SPZ3t7cbbaFk297PjQS/yov24R18h6OZe6iZwj3NSLw==", + "dependencies": { + "debug": "^2.2.0", + "minimatch": "^3.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/resp-modifier/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/resp-modifier/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/resp-modifier/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/resp-modifier/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "license": "MIT" + }, + "node_modules/restore-cursor": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", + "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", + "dev": true, + "dependencies": { + "onetime": "^7.0.0", + "signal-exit": "^4.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", + "dev": true, + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/rimraf/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/rollup": { + "version": "4.26.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.26.0.tgz", + "integrity": "sha512-ilcl12hnWonG8f+NxU6BlgysVA0gvY2l8N0R84S1HcINbW20bvwuCngJkkInV6LXhwRpucsW5k1ovDwEdBVrNg==", + "dev": true, + "dependencies": { + "@types/estree": "1.0.6" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.26.0", + "@rollup/rollup-android-arm64": "4.26.0", + "@rollup/rollup-darwin-arm64": "4.26.0", + "@rollup/rollup-darwin-x64": "4.26.0", + "@rollup/rollup-freebsd-arm64": "4.26.0", + "@rollup/rollup-freebsd-x64": "4.26.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.26.0", + "@rollup/rollup-linux-arm-musleabihf": "4.26.0", + "@rollup/rollup-linux-arm64-gnu": "4.26.0", + "@rollup/rollup-linux-arm64-musl": "4.26.0", + "@rollup/rollup-linux-powerpc64le-gnu": "4.26.0", + "@rollup/rollup-linux-riscv64-gnu": "4.26.0", + "@rollup/rollup-linux-s390x-gnu": "4.26.0", + "@rollup/rollup-linux-x64-gnu": "4.26.0", + "@rollup/rollup-linux-x64-musl": "4.26.0", + "@rollup/rollup-win32-arm64-msvc": "4.26.0", + "@rollup/rollup-win32-ia32-msvc": "4.26.0", + "@rollup/rollup-win32-x64-msvc": "4.26.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-applescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", + "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/rx": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", + "integrity": "sha512-CiaiuN6gapkdl+cZUr67W6I8jquN4lkak3vtIsIWCl4XIPP8ffsoyN6/+PuGXnQy8Cu8W2y9Xxh31Rq4M6wUug==", + "license": "Apache-2.0" + }, + "node_modules/rxjs": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-identifier": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/safe-identifier/-/safe-identifier-0.4.2.tgz", + "integrity": "sha512-6pNbSMW6OhAi9j+N8V+U715yBQsaWJ7eyEUaOrawX+isg5ZxhUlV1NipNtgaKHmFGiABwt+ZF04Ii+3Xjkg+8w==" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/sass": { + "version": "1.80.7", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.80.7.tgz", + "integrity": "sha512-MVWvN0u5meytrSjsU7AWsbhoXi1sc58zADXFllfZzbsBT1GHjjar6JwBINYPRrkx/zqnQ6uqbQuHgE95O+C+eQ==", + "dev": true, + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.0.2", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" + } + }, + "node_modules/sass-loader": { + "version": "16.0.3", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.3.tgz", + "integrity": "sha512-gosNorT1RCkuCMyihv6FBRR7BMV06oKRAs+l4UMp1mlcVg9rWN6KMmUj3igjQwmYys4mDP3etEYJgiHRbgHCHA==", + "dev": true, + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "node-sass": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/sax": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", + "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", + "dev": true, + "optional": true + }, + "node_modules/schema-utils": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", + "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/schema-utils/node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", + "dev": true + }, + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", + "dev": true, + "dependencies": { + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.6.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", + "integrity": "sha512-oVekP1cKtI+CTDvHWYFUcMtsK/00wmAEfyqKfNdARm8u1wNVhSgaX7A8d4UuIlUI5e84iEwOhs7ZPYRmzU9U6A==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/send/node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/send/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==" + }, + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/serve-static/node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/server-destroy": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/server-destroy/-/server-destroy-1.0.1.tgz", + "integrity": "sha512-rb+9B5YBIEzYcD6x2VKidaa+cqYBJQKnU4oe4E3ANwRRN56yk/ua1YCJT1n21NTS8w6CcOclAKNP3PhdCXKYtQ==", + "license": "ISC" + }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "engines": { + "node": ">=8" + } + }, + "node_modules/shell-quote": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/sigstore": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.0.0.tgz", + "integrity": "sha512-PHMifhh3EN4loMcHCz6l3v/luzgT3za+9f8subGgeMNjbJjzH4Ij/YoX3Gvu+kaouJRIlVdTHHCREADYf+ZteA==", + "dev": true, + "dependencies": { + "@sigstore/bundle": "^3.0.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/sign": "^3.0.0", + "@sigstore/tuf": "^3.0.0", + "@sigstore/verify": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/slash": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", + "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/slice-ansi": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", + "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^6.0.0", + "is-fullwidth-code-point": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-client": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.1.tgz", + "integrity": "sha512-hJVXfu3E28NmzGk8o1sHhN3om52tRvwYeidbj7xKy2eIIse5IoKX3USlS6Tqt3BHAtflLIkCQBkzVrEEfWUyYQ==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.6.1", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", + "dev": true, + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" + } + }, + "node_modules/source-map-loader/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.21", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.21.tgz", + "integrity": "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg==", + "dev": true + }, + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" + } + }, + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, + "node_modules/ssri": { + "version": "12.0.0", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", + "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-throttle": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/stream-throttle/-/stream-throttle-0.1.3.tgz", + "integrity": "sha512-889+B9vN9dq7/vLbGyuHeZ6/ctf5sNuGWsDy89uNxkFTAgzy0eK7+w5fL3KLNRTkLle7EgZGvHUphZW0Q26MnQ==", + "license": "BSD-3-Clause", + "dependencies": { + "commander": "^2.2.0", + "limiter": "^1.0.5" + }, + "bin": { + "throttleproxy": "bin/throttleproxy.js" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/stream-throttle/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "license": "MIT" + }, + "node_modules/stream-transform": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-3.3.3.tgz", + "integrity": "sha512-dALXrXe+uq4aO5oStdHKlfCM/b3NBdouigvxVPxCdrMRAU6oHh3KNss20VbTPQNQmjAHzZGKGe66vgwegFEIog==" + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/streamroller/node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/streamroller/node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/string-width-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/symbol-observable": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-4.0.0.tgz", + "integrity": "sha512-b19dMThMV4HVFynSAM1++gBHAbk2Tc/osgLIBZMKsyqh34jb2e8Os7T6ZW/Bt3pJFdBTd2JwAnAAEQV7rSNvcQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/tapable": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.1.tgz", + "integrity": "sha512-GNzQvQTOIP6RyTfE2Qxb8ZVlNmw0n88vp1szwWRimP02mnTsx3Wtn5qRdqY9w2XduFNUgvOwhNnQsjwCp+kqaQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true + }, + "node_modules/terser": { + "version": "5.36.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.36.0.tgz", + "integrity": "sha512-IYV9eNMuFAV4THUspIRXkLakHnV6XO7FEdtKjf/mDyrnqUg9LnlOn6/RwRvM9SZjR4GUq8Nk8zj67FzVARr74w==", + "dev": true, + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.11", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.11.tgz", + "integrity": "sha512-RVCsMfuD0+cTt3EwX8hSl2Ks56EbFHWmhluwcqoPKtBnfjiT6olaq7PRIRfhyU8nnC2MrnDrBLfrD/RGE+cVXQ==", + "dev": true, + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "serialize-javascript": "^6.0.2", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/terser/node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true + }, + "node_modules/thingies": { + "version": "1.21.0", + "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", + "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", + "dev": true, + "engines": { + "node": ">=10.18" + }, + "peerDependencies": { + "tslib": "^2" + } + }, + "node_modules/thunky": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", + "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", + "dev": true + }, + "node_modules/tmp": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", + "dev": true, + "dependencies": { + "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tree-dump": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", + "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", + "dev": true, + "engines": { + "node": ">=10.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + }, + "peerDependencies": { + "tslib": "2" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.0.0.tgz", + "integrity": "sha512-xCt/TOAc+EOHS1XPnijD3/yzpH6qg2xppZO1YDqGoVsNXfQfzHpOdNuXwrwOU8u4ITXJyDCTyt8w5g1sZv9ynQ==", + "dev": true, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tuf-js": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", + "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", + "dev": true, + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-assert": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/typed-assert/-/typed-assert-1.0.9.tgz", + "integrity": "sha512-KNNZtayBCtmnNmbo5mG47p1XsCyrx6iVqomjcZnec/1Y5GGARaxPs6r49RnSPeUP3YjNYiU9sQHAtY4BBvnZwg==", + "dev": true + }, + "node_modules/typescript": { + "version": "5.6.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz", + "integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==", + "devOptional": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.0.tgz", + "integrity": "sha512-Xq2rRjn6tzVpAyHr3+nmSg1/9k9aIHnJ2iZeOH7cfGOWqTkXTm3kwpQglEuLGdNrYvPF+2gtAs+/KF5rjVo+WQ==", + "dev": true, + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.18.0", + "@typescript-eslint/parser": "8.18.0", + "@typescript-eslint/utils": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/scope-manager": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.0.tgz", + "integrity": "sha512-PNGcHop0jkK2WVYGotk/hxj+UFLhXtGPiGtiaWgVBVP1jhMoMCHlTyJA+hEj4rszoSdLTK3fN4oOatrL0Cp+Xw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/types": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.0.tgz", + "integrity": "sha512-FNYxgyTCAnFwTrzpBGq+zrnoTO4x0c1CKYY5MuUTzpScqmY5fmsh2o3+57lqdI3NZucBDCzDgdEbIaNfAjAHQA==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/typescript-estree": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.0.tgz", + "integrity": "sha512-rqQgFRu6yPkauz+ms3nQpohwejS8bvgbPyIDq13cgEDbkXt4LH4OkDMT0/fN1RUtzG8e8AKJyDBoocuQh8qNeg==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/visitor-keys": "8.18.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^1.3.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/utils": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.0.tgz", + "integrity": "sha512-p6GLdY383i7h5b0Qrfbix3Vc3+J2k6QWw6UMUeY5JGfm3C5LbZ4QIZzJNoNOfgyRe0uuYKjvVOsO/jD4SJO+xg==", + "dev": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@typescript-eslint/scope-manager": "8.18.0", + "@typescript-eslint/types": "8.18.0", + "@typescript-eslint/typescript-estree": "8.18.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" + } + }, + "node_modules/typescript-eslint/node_modules/@typescript-eslint/visitor-keys": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.0.tgz", + "integrity": "sha512-pCh/qEA8Lb1wVIqNvBke8UaRjJ6wrAWkJO5yyIbs8Yx6TNGYyfNjOo61tLv+WwLvoLPp4BQ8B7AHKijl8NGUfw==", + "dev": true, + "dependencies": { + "@typescript-eslint/types": "8.18.0", + "eslint-visitor-keys": "^4.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/typescript-eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/typescript-eslint/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" + }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-filename": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", + "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", + "dev": true, + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/unique-slug": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", + "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "peer": true, + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", + "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uri-js/node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/validate-npm-package-name": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", + "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "dev": true, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/vite": { + "version": "5.4.11", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.11.tgz", + "integrity": "sha512-c7jFQRklXua0mTzneGW9QVyxFjUgwcihC4bXEtujIo2ouWCe1Ajt/amn2PCxYnhYfd5k09JX3SB7OYWFKYqj8Q==", + "dev": true, + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/watchpack": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.2.tgz", + "integrity": "sha512-TnbFSbcOCcDgjZ4piURLCbJ3nJhznVh9kw6F6iokjiFPl8ONxe9A6nMDVXDiNbrSfLILs6vB07F7wLBrwPYzJw==", + "dev": true, + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "dev": true, + "dependencies": { + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/wcwidth": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", + "integrity": "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg==", + "dependencies": { + "defaults": "^1.0.3" + } + }, + "node_modules/weak-lru-cache": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", + "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", + "dev": true, + "optional": true + }, + "node_modules/webpack": { + "version": "5.96.1", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.96.1.tgz", + "integrity": "sha512-l2LlBSvVZGhL4ZrPwyr8+37AunkcYj5qh8o6u2/2rzoPc8gxFJkLj1WxNgooi9pnoc06jh0BjuXnamM4qlujZA==", + "dev": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.6", + "@webassemblyjs/ast": "^1.12.1", + "@webassemblyjs/wasm-edit": "^1.12.1", + "@webassemblyjs/wasm-parser": "^1.12.1", + "acorn": "^8.14.0", + "browserslist": "^4.24.0", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.17.1", + "es-module-lexer": "^1.2.1", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.2.0", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^3.2.0", + "tapable": "^2.1.1", + "terser-webpack-plugin": "^5.3.10", + "watchpack": "^2.4.1", + "webpack-sources": "^3.2.3" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-middleware": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^4.6.0", + "mime-types": "^2.1.31", + "on-finished": "^2.4.1", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.1.0.tgz", + "integrity": "sha512-aQpaN81X6tXie1FoOB7xlMfCsN19pSvRAeYUHOdFWOlhpQ/LlbfTqYwwmEDFV0h8GGuqmCmKmT+pxcUV/Nt2gQ==", + "dev": true, + "dependencies": { + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "express": "^4.19.2", + "graceful-fs": "^4.2.6", + "html-entities": "^2.4.0", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.7.tgz", + "integrity": "sha512-fgVY8AV7qU7z/MmXJ/rxwbrtQH4jBQ9m7kp3llF0liB7glmFeVZFBepQb32T3y8n8k2+AEYuMPCpinYW+/CuRA==", + "dev": true, + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/webpack-dev-server/node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.2.3.tgz", + "integrity": "sha512-/DyMEOrDgLKKIG0fmvtz+4dUX/3Ghozwgm6iPp8KRhvn+eQf9+Q7GWxVNMk3+uCPWfdXYC4ExGBckIXdFEfH1w==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack-subresource-integrity": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/webpack-subresource-integrity/-/webpack-subresource-integrity-5.1.0.tgz", + "integrity": "sha512-sacXoX+xd8r4WKsy9MvH/q/vBtEHr86cpImXwyg74pFIpERKt6FmB8cXpeuh0ZLgclOlHI4Wcll7+R5L02xk9Q==", + "dev": true, + "dependencies": { + "typed-assert": "^1.0.8" + }, + "engines": { + "node": ">= 12" + }, + "peerDependencies": { + "html-webpack-plugin": ">= 5.0.0-beta.1 < 6", + "webpack": "^5.12.0" + }, + "peerDependenciesMeta": { + "html-webpack-plugin": { + "optional": true + } + } + }, + "node_modules/webpack/node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", + "dev": true, + "peerDependencies": { + "ajv": "^6.9.1" + } + }, + "node_modules/webpack/node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/webpack/node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/webpack/node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dev": true, + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT" + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/wrap-ansi-cjs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==" + }, + "node_modules/ws": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", + "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz", + "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==" + }, + "node_modules/yaml": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", + "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors-cjs": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", + "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zone.js": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", + "integrity": "sha512-9oxn0IIjbCZkJ67L+LkhYWRyAy7axphb3VgE2MBDlOqnmHMPWGYMxJxBYFueFq/JGY2GMwS0rU+UCLunEmy5UA==" + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/package.json b/guacamole/src/main/guacamole-frontend/package.json new file mode 100644 index 0000000000..f548892bd4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/package.json @@ -0,0 +1,69 @@ +{ + "name": "frontend", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve", + "build": "ng build", + "build:app": "ng build guacamole-frontend --base-href /guacamole/", + "test": "ng test", + "test-guacamole-frontend-lib:ci": "ng test --karma-config projects/guacamole-frontend-lib/karma.conf.ci.js guacamole-frontend-lib", + "test-guacamole-frontend:ci": "ng test --karma-config projects/guacamole-frontend/karma.conf.ci.js guacamole-frontend", + "lint": "ng lint", + "run:all": "node node_modules/@angular-architects/module-federation/src/server/mf-dev-server.js" + }, + "private": true, + "dependencies": { + "@angular-architects/native-federation": "^19.0.5", + "@angular/animations": "^19.0.6", + "@angular/common": "^19.0.6", + "@angular/compiler": "^19.0.6", + "@angular/core": "^19.0.6", + "@angular/forms": "^19.0.6", + "@angular/platform-browser": "^19.0.6", + "@angular/platform-browser-dynamic": "^19.0.6", + "@angular/router": "^19.0.6", + "@ngneat/transloco": "^4.2.2", + "@ngneat/transloco-messageformat": "^4.1.0", + "@simonwep/pickr": "^1.8.2", + "@softarc/native-federation-node": "^2.0.10", + "angular-expressions": "^1.1.9", + "csv": "^6.2.5", + "d3-path": "^3.1.0", + "d3-shape": "^3.2.0", + "es-module-shims": "^1.5.12", + "file-saver": "^2.0.5", + "fuzzysort": "^3.0.2", + "jquery": "^3.6.4", + "jstz": "^2.1.1", + "lodash": "^4.17.21", + "rxjs": "~7.8.0", + "tslib": "^2.3.0", + "yaml": "^2.2.2", + "zone.js": "~0.15.0" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^19.0.7", + "@angular/cli": "~19.0.7", + "@angular/compiler-cli": "^19.0.6", + "@types/d3-path": "^3.1.0", + "@types/d3-shape": "^3.1.7", + "@types/file-saver": "^2.0.5", + "@types/guacamole-common-js": "^1.5.3", + "@types/jasmine": "~5.1.0", + "@types/jquery": "^3.5.6", + "@types/lodash": "^4.14.194", + "angular-eslint": "19.0.2", + "eslint": "^9.16.0", + "jasmine-core": "~5.4.0", + "karma": "~6.4.0", + "karma-coverage": "~2.2.0", + "karma-firefox-launcher": "~2.1.2", + "karma-jasmine": "~5.1.0", + "karma-jasmine-html-reporter": "~2.1.0", + "ng-packagr": "^19.0.1", + "ngx-build-plus": "^19.0.0", + "typescript": "~5.6.3", + "typescript-eslint": "8.18.0" + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/README.md b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/README.md new file mode 100644 index 0000000000..72656a638a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/README.md @@ -0,0 +1,8 @@ +# GuacamoleFrontendExtLib + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.1. + +## Build + +Run `ng build guacamole-frontend-ext-lib` to build the project. The build artifacts will be stored in the `dist/` +directory. diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/eslint.config.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/eslint.config.js new file mode 100644 index 0000000000..78efcf108e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/eslint.config.js @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// @ts-check +const tseslint = require('typescript-eslint'); +const rootConfig = require('../../eslint.config.js'); + +module.exports = tseslint.config( + ...rootConfig, + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + 'type': 'attribute', + 'prefix': 'guac', + 'style': 'camelCase' + } + ], + '@angular-eslint/component-selector': [ + 'error', + { + 'type': 'element', + 'prefix': 'guac', + 'style': 'kebab-case' + } + ] + } + }, + { + files: ['**/*.html'], + 'rules': {} + } +); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/ng-package.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/ng-package.json new file mode 100644 index 0000000000..bfcb278d4b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/ng-package.json @@ -0,0 +1,7 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/guacamole-frontend-ext-lib", + "lib": { + "entryFile": "src/public-api.ts" + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/package.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/package.json new file mode 100644 index 0000000000..ee9715a9a0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "guacamole-frontend-ext-lib", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "16.1.1", + "@angular/core": "16.1.1" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "sideEffects": false +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/BootsrapExtensionFunction.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/BootsrapExtensionFunction.ts new file mode 100644 index 0000000000..da9e773624 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/BootsrapExtensionFunction.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injector } from '@angular/core'; +import { Routes } from '@angular/router'; + +/** + * Function definition for the extension bootstrap function. This function + * is called by the frontend to initialize the extension. The injector + * can be used together with runInInjectionContext() and + * inject() to inject required dependencies. The function + * must return the routes of the extension which will be added to the + * routes of the frontend. If no routes should be added, return an empty + * array. + * + * @example + * export const extensionConfig: BootsrapExtensionFunction = (injector: Injector): Routes => { + * runInInjectionContext(injector, () => { + * // Register a new field type + * const fieldTypeService = inject(FieldTypeService); + * fieldTypeService.registerFieldType(...); + * }); + * + * // Return the routes of the extension + * return routes; + * } + * + */ +export type BootsrapExtensionFunction = (injector: Injector) => Routes; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormField.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormField.ts new file mode 100644 index 0000000000..80913ce911 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormField.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * An interface for the object returned by REST API calls when representing the data + * associated with a field or configuration parameter. + */ +export interface FormField { + + /** + * The name which uniquely identifies this parameter. + */ + name: string; + + /** + * The type string defining which values this parameter may contain, + * as well as what properties are applicable. + */ + type: string; + + /** + * All possible legal values for this parameter. + */ + options?: string[]; + + /** + * A message which can be translated using the translation service, + * consisting of a translation key and optional set of substitution + * variables. + */ + translatableMessage?: unknown; + + /** + * The URL to which the user should be redirected when a field of type + * REDIRECT is displayed. + */ + redirectUrl?: string; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormFieldComponentData.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormFieldComponentData.ts new file mode 100644 index 0000000000..e07dbd597f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/FormFieldComponentData.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { FormControl } from '@angular/forms'; +import { FormField } from './FormField'; + +/** + * An interface which defines the basic properties of a form field component. + * Used by the form service to generate form field components. + */ +export interface FormFieldComponentData { + + /** + * The translation namespace of the translation strings that will + * be generated for this field. This namespace is absolutely + * required. If this namespace is omitted, all generated + * translation strings will be placed within the MISSING_NAMESPACE + * namespace, as a warning. + */ + namespace: string | undefined; + + /** + * The field to display. + */ + field: FormField; + + /** + * The form control which contains this fields current value. When this + * field changes, the property will be updated accordingly. + */ + control?: FormControl; + + /** + * Whether this field should be rendered as disabled. By default, + * form fields are enabled. + */ + disabled: boolean; + + /** + * Whether this field should be focused. + */ + focused: boolean; + + /** + * An ID value which is reasonably likely to be unique relative to + * other elements on the page. This ID should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + fieldId?: string; + + /** + * The managed client associated with this form field, if any. + */ + client?: any; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/field-type.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/field-type.service.ts new file mode 100644 index 0000000000..c7395e5541 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/lib/form/field-type.service.ts @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable, Type } from '@angular/core'; +import { FormFieldComponentData } from './FormFieldComponentData'; + +/** + * Service for managing all registered field types. + */ +@Injectable({ + providedIn: 'root' +}) +export class FieldTypeService { + + /** + * Map of all registered field type definitions by name. + */ + private fieldTypes: Record> = {}; + + /** + * Registers a new field type under the given name. + * + * @param fieldTypeName + * The name which uniquely identifies the field type being registered. + * + * @param component + * The component type to associate with the given name. + */ + registerFieldType(fieldTypeName: string, component: Type): void { + + // Store field type + this.fieldTypes[fieldTypeName] = component; + + } + + /** + * Returns the component associated with the given name, if any. + * + * @param fieldTypeName + * The name which uniquely identifies the field type to retrieve. + */ + getComponent(fieldTypeName: string): Type | null { + return this.fieldTypes[fieldTypeName] || null; + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/public-api.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/public-api.ts new file mode 100644 index 0000000000..c301955200 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/src/public-api.ts @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* + * Public API Surface of guacamole-frontend-ext-lib + */ + +export * from './lib/BootsrapExtensionFunction'; + +export * from './lib/form/FormField'; +export * from './lib/form/FormFieldComponentData'; +export * from './lib/form/field-type.service'; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.json new file mode 100644 index 0000000000..f326bdb7a0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [] + }, + "exclude": [ + "**/*.spec.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.prod.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.prod.json new file mode 100644 index 0000000000..560c23915f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.spec.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.spec.json new file mode 100644 index 0000000000..06f042c2a5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-ext-lib/tsconfig.spec.json @@ -0,0 +1,14 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine" + ] + }, + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/README.md b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/README.md new file mode 100644 index 0000000000..83fe5b001b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/README.md @@ -0,0 +1,7 @@ +# GuacamoleFrontendLib + +This library was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.1.1. + +## Build + +Run `ng build guacamole-frontend-lib` to build the project. The build artifacts will be stored in the `dist/` directory. diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/eslint.config.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/eslint.config.js new file mode 100644 index 0000000000..78efcf108e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/eslint.config.js @@ -0,0 +1,51 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// @ts-check +const tseslint = require('typescript-eslint'); +const rootConfig = require('../../eslint.config.js'); + +module.exports = tseslint.config( + ...rootConfig, + { + files: ['**/*.ts'], + rules: { + '@angular-eslint/directive-selector': [ + 'error', + { + 'type': 'attribute', + 'prefix': 'guac', + 'style': 'camelCase' + } + ], + '@angular-eslint/component-selector': [ + 'error', + { + 'type': 'element', + 'prefix': 'guac', + 'style': 'kebab-case' + } + ] + } + }, + { + files: ['**/*.html'], + 'rules': {} + } +); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.ci.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.ci.js new file mode 100644 index 0000000000..8fb443ddb1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.ci.js @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Karma configuration file for CI. +// Run the tests once in headless Firefox and exit. +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-firefox-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + random: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/guacamole-frontend-lib'), + subdir: '.', + reporters: [ + {type: 'html'}, + {type: 'text-summary'} + ] + }, + reporters: ['progress', 'kjhtml'], + + // Run the tests once and exit + singleRun: true, + + // Disable automatic test running on changed files + autoWatch: false, + + // Use a headless firefox browser to run the tests + browsers: ['FirefoxHeadless'], + customLaunchers: { + 'FirefoxHeadless': { + base: 'Firefox', + flags: [ + '--headless' + ] + } + } + }); +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.js new file mode 100644 index 0000000000..09dcc4480b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/karma.conf.js @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Karma configuration file for development. +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-firefox-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + random: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/guacamole-frontend-lib'), + subdir: '.', + reporters: [ + {type: 'html'}, + {type: 'text-summary'} + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Firefox'], + restartOnFileChange: true + }); +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/ng-package.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/ng-package.json new file mode 100644 index 0000000000..581c980503 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/ng-package.json @@ -0,0 +1,19 @@ +{ + "$schema": "../../node_modules/ng-packagr/ng-package.schema.json", + "dest": "../../dist/guacamole-frontend-lib", + "lib": { + "entryFile": "src/public-api.ts" + }, + "assets": [ + { + "glob": "**/*", + "input": "src/assets", + "output": "assets" + }, + { + "glob": "**/*.css", + "input": "src/lib", + "output": "assets/styles" + } + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/package.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/package.json new file mode 100644 index 0000000000..3689b01404 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/package.json @@ -0,0 +1,12 @@ +{ + "name": "guacamole-frontend-lib", + "version": "0.0.1", + "peerDependencies": { + "@angular/common": "^16.1.0", + "@angular/core": "^16.1.0" + }, + "dependencies": { + "tslib": "^2.3.0" + }, + "sideEffects": false +} diff --git a/guacamole/src/main/frontend/src/layouts/de-de-qwertz.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/de-de-qwertz.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/de-de-qwertz.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/de-de-qwertz.json diff --git a/guacamole/src/main/frontend/src/layouts/en-us-qwerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/en-us-qwerty.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/en-us-qwerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/en-us-qwerty.json diff --git a/guacamole/src/main/frontend/src/layouts/es-es-qwerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/es-es-qwerty.json similarity index 97% rename from guacamole/src/main/frontend/src/layouts/es-es-qwerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/es-es-qwerty.json index e88c6cda47..d851eb1d64 100644 --- a/guacamole/src/main/frontend/src/layouts/es-es-qwerty.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/es-es-qwerty.json @@ -1,455 +1,455 @@ -{ - - "language" : "es_ES", - "type" : "qwerty", - "width" : 23, - - "keys" : { - - "Esc" : 65307, - "F1" : 65470, - "F2" : 65471, - "F3" : 65472, - "F4" : 65473, - "F5" : 65474, - "F6" : 65475, - "F7" : 65476, - "F8" : 65477, - "F9" : 65478, - "F10" : 65479, - "F11" : 65480, - "F12" : 65481, - - "Space" : " ", - - "Back" : [{ - "title" : "⟵", - "keysym" : 65288 - }], - "Tab" : [{ - "title" : "Tab ↹", - "keysym" : 65289 - }], - "Enter" : [{ - "title" : "↵", - "keysym" : 65293 - }], - "Home" : [{ - "title" : "Inicio", - "keysym" : 65360 - }], - "PgUp" : [{ - "title" : "RePág ↑", - "keysym" : 65365 - }], - "PgDn" : [{ - "title" : "AvPág ↓", - "keysym" : 65366 - }], - "End" : [{ - "title" : "Fin", - "keysym" : 65367 - }], - "Ins" : [{ - "title" : "Ins", - "keysym" : 65379 - }], - "Del" : [{ - "title" : "Supr", - "keysym" : 65535 - }], - - "Left" : [{ - "title" : "←", - "keysym" : 65361 - }], - "Up" : [{ - "title" : "↑", - "keysym" : 65362 - }], - "Right" : [{ - "title" : "→", - "keysym" : 65363 - }], - "Down" : [{ - "title" : "↓", - "keysym" : 65364 - }], - - "Menu" : [{ - "title" : "Menu", - "keysym" : 65383 - }], - "LShift" : [{ - "title" : "Shift", - "modifier" : "shift", - "keysym" : 65505 - }], - "RShift" : [{ - "title" : "Shift", - "modifier" : "shift", - "keysym" : 65506 - }], - "LCtrl" : [{ - "title" : "Ctrl", - "modifier" : "control", - "keysym" : 65507 - }], - "RCtrl" : [{ - "title" : "Ctrl", - "modifier" : "control", - "keysym" : 65508 - }], - "Caps" : [{ - "title" : "Caps", - "modifier" : "caps", - "keysym" : 65509 - }], - "LAlt" : [{ - "title" : "Alt", - "modifier" : "alt", - "keysym" : 65513 - }], - "AltGr" : [{ - "title" : "AltGr", - "modifier" : "alt-gr", - "keysym" : 65027 - }], - "Meta" : [{ - "title" : "Meta", - "modifier" : "meta", - "keysym" : 65511 - }], - - "º" : [ - { "title" : "º", "requires" : [ ] }, - { "title" : "ª", "requires" : [ "shift" ] }, - { "title" : "\\", "requires" : [ "alt-gr" ] } - ], - "1" : [ - { "title" : "1", "requires" : [ ] }, - { "title" : "!", "requires" : [ "shift" ] }, - { "title" : "|", "requires" : [ "alt-gr" ] } - ], - "2" : [ - { "title" : "2", "requires" : [ ] }, - { "title" : "\"", "requires" : [ "shift" ] }, - { "title" : "@", "requires" : [ "alt-gr" ] } - ], - "3" : [ - { "title" : "3", "requires" : [ ] }, - { "title" : ".", "requires" : [ "shift" ] }, - { "title" : "#", "requires" : [ "alt-gr" ] } - ], - "4" : [ - { "title" : "4", "requires" : [ ] }, - { "title" : "$", "requires" : [ "shift" ] }, - { "title" : "~", "requires" : [ "alt-gr" ] } - ], - "5" : [ - { "title" : "5", "requires" : [ ] }, - { "title" : "%", "requires" : [ "shift" ] }, - { "title" : "€", "requires" : [ "alt-gr" ] } - ], - "6" : [ - { "title" : "6", "requires" : [ ] }, - { "title" : "&", "requires" : [ "shift" ] }, - { "title" : "¬", "requires" : [ "alt-gr" ] } - ], - "7" : [ - { "title" : "7", "requires" : [ ] }, - { "title" : "/", "requires" : [ "shift" ] } - ], - "8" : [ - { "title" : "8", "requires" : [ ] }, - { "title" : "(", "requires" : [ "shift" ] } - ], - "9" : [ - { "title" : "9", "requires" : [ ] }, - { "title" : ")", "requires" : [ "shift" ] } - ], - "0" : [ - { "title" : "0", "requires" : [ ] }, - { "title" : "=", "requires" : [ "shift" ] } - ], - "'" : [ - { "title" : "'", "requires" : [ ] }, - { "title" : "?", "requires" : [ "shift" ] } - ], - "¡" : [ - { "title" : "¡", "requires" : [ ] }, - { "title" : "¿", "requires" : [ "shift" ] } - ], - - "q" : [ - { "title" : "q", "requires" : [ ] }, - { "title" : "Q", "requires" : [ "caps" ] }, - { "title" : "Q", "requires" : [ "shift" ] }, - { "title" : "q", "requires" : [ "caps", "shift" ] } - ], - "w" : [ - { "title" : "w", "requires" : [ ] }, - { "title" : "W", "requires" : [ "caps" ] }, - { "title" : "W", "requires" : [ "shift" ] }, - { "title" : "w", "requires" : [ "caps", "shift" ] } - ], - "e" : [ - { "title" : "e", "requires" : [ ] }, - { "title" : "E", "requires" : [ "caps" ] }, - { "title" : "E", "requires" : [ "shift" ] }, - { "title" : "e", "requires" : [ "caps", "shift" ] }, - { "title" : "€", "requires" : [ "alt-gr" ] } - ], - "r" : [ - { "title" : "r", "requires" : [ ] }, - { "title" : "R", "requires" : [ "caps" ] }, - { "title" : "R", "requires" : [ "shift" ] }, - { "title" : "r", "requires" : [ "caps", "shift" ] } - ], - "t" : [ - { "title" : "t", "requires" : [ ] }, - { "title" : "T", "requires" : [ "caps" ] }, - { "title" : "T", "requires" : [ "shift" ] }, - { "title" : "t", "requires" : [ "caps", "shift" ] } - ], - "y" : [ - { "title" : "y", "requires" : [ ] }, - { "title" : "Y", "requires" : [ "caps" ] }, - { "title" : "Y", "requires" : [ "shift" ] }, - { "title" : "y", "requires" : [ "caps", "shift" ] } - ], - "u" : [ - { "title" : "u", "requires" : [ ] }, - { "title" : "U", "requires" : [ "caps" ] }, - { "title" : "U", "requires" : [ "shift" ] }, - { "title" : "u", "requires" : [ "caps", "shift" ] } - ], - "i" : [ - { "title" : "i", "requires" : [ ] }, - { "title" : "I", "requires" : [ "caps" ] }, - { "title" : "I", "requires" : [ "shift" ] }, - { "title" : "i", "requires" : [ "caps", "shift" ] } - ], - "o" : [ - { "title" : "o", "requires" : [ ] }, - { "title" : "O", "requires" : [ "caps" ] }, - { "title" : "O", "requires" : [ "shift" ] }, - { "title" : "o", "requires" : [ "caps", "shift" ] } - ], - "p" : [ - { "title" : "p", "requires" : [ ] }, - { "title" : "P", "requires" : [ "caps" ] }, - { "title" : "P", "requires" : [ "shift" ] }, - { "title" : "p", "requires" : [ "caps", "shift" ] } - ], - "`" : [ - { "title" : "`", "requires" : [ ] }, - { "title" : "`", "requires" : [ "caps" ] }, - { "title" : "^", "requires" : [ "shift" ] }, - { "title" : "^", "requires" : [ "caps", "shift" ] }, - { "title" : "[", "requires" : [ "alt-gr" ] } - ], - "+" : [ - { "title" : "+", "requires" : [ ] }, - { "title" : "+", "requires" : [ "caps" ] }, - { "title" : "*", "requires" : [ "shift" ] }, - { "title" : "*", "requires" : [ "caps", "shift" ] }, - { "title" : "]", "requires" : [ "alt-gr" ] } - ], - "a" : [ - { "title" : "a", "requires" : [ ] }, - { "title" : "A", "requires" : [ "caps" ] }, - { "title" : "A", "requires" : [ "shift" ] }, - { "title" : "a", "requires" : [ "caps", "shift" ] } - ], - "s" : [ - { "title" : "s", "requires" : [ ] }, - { "title" : "S", "requires" : [ "caps" ] }, - { "title" : "S", "requires" : [ "shift" ] }, - { "title" : "s", "requires" : [ "caps", "shift" ] } - ], - "d" : [ - { "title" : "d", "requires" : [ ] }, - { "title" : "D", "requires" : [ "caps" ] }, - { "title" : "D", "requires" : [ "shift" ] }, - { "title" : "d", "requires" : [ "caps", "shift" ] } - ], - "f" : [ - { "title" : "f", "requires" : [ ] }, - { "title" : "F", "requires" : [ "caps" ] }, - { "title" : "F", "requires" : [ "shift" ] }, - { "title" : "f", "requires" : [ "caps", "shift" ] } - ], - "g" : [ - { "title" : "g", "requires" : [ ] }, - { "title" : "G", "requires" : [ "caps" ] }, - { "title" : "G", "requires" : [ "shift" ] }, - { "title" : "g", "requires" : [ "caps", "shift" ] } - ], - "h" : [ - { "title" : "h", "requires" : [ ] }, - { "title" : "H", "requires" : [ "caps" ] }, - { "title" : "H", "requires" : [ "shift" ] }, - { "title" : "h", "requires" : [ "caps", "shift" ] } - ], - "j" : [ - { "title" : "j", "requires" : [ ] }, - { "title" : "J", "requires" : [ "caps" ] }, - { "title" : "J", "requires" : [ "shift" ] }, - { "title" : "j", "requires" : [ "caps", "shift" ] } - ], - "k" : [ - { "title" : "k", "requires" : [ ] }, - { "title" : "K", "requires" : [ "caps" ] }, - { "title" : "K", "requires" : [ "shift" ] }, - { "title" : "k", "requires" : [ "caps", "shift" ] } - ], - "l" : [ - { "title" : "l", "requires" : [ ] }, - { "title" : "L", "requires" : [ "caps" ] }, - { "title" : "L", "requires" : [ "shift" ] }, - { "title" : "l", "requires" : [ "caps", "shift" ] } - ], - "ñ" : [ - { "title" : "ñ", "requires" : [ ] }, - { "title" : "Ñ", "requires" : [ "caps" ] }, - { "title" : "Ñ", "requires" : [ "shift" ] }, - { "title" : "ñ", "requires" : [ "caps", "shift" ] } - ], - "´" : [ - { "title" : "´", "requires" : [ ], "keysym" : 65105 }, - { "title" : "´", "requires" : [ "caps" ] }, - { "title" : "¨", "requires" : [ "shift" ] }, - { "title" : "¨", "requires" : [ "caps", "shift" ] }, - { "title" : "{", "requires" : [ "alt-gr" ] } - ], - "ç" : [ - { "title" : "ç", "requires" : [ ] }, - { "title" : "Ç", "requires" : [ "caps" ] }, - { "title" : "Ç", "requires" : [ "shift" ] }, - { "title" : "ç", "requires" : [ "caps", "shift" ] }, - { "title" : "}", "requires" : [ "alt-gr" ] } - ], - "<" : [ - { "title" : "<", "requires" : [ ] }, - { "title" : "<", "requires" : [ "caps" ] }, - { "title" : ">", "requires" : [ "shift" ] }, - { "title" : ">", "requires" : [ "caps", "shift" ] } - ], - - "z" : [ - { "title" : "z", "requires" : [ ] }, - { "title" : "Z", "requires" : [ "caps" ] }, - { "title" : "Z", "requires" : [ "shift" ] }, - { "title" : "z", "requires" : [ "caps", "shift" ] } - ], - "x" : [ - { "title" : "x", "requires" : [ ] }, - { "title" : "X", "requires" : [ "caps" ] }, - { "title" : "X", "requires" : [ "shift" ] }, - { "title" : "x", "requires" : [ "caps", "shift" ] } - ], - "c" : [ - { "title" : "c", "requires" : [ ] }, - { "title" : "C", "requires" : [ "caps" ] }, - { "title" : "C", "requires" : [ "shift" ] }, - { "title" : "c", "requires" : [ "caps", "shift" ] } - ], - "v" : [ - { "title" : "v", "requires" : [ ] }, - { "title" : "V", "requires" : [ "caps" ] }, - { "title" : "V", "requires" : [ "shift" ] }, - { "title" : "v", "requires" : [ "caps", "shift" ] } - ], - "b" : [ - { "title" : "b", "requires" : [ ] }, - { "title" : "B", "requires" : [ "caps" ] }, - { "title" : "B", "requires" : [ "shift" ] }, - { "title" : "b", "requires" : [ "caps", "shift" ] } - ], - "n" : [ - { "title" : "n", "requires" : [ ] }, - { "title" : "N", "requires" : [ "caps" ] }, - { "title" : "N", "requires" : [ "shift" ] }, - { "title" : "n", "requires" : [ "caps", "shift" ] } - ], - "m" : [ - { "title" : "m", "requires" : [ ] }, - { "title" : "M", "requires" : [ "caps" ] }, - { "title" : "M", "requires" : [ "shift" ] }, - { "title" : "m", "requires" : [ "caps", "shift" ] } - ], - "," : [ - { "title" : ",", "requires" : [ ] }, - { "title" : ";", "requires" : [ "shift" ] } - ], - "." : [ - { "title" : ".", "requires" : [ ] }, - { "title" : ":", "requires" : [ "shift" ] } - ], - "-" : [ - { "title" : "-", "requires" : [ ] }, - { "title" : "_", "requires" : [ "shift" ] } - ] - }, - - "layout" : [ - - [ "Esc", 0.8, "F1", "F2", "F3", "F4", - 0.8, "F5", "F6", "F7", "F8", - 0.8, "F9", "F10", "F11", "F12" ], - - [ 0.1 ], - - { - "main" : { - "alpha" : [ - - [ "º", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "¡", "Back" ], - [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "`", "+", 1, 0.6 ], - [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "´", "ç", "Enter" ], - [ "LShift", "<", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "RShift" ], - [ "LCtrl", "Meta", "LAlt", "Space", "AltGr", "Menu", "RCtrl" ] - - ], - - "movement" : [ - [ "Ins", "Home", "PgUp" ], - [ "Del", "End", "PgDn" ], - [ 1 ], - [ "Up" ], - [ "Left", "Down", "Right" ] - ] - } - } - - ], - - "keyWidths" : { - - "Back" : 2.3, - "Tab" : 1.75, - "\\" : 1.25, - "Caps" : 1.75, - "Enter" : 1.5, - "LShift" : 2.2, - "RShift" : 2.2, - - "LCtrl" : 1.6, - "Meta" : 1.6, - "LAlt" : 1.6, - "Space" : 6.4, - "AltGr" : 1.6, - "Menu" : 1.6, - "RCtrl" : 1.6, - - "Ins" : 1.6, - "Home" : 1.6, - "PgUp" : 1.6, - "Del" : 1.6, - "End" : 1.6, - "PgDn" : 1.6 - - } - -} +{ + + "language" : "es_ES", + "type" : "qwerty", + "width" : 23, + + "keys" : { + + "Esc" : 65307, + "F1" : 65470, + "F2" : 65471, + "F3" : 65472, + "F4" : 65473, + "F5" : 65474, + "F6" : 65475, + "F7" : 65476, + "F8" : 65477, + "F9" : 65478, + "F10" : 65479, + "F11" : 65480, + "F12" : 65481, + + "Space" : " ", + + "Back" : [{ + "title" : "⟵", + "keysym" : 65288 + }], + "Tab" : [{ + "title" : "Tab ↹", + "keysym" : 65289 + }], + "Enter" : [{ + "title" : "↵", + "keysym" : 65293 + }], + "Home" : [{ + "title" : "Inicio", + "keysym" : 65360 + }], + "PgUp" : [{ + "title" : "RePág ↑", + "keysym" : 65365 + }], + "PgDn" : [{ + "title" : "AvPág ↓", + "keysym" : 65366 + }], + "End" : [{ + "title" : "Fin", + "keysym" : 65367 + }], + "Ins" : [{ + "title" : "Ins", + "keysym" : 65379 + }], + "Del" : [{ + "title" : "Supr", + "keysym" : 65535 + }], + + "Left" : [{ + "title" : "←", + "keysym" : 65361 + }], + "Up" : [{ + "title" : "↑", + "keysym" : 65362 + }], + "Right" : [{ + "title" : "→", + "keysym" : 65363 + }], + "Down" : [{ + "title" : "↓", + "keysym" : 65364 + }], + + "Menu" : [{ + "title" : "Menu", + "keysym" : 65383 + }], + "LShift" : [{ + "title" : "Shift", + "modifier" : "shift", + "keysym" : 65505 + }], + "RShift" : [{ + "title" : "Shift", + "modifier" : "shift", + "keysym" : 65506 + }], + "LCtrl" : [{ + "title" : "Ctrl", + "modifier" : "control", + "keysym" : 65507 + }], + "RCtrl" : [{ + "title" : "Ctrl", + "modifier" : "control", + "keysym" : 65508 + }], + "Caps" : [{ + "title" : "Caps", + "modifier" : "caps", + "keysym" : 65509 + }], + "LAlt" : [{ + "title" : "Alt", + "modifier" : "alt", + "keysym" : 65513 + }], + "AltGr" : [{ + "title" : "AltGr", + "modifier" : "alt-gr", + "keysym" : 65027 + }], + "Meta" : [{ + "title" : "Meta", + "modifier" : "meta", + "keysym" : 65511 + }], + + "º" : [ + { "title" : "º", "requires" : [ ] }, + { "title" : "ª", "requires" : [ "shift" ] }, + { "title" : "\\", "requires" : [ "alt-gr" ] } + ], + "1" : [ + { "title" : "1", "requires" : [ ] }, + { "title" : "!", "requires" : [ "shift" ] }, + { "title" : "|", "requires" : [ "alt-gr" ] } + ], + "2" : [ + { "title" : "2", "requires" : [ ] }, + { "title" : "\"", "requires" : [ "shift" ] }, + { "title" : "@", "requires" : [ "alt-gr" ] } + ], + "3" : [ + { "title" : "3", "requires" : [ ] }, + { "title" : ".", "requires" : [ "shift" ] }, + { "title" : "#", "requires" : [ "alt-gr" ] } + ], + "4" : [ + { "title" : "4", "requires" : [ ] }, + { "title" : "$", "requires" : [ "shift" ] }, + { "title" : "~", "requires" : [ "alt-gr" ] } + ], + "5" : [ + { "title" : "5", "requires" : [ ] }, + { "title" : "%", "requires" : [ "shift" ] }, + { "title" : "€", "requires" : [ "alt-gr" ] } + ], + "6" : [ + { "title" : "6", "requires" : [ ] }, + { "title" : "&", "requires" : [ "shift" ] }, + { "title" : "¬", "requires" : [ "alt-gr" ] } + ], + "7" : [ + { "title" : "7", "requires" : [ ] }, + { "title" : "/", "requires" : [ "shift" ] } + ], + "8" : [ + { "title" : "8", "requires" : [ ] }, + { "title" : "(", "requires" : [ "shift" ] } + ], + "9" : [ + { "title" : "9", "requires" : [ ] }, + { "title" : ")", "requires" : [ "shift" ] } + ], + "0" : [ + { "title" : "0", "requires" : [ ] }, + { "title" : "=", "requires" : [ "shift" ] } + ], + "'" : [ + { "title" : "'", "requires" : [ ] }, + { "title" : "?", "requires" : [ "shift" ] } + ], + "¡" : [ + { "title" : "¡", "requires" : [ ] }, + { "title" : "¿", "requires" : [ "shift" ] } + ], + + "q" : [ + { "title" : "q", "requires" : [ ] }, + { "title" : "Q", "requires" : [ "caps" ] }, + { "title" : "Q", "requires" : [ "shift" ] }, + { "title" : "q", "requires" : [ "caps", "shift" ] } + ], + "w" : [ + { "title" : "w", "requires" : [ ] }, + { "title" : "W", "requires" : [ "caps" ] }, + { "title" : "W", "requires" : [ "shift" ] }, + { "title" : "w", "requires" : [ "caps", "shift" ] } + ], + "e" : [ + { "title" : "e", "requires" : [ ] }, + { "title" : "E", "requires" : [ "caps" ] }, + { "title" : "E", "requires" : [ "shift" ] }, + { "title" : "e", "requires" : [ "caps", "shift" ] }, + { "title" : "€", "requires" : [ "alt-gr" ] } + ], + "r" : [ + { "title" : "r", "requires" : [ ] }, + { "title" : "R", "requires" : [ "caps" ] }, + { "title" : "R", "requires" : [ "shift" ] }, + { "title" : "r", "requires" : [ "caps", "shift" ] } + ], + "t" : [ + { "title" : "t", "requires" : [ ] }, + { "title" : "T", "requires" : [ "caps" ] }, + { "title" : "T", "requires" : [ "shift" ] }, + { "title" : "t", "requires" : [ "caps", "shift" ] } + ], + "y" : [ + { "title" : "y", "requires" : [ ] }, + { "title" : "Y", "requires" : [ "caps" ] }, + { "title" : "Y", "requires" : [ "shift" ] }, + { "title" : "y", "requires" : [ "caps", "shift" ] } + ], + "u" : [ + { "title" : "u", "requires" : [ ] }, + { "title" : "U", "requires" : [ "caps" ] }, + { "title" : "U", "requires" : [ "shift" ] }, + { "title" : "u", "requires" : [ "caps", "shift" ] } + ], + "i" : [ + { "title" : "i", "requires" : [ ] }, + { "title" : "I", "requires" : [ "caps" ] }, + { "title" : "I", "requires" : [ "shift" ] }, + { "title" : "i", "requires" : [ "caps", "shift" ] } + ], + "o" : [ + { "title" : "o", "requires" : [ ] }, + { "title" : "O", "requires" : [ "caps" ] }, + { "title" : "O", "requires" : [ "shift" ] }, + { "title" : "o", "requires" : [ "caps", "shift" ] } + ], + "p" : [ + { "title" : "p", "requires" : [ ] }, + { "title" : "P", "requires" : [ "caps" ] }, + { "title" : "P", "requires" : [ "shift" ] }, + { "title" : "p", "requires" : [ "caps", "shift" ] } + ], + "`" : [ + { "title" : "`", "requires" : [ ] }, + { "title" : "`", "requires" : [ "caps" ] }, + { "title" : "^", "requires" : [ "shift" ] }, + { "title" : "^", "requires" : [ "caps", "shift" ] }, + { "title" : "[", "requires" : [ "alt-gr" ] } + ], + "+" : [ + { "title" : "+", "requires" : [ ] }, + { "title" : "+", "requires" : [ "caps" ] }, + { "title" : "*", "requires" : [ "shift" ] }, + { "title" : "*", "requires" : [ "caps", "shift" ] }, + { "title" : "]", "requires" : [ "alt-gr" ] } + ], + "a" : [ + { "title" : "a", "requires" : [ ] }, + { "title" : "A", "requires" : [ "caps" ] }, + { "title" : "A", "requires" : [ "shift" ] }, + { "title" : "a", "requires" : [ "caps", "shift" ] } + ], + "s" : [ + { "title" : "s", "requires" : [ ] }, + { "title" : "S", "requires" : [ "caps" ] }, + { "title" : "S", "requires" : [ "shift" ] }, + { "title" : "s", "requires" : [ "caps", "shift" ] } + ], + "d" : [ + { "title" : "d", "requires" : [ ] }, + { "title" : "D", "requires" : [ "caps" ] }, + { "title" : "D", "requires" : [ "shift" ] }, + { "title" : "d", "requires" : [ "caps", "shift" ] } + ], + "f" : [ + { "title" : "f", "requires" : [ ] }, + { "title" : "F", "requires" : [ "caps" ] }, + { "title" : "F", "requires" : [ "shift" ] }, + { "title" : "f", "requires" : [ "caps", "shift" ] } + ], + "g" : [ + { "title" : "g", "requires" : [ ] }, + { "title" : "G", "requires" : [ "caps" ] }, + { "title" : "G", "requires" : [ "shift" ] }, + { "title" : "g", "requires" : [ "caps", "shift" ] } + ], + "h" : [ + { "title" : "h", "requires" : [ ] }, + { "title" : "H", "requires" : [ "caps" ] }, + { "title" : "H", "requires" : [ "shift" ] }, + { "title" : "h", "requires" : [ "caps", "shift" ] } + ], + "j" : [ + { "title" : "j", "requires" : [ ] }, + { "title" : "J", "requires" : [ "caps" ] }, + { "title" : "J", "requires" : [ "shift" ] }, + { "title" : "j", "requires" : [ "caps", "shift" ] } + ], + "k" : [ + { "title" : "k", "requires" : [ ] }, + { "title" : "K", "requires" : [ "caps" ] }, + { "title" : "K", "requires" : [ "shift" ] }, + { "title" : "k", "requires" : [ "caps", "shift" ] } + ], + "l" : [ + { "title" : "l", "requires" : [ ] }, + { "title" : "L", "requires" : [ "caps" ] }, + { "title" : "L", "requires" : [ "shift" ] }, + { "title" : "l", "requires" : [ "caps", "shift" ] } + ], + "ñ" : [ + { "title" : "ñ", "requires" : [ ] }, + { "title" : "Ñ", "requires" : [ "caps" ] }, + { "title" : "Ñ", "requires" : [ "shift" ] }, + { "title" : "ñ", "requires" : [ "caps", "shift" ] } + ], + "´" : [ + { "title" : "´", "requires" : [ ], "keysym" : 65105 }, + { "title" : "´", "requires" : [ "caps" ] }, + { "title" : "¨", "requires" : [ "shift" ] }, + { "title" : "¨", "requires" : [ "caps", "shift" ] }, + { "title" : "{", "requires" : [ "alt-gr" ] } + ], + "ç" : [ + { "title" : "ç", "requires" : [ ] }, + { "title" : "Ç", "requires" : [ "caps" ] }, + { "title" : "Ç", "requires" : [ "shift" ] }, + { "title" : "ç", "requires" : [ "caps", "shift" ] }, + { "title" : "}", "requires" : [ "alt-gr" ] } + ], + "<" : [ + { "title" : "<", "requires" : [ ] }, + { "title" : "<", "requires" : [ "caps" ] }, + { "title" : ">", "requires" : [ "shift" ] }, + { "title" : ">", "requires" : [ "caps", "shift" ] } + ], + + "z" : [ + { "title" : "z", "requires" : [ ] }, + { "title" : "Z", "requires" : [ "caps" ] }, + { "title" : "Z", "requires" : [ "shift" ] }, + { "title" : "z", "requires" : [ "caps", "shift" ] } + ], + "x" : [ + { "title" : "x", "requires" : [ ] }, + { "title" : "X", "requires" : [ "caps" ] }, + { "title" : "X", "requires" : [ "shift" ] }, + { "title" : "x", "requires" : [ "caps", "shift" ] } + ], + "c" : [ + { "title" : "c", "requires" : [ ] }, + { "title" : "C", "requires" : [ "caps" ] }, + { "title" : "C", "requires" : [ "shift" ] }, + { "title" : "c", "requires" : [ "caps", "shift" ] } + ], + "v" : [ + { "title" : "v", "requires" : [ ] }, + { "title" : "V", "requires" : [ "caps" ] }, + { "title" : "V", "requires" : [ "shift" ] }, + { "title" : "v", "requires" : [ "caps", "shift" ] } + ], + "b" : [ + { "title" : "b", "requires" : [ ] }, + { "title" : "B", "requires" : [ "caps" ] }, + { "title" : "B", "requires" : [ "shift" ] }, + { "title" : "b", "requires" : [ "caps", "shift" ] } + ], + "n" : [ + { "title" : "n", "requires" : [ ] }, + { "title" : "N", "requires" : [ "caps" ] }, + { "title" : "N", "requires" : [ "shift" ] }, + { "title" : "n", "requires" : [ "caps", "shift" ] } + ], + "m" : [ + { "title" : "m", "requires" : [ ] }, + { "title" : "M", "requires" : [ "caps" ] }, + { "title" : "M", "requires" : [ "shift" ] }, + { "title" : "m", "requires" : [ "caps", "shift" ] } + ], + "," : [ + { "title" : ",", "requires" : [ ] }, + { "title" : ";", "requires" : [ "shift" ] } + ], + "." : [ + { "title" : ".", "requires" : [ ] }, + { "title" : ":", "requires" : [ "shift" ] } + ], + "-" : [ + { "title" : "-", "requires" : [ ] }, + { "title" : "_", "requires" : [ "shift" ] } + ] + }, + + "layout" : [ + + [ "Esc", 0.8, "F1", "F2", "F3", "F4", + 0.8, "F5", "F6", "F7", "F8", + 0.8, "F9", "F10", "F11", "F12" ], + + [ 0.1 ], + + { + "main" : { + "alpha" : [ + + [ "º", "1", "2", "3", "4", "5", "6", "7", "8", "9", "0", "'", "¡", "Back" ], + [ "Tab", "q", "w", "e", "r", "t", "y", "u", "i", "o", "p", "`", "+", 1, 0.6 ], + [ "Caps", "a", "s", "d", "f", "g", "h", "j", "k", "l", "ñ", "´", "ç", "Enter" ], + [ "LShift", "<", "z", "x", "c", "v", "b", "n", "m", ",", ".", "-", "RShift" ], + [ "LCtrl", "Meta", "LAlt", "Space", "AltGr", "Menu", "RCtrl" ] + + ], + + "movement" : [ + [ "Ins", "Home", "PgUp" ], + [ "Del", "End", "PgDn" ], + [ 1 ], + [ "Up" ], + [ "Left", "Down", "Right" ] + ] + } + } + + ], + + "keyWidths" : { + + "Back" : 2.3, + "Tab" : 1.75, + "\\" : 1.25, + "Caps" : 1.75, + "Enter" : 1.5, + "LShift" : 2.2, + "RShift" : 2.2, + + "LCtrl" : 1.6, + "Meta" : 1.6, + "LAlt" : 1.6, + "Space" : 6.4, + "AltGr" : 1.6, + "Menu" : 1.6, + "RCtrl" : 1.6, + + "Ins" : 1.6, + "Home" : 1.6, + "PgUp" : 1.6, + "Del" : 1.6, + "End" : 1.6, + "PgDn" : 1.6 + + } + +} diff --git a/guacamole/src/main/frontend/src/layouts/fr-fr-azerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/fr-fr-azerty.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/fr-fr-azerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/fr-fr-azerty.json diff --git a/guacamole/src/main/frontend/src/layouts/it-it-qwerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/it-it-qwerty.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/it-it-qwerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/it-it-qwerty.json diff --git a/guacamole/src/main/frontend/src/layouts/nl-nl-qwerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/nl-nl-qwerty.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/nl-nl-qwerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/nl-nl-qwerty.json diff --git a/guacamole/src/main/frontend/src/layouts/ru-ru-qwerty.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/ru-ru-qwerty.json similarity index 100% rename from guacamole/src/main/frontend/src/layouts/ru-ru-qwerty.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/assets/layouts/ru-ru-qwerty.json diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/client.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/client.module.ts new file mode 100644 index 0000000000..7468639f95 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/client.module.ts @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ViewportComponent } from './components/viewport.component'; + +/** + * The module for code used to connect to a connection or balancing group. + */ +@NgModule({ + declarations: [ + ViewportComponent + ], + imports : [ + CommonModule + ], + exports : [ + ViewportComponent + ] +}) +export class ClientLibModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.html new file mode 100644 index 0000000000..c6443222bf --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.html @@ -0,0 +1,22 @@ + + +
    + +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.ts new file mode 100644 index 0000000000..ba47be5eec --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/components/viewport.component.ts @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { AfterViewInit, Component, ElementRef, OnDestroy, ViewChild, ViewEncapsulation } from '@angular/core'; + +/** + * A component which provides a fullscreen environment for its content. + */ +@Component({ + selector: 'guac-viewport', + templateUrl: './viewport.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ViewportComponent implements AfterViewInit, OnDestroy { + + /** + * The wrapped fullscreen container element. + */ + @ViewChild('viewport') elementRef!: ElementRef; + + /** + * The fullscreen container element. + */ + element!: HTMLElement; + + /** + * The width of the browser viewport when fitVisibleArea() was last + * invoked, in pixels, or null if fitVisibleArea() has not yet been + * called. + */ + lastViewportWidth?: number = undefined; + + /** + * The height of the browser viewport when fitVisibleArea() was + * last invoked, in pixels, or null if fitVisibleArea() has not yet + * been called. + */ + lastViewportHeight?: number = undefined; + private pollArea?: number; + + + ngAfterViewInit(): void { + this.element = this.elementRef.nativeElement; + + // Fit container within visible region when window scrolls + window.addEventListener('scroll', this.fitVisibleArea); + + // Poll every 10ms, in case scroll event does not fire + this.pollArea = window.setInterval(() => this.fitVisibleArea(), 10); + } + + /** + * Clean up on destruction + */ + ngOnDestroy(): void { + window.removeEventListener('scroll', this.fitVisibleArea); + window.clearInterval(this.pollArea); + } + + /** + * Resizes the container element inside the guacViewport such that + * it exactly fits within the visible area, even if the browser has + * been scrolled. + */ + fitVisibleArea(): void { + + // Calculate viewport dimensions (this is NOT necessarily the + // same as 100vw and 100vh, 100%, etc., particularly when the + // on-screen keyboard of a mobile device pops open) + const viewportWidth = window.innerWidth; + const viewportHeight = window.innerHeight; + + // Adjust element width to fit exactly within visible area + if (viewportWidth !== this.lastViewportWidth) { + this.element.style.width = viewportWidth + 'px'; + this.lastViewportWidth = viewportWidth; + } + + // Adjust this.element height to fit exactly within visible area + if (viewportHeight !== this.lastViewportHeight) { + this.element.style.height = viewportHeight + 'px'; + this.lastViewportHeight = viewportHeight; + } + + // Scroll this.element such that its upper-left corner is exactly + // within the viewport upper-left corner, if not already there + if (this.element.scrollLeft || this.element.scrollTop) { + window.scrollTo( + window.scrollX + this.element.scrollLeft, + window.scrollY + this.element.scrollTop + ); + } + + } +} diff --git a/guacamole/src/main/frontend/src/app/index/config/httpDefaults.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.spec.ts similarity index 61% rename from guacamole/src/main/frontend/src/app/index/config/httpDefaults.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.spec.ts index 9de1ce22b9..c7ec733f0b 100644 --- a/guacamole/src/main/frontend/src/app/index/config/httpDefaults.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.spec.ts @@ -17,20 +17,23 @@ * under the License. */ -/** - * Defaults for the AngularJS $http service. - */ -angular.module('index').config(['$httpProvider', function httpDefaults($httpProvider) { +import { TestBed } from '@angular/core/testing'; + +import { GuacAudioService } from './guac-audio.service'; + +describe('GuacAudioService', () => { + let service: GuacAudioService; - // Do not cache the responses of GET requests - $httpProvider.defaults.headers.get = { - 'Cache-Control' : 'no-cache', - 'Pragma' : 'no-cache' - }; + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuacAudioService); + }); - // Use "application/json" content type by default for PATCH requests - $httpProvider.defaults.headers.patch = { - 'Content-Type' : 'application/json' - }; + it('should be created', () => { + expect(service).toBeTruthy(); + }); -}]); + it('should support at least one audio type', () => { + expect(service.supported).not.toBe([]); + }); +}); diff --git a/guacamole/src/main/frontend/src/app/client/services/guacAudio.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.ts similarity index 80% rename from guacamole/src/main/frontend/src/app/client/services/guacAudio.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.ts index 95aec4c7a5..75f4fe1bab 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacAudio.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-audio.service.ts @@ -17,23 +17,25 @@ * under the License. */ +import { Injectable } from '@angular/core'; + /** * A service for checking browser audio support. */ -angular.module('client').factory('guacAudio', [function guacAudio() { - +@Injectable({ + providedIn: 'root' +}) +export class GuacAudioService { + /** - * Object describing the UI's level of audio support. + * Array of all supported audio mimetypes. */ - return new (function() { + supported: string[]; - /** - * Array of all supported audio mimetypes. - * - * @type String[] - */ + /** + * Object describing the UI's level of audio support. + */ + constructor() { this.supported = Guacamole.AudioPlayer.getSupportedTypes(); - - })(); - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.spec.ts new file mode 100644 index 0000000000..6f4aceb924 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.spec.ts @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { GuacImageService } from './guac-image.service'; + +describe('GuacImageService', () => { + let service: GuacImageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuacImageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return that all image types are supported', async () => { + const supported = await service.getSupportedMimetypes(); + expect(supported).toEqual(['image/jpeg', 'image/png', 'image/webp']); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.ts new file mode 100644 index 0000000000..fd1e1f57ff --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-image.service.ts @@ -0,0 +1,140 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; + +/** + * A service for checking browser image support. + */ +@Injectable({ + providedIn: 'root' +}) +export class GuacImageService { + + /** + * Map of possibly-supported image mimetypes to corresponding test images + * encoded with base64. If the image is correctly decoded, it will be a + * single pixel (1x1) image. + */ + private testImages: Record = { + + /** + * Test JPEG image, encoded as base64. + */ + 'image/jpeg': + '/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAMCAgMCAgMDAwMEAwMEBQgFBQQEBQoH' + + 'BwYIDAoMDAsKCwsNDhIQDQ4RDgsLEBYQERMUFRUVDA8XGBYUGBIUFRT/2wBDAQME' + + 'BAUEBQkFBQkUDQsNFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQUFBQU' + + 'FBQUFBQUFBQUFBQUFBT/wAARCAABAAEDAREAAhEBAxEB/8QAFAABAAAAAAAAAAA' + + 'AAAAAAAAACf/EABQQAQAAAAAAAAAAAAAAAAAAAAD/xAAUAQEAAAAAAAAAAAAAAA' + + 'AAAAAA/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AVMH/2Q==', + + /** + * Test PNG image, encoded as base64. + */ + 'image/png': + 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABAQMAAAAl21bKAAAAA1BMVEX///+nxBvI' + + 'AAAACklEQVQI12NgAAAAAgAB4iG8MwAAAABJRU5ErkJggg==', + + /** + * Test WebP image, encoded as base64. + */ + 'image/webp': 'UklGRhoAAABXRUJQVlA4TA0AAAAvAAAAEAcQERGIiP4HAA==' + + }; + + /** + * The resolve function associated with {@link deferredSupportedMimetypes}. + */ + private deferredSupportedMimetypesResolve?: (value: string[] | PromiseLike) => void; + + /** + * Deferred which tracks the progress and ultimate result of all pending + * image format tests. + * + */ + private deferredSupportedMimetypes: Promise = + new Promise((resolve) => { + return this.deferredSupportedMimetypesResolve = resolve; + }); + + /** + * Array of all promises associated with pending image tests. Each image + * test promise MUST be guaranteed to resolve and MUST NOT be rejected. + */ + private pendingTests: Promise[] = []; + + /** + * The array of supported image formats. This will be gradually populated + * by the various image tests that occur in the background, and will not be + * fully populated until all promises within pendingTests are resolved. + */ + private supported: string[] = []; + + /** + * Return a promise which resolves with to an array of image mimetypes + * supported by the browser, once those mimetypes are known. The returned + * promise is guaranteed to resolve successfully. + * + * @returns + * A promise which resolves with an array of image mimetypes supported + * by the browser. + */ + getSupportedMimetypes(): Promise { + return this.deferredSupportedMimetypes; + } + + constructor() { + // Test each possibly-supported image + for (const mimetype in this.testImages) { + const data = this.testImages[mimetype]; + + // Add promise for current image test + const imageTest = new Promise((resolve) => { + // Attempt to load image + const image = new Image(); + image.src = 'data:' + mimetype + ';base64,' + data; + + // Store as supported depending on whether load was successful + image.onload = image.onerror = () => { + // imageTestComplete + + // Image format is supported if successfully decoded + if (image.width === 1 && image.height === 1) + this.supported.push(mimetype); + + // Test is complete + resolve(); + + }; + }); + + this.pendingTests.push(imageTest); + + } + + // When all image tests are complete, resolve promise with list of + // supported formats + Promise.all(this.pendingTests).then(() => { + this.deferredSupportedMimetypesResolve?.(this.supported); + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.spec.ts new file mode 100644 index 0000000000..d4e45ca496 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.spec.ts @@ -0,0 +1,36 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; + +import { GuacVideoService } from './guac-video.service'; + +describe('GuacVideoService', () => { + let service: GuacVideoService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuacVideoService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + +}); diff --git a/guacamole/src/main/frontend/src/app/client/services/guacVideo.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.ts similarity index 81% rename from guacamole/src/main/frontend/src/app/client/services/guacVideo.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.ts index 08a3c368c3..25011ae697 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacVideo.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/client/services/guac-video.service.ts @@ -17,21 +17,25 @@ * under the License. */ +import { Injectable } from '@angular/core'; + /** * A service for checking browser video support. */ -angular.module('client').factory('guacVideo', [function guacVideo() { - +@Injectable({ + providedIn: 'root' +}) +export class GuacVideoService { + /** - * Object describing the UI's level of video support. + * Array of all supported video mimetypes. */ - return new (function() { + supported: string[]; - /** - * Array of all supported video mimetypes. - */ + /** + * Object describing the UI's level of video support. + */ + constructor() { this.supported = Guacamole.VideoPlayer.getSupportedTypes(); - - })(); - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-click.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-click.directive.ts new file mode 100644 index 0000000000..17748c59e3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-click.directive.ts @@ -0,0 +1,130 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DestroyRef, Directive, ElementRef, HostListener, Input } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GuacEventService } from '../../events/services/guac-event.service'; +import { GuacEventArguments } from '../../events/types/GuacEventArguments'; + +/** + * A callback that is invoked by the guacClick directive when a + * click or click-like event is received. + * + * @param shift + * Whether Shift was held down at the time the click occurred. + * + * @param ctrl + * Whether Ctrl or Meta (the Mac "Command" key) was held down + * at the time the click occurred. + */ +export type GuacClickCallback = (shift: boolean, ctrl: boolean) => void; + +/** + * A directive which provides handling of click and click-like touch events. + * The state of Shift and Ctrl modifiers is tracked through these click events + * to allow for specific handling of Shift+Click and Ctrl+Click. + */ +@Directive({ + selector: '[guacClick]', + standalone: false +}) +export class GuacClickDirective { + + /** + * A callback that is invoked by the guacClick directive when a + * click or click-like event is received. + */ + @Input({ required: true }) guacClick!: GuacClickCallback; + + /** + * The element which will register the click. + */ + element: Element; + + /** + * Whether either Shift key is currently pressed. + */ + shift = false; + + /** + * Whether either Ctrl key is currently pressed. To allow the + * Command key to be used on Mac platforms, this flag also + * considers the state of either Meta key. + */ + ctrl = false; + + // Fire provided callback for each mouse-initiated "click" event ... + @HostListener('click', ['$event']) elementClicked(e: MouseEvent) { + if (this.element.contains(e.target as Node)) { + this.guacClick(this.shift, this.ctrl); + } + } + + // ... and for touch-initiated click-like events + @HostListener('touchstart', ['$event']) elementTouched(e: Event) { + if (this.element.contains(e.target as Node)) { + this.guacClick(this.shift, this.ctrl); + } + } + + constructor(el: ElementRef, + private guacEventService: GuacEventService, + private destroyRef: DestroyRef) { + this.element = el.nativeElement; + + // Update tracking of modifier states for each key press + this.guacEventService.on('guacKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ keyboard }) => { + this.updateModifiers(keyboard); + }); + + // Update tracking of modifier states for each key release + this.guacEventService.on('guacKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ keyboard }) => { + this.updateModifiers(keyboard); + }); + } + + /** + * Updates the state of the {@link shift} and {@link ctrl} flags + * based on which keys are currently marked as held down by the + * given Guacamole.Keyboard. + * + * @param keyboard + * The Guacamole.Keyboard instance to read key states from. + */ + updateModifiers(keyboard: Guacamole.Keyboard) { + + this.shift = !!( + keyboard.pressed[0xFFE1] // Left shift + || keyboard.pressed[0xFFE2] // Right shift + ); + + this.ctrl = !!( + keyboard.pressed[0xFFE3] // Left ctrl + || keyboard.pressed[0xFFE4] // Right ctrl + || keyboard.pressed[0xFFE7] // Left meta (command) + || keyboard.pressed[0xFFE8] // Right meta (command) + ); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-drop.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-drop.directive.ts new file mode 100644 index 0000000000..c8f4742ffb --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-drop.directive.ts @@ -0,0 +1,162 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, Input } from '@angular/core'; + +/** + * A directive which allows multiple files to be uploaded. Dragging files onto + * the associated element will call the provided callback function with any + * dragged files. + */ +@Directive({ + selector: '[guacDrop]', + standalone: false +}) +export class GuacDropDirective { + + /** + * The function to call whenever files are dragged. The callback is + * provided a single parameter: the FileList containing all dragged + * files. + */ + @Input({ required: true }) guacDrop!: (files: FileList) => void; + + /** + * Any number of space-seperated classes to be applied to the + * element a drop is pending: when the user has dragged something + * over the element, but not yet dropped. These classes will be + * removed when a drop is not pending. + */ + @Input() guacDraggedClass: string | undefined; + + + /** + * Whether upload of multiple files should be allowed. If false, + * guacDropError callback will be called with a message explaining + * the restriction, otherwise any number of files may be dragged. + * Defaults to true if not set. + */ + @Input() guacMultiple: boolean = true; + + /** + * The function to call whenever an error occurs during the drop + * operation. The callback is provided a single parameter: the translation + * key of the error message to display. + */ + @Input() guacDropError: (errorTextKey: string) => void = () => { + }; + + /** + * The element which will register drag event. + */ + private element: HTMLElement; + + constructor(el: ElementRef) { + this.element = el.nativeElement; + + // Add listeners to the drop target to ensure that the visual state + // stays up to date + this.element.addEventListener('dragenter', this.notifyDragStart.bind(this)); + this.element.addEventListener('dragover', this.notifyDragStart.bind(this)); + this.element.addEventListener('dragleave', this.notifyDragEnd.bind(this)); + + /** + * Event listener that will be invoked if the user drops anything + * onto the event. If a valid file is provided, the onFile callback + * provided to this directive will be called; otherwise the guacDropError + * callback will be called, if appropriate. + * + * @param e + * The drop event that triggered this handler. + */ + this.element.addEventListener('drop', (e: DragEvent) => { + + this.notifyDragEnd(e); + + const files: FileList = e.dataTransfer?.files ?? new FileList(); + + // Ignore any non-files that are dragged into the drop area + if (files.length < 1) + return; + + // If multi-file upload is disabled, If more than one file was + // provided, print an error explaining the problem + if (!this.guacMultiple && files.length >= 2) { + + this.guacDropError('APP.ERROR_SINGLE_FILE_ONLY'); + + return; + + } + + // Invoke the callback with the files. Note that if guacMultiple + // is set to false, this will always be a single file. + this.guacDrop(files); + + }); + + } + + /** + * Applies any classes provided in the guacDraggedClass attribute. + * Further propagation and default behavior of the given event is + * automatically prevented. + * + * @param e + * The event related to the in-progress drag/drop operation. + */ + private notifyDragStart(e: Event): void { + + e.preventDefault(); + e.stopPropagation(); + + // Skip further processing if no classes were provided + if (!this.guacDraggedClass) + return; + + // Add each provided class + this.guacDraggedClass.split(' ').forEach(classToApply => + this.element.classList.add(classToApply)); + + } + + /** + * Removes any classes provided in the guacDraggedClass attribute. + * Further propagation and default behavior of the given event is + * automatically prevented. + * + * @param e + * The event related to the end of the drag/drop operation. + */ + private notifyDragEnd(e: Event): void { + + e.preventDefault(); + e.stopPropagation(); + + // Skip further processing if no classes were provided + if (!this.guacDraggedClass) + return; + + // Remove each provided class + this.guacDraggedClass.split(' ').forEach(classToRemove => + this.element.classList.remove(classToRemove)); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-focus.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-focus.directive.ts new file mode 100644 index 0000000000..06e4250525 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-focus.directive.ts @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; + +/** + * A directive which allows elements to be manually focused / blurred. + */ +@Directive({ + selector: '[guacFocus]', + standalone: false +}) +export class GuacFocusDirective implements OnInit, OnChanges { + + /** + * Whether the element associated with this directive should be + * focussed. + */ + @Input() guacFocus?: boolean; + + /** + * The element which will be focused / blurred. + */ + element: any; + + constructor(private el: ElementRef) { + this.element = el.nativeElement; + } + + ngOnInit() { + this.updateFocus(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['guacFocus']) + this.updateFocus(); + } + + /** + * Set/unset focus depending on value of guacFocus. + */ + updateFocus() { + if (this.guacFocus) { + this.element.focus(); + } else { + this.element.blur(); + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-resize.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-resize.directive.ts new file mode 100644 index 0000000000..f414678702 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-resize.directive.ts @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Directive, ElementRef, EventEmitter, Inject, Output } from '@angular/core'; + +/** + * A directive which calls a given callback when its associated element is + * resized. This will modify the internal DOM tree of the associated element, + * and the associated element MUST have position (for example, + * "position: relative"). + */ +@Directive({ + selector: '[guacResize]', + standalone: false +}) +export class GuacResizeDirective { + + /** + * Will emit an event including the width and height of the element, in pixels whenever the associated + * element is resized. + */ + @Output() guacResize = new EventEmitter<{ width: number, height: number }>(); + + /** + * The element which will monitored for size changes. + */ + element: any; + + /** + * The resize sensor - an HTML object element. + */ + resizeSensor: HTMLObjectElement; + + /** + * The width of the associated element, in pixels. + */ + lastWidth: number; + + /** + * The height of the associated element, in pixels. + */ + lastHeight: number; + + constructor(private el: ElementRef, @Inject(DOCUMENT) private document: Document) { + this.element = el.nativeElement; + this.lastWidth = this.element.offsetWidth; + this.lastHeight = this.element.offsetHeight; + + this.resizeSensor = this.document.createElement('object'); + + // Register event listener once window object exists + this.resizeSensor.onload = () => { + this.resizeSensor.contentDocument?.defaultView?.addEventListener('resize', () => this.checkSize()); + this.checkSize(); + }; + + // Load blank contents + this.resizeSensor.style.cssText = 'height: 100%; width: 100%; position: absolute; left: 0; top: 0; overflow: hidden; border: none; opacity: 0; z-index: -1;'; + this.resizeSensor.type = 'text/html'; + this.resizeSensor.innerHTML = ` + + + + + _ + + + + `; + + // Add resize sensor to associated element + this.element.insertBefore(this.resizeSensor, this.element.firstChild); + } + + /** + * Checks whether the size of the associated element has changed + * and, if so, calls the resize callback with the new width and + * height as parameters. + */ + checkSize(): void { + // Call callback only if size actually changed + if (this.element.offsetWidth !== this.lastWidth + || this.element.offsetHeight !== this.lastHeight) { + + // Call resize callback, if defined + this.guacResize.emit({ width: this.element.offsetWidth, height: this.element.offsetHeight }); + + // Update stored size + this.lastWidth = this.element.offsetWidth; + this.lastHeight = this.element.offsetHeight; + + } + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-scroll.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-scroll.directive.ts new file mode 100644 index 0000000000..de81b391c2 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-scroll.directive.ts @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, Input, OnChanges, SimpleChanges } from '@angular/core'; +import { ScrollState } from '../types/ScrollState'; + +/** + * A directive which allows elements to be manually scrolled, and for their + * scroll state to be observed. + */ +@Directive({ + selector: '[guacScroll]', + standalone: false +}) +export class GuacScrollDirective implements OnChanges { + + /** + * The current scroll state of the element. + */ + @Input({ required: false }) guacScroll!: ScrollState; + + /** + * The element which is being scrolled, or monitored for changes + * in scroll. + */ + element: Element; + + constructor(el: ElementRef) { + this.element = el.nativeElement; + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['guacScroll']) { + const currentValue = changes['guacScroll'].currentValue as ScrollState; + const previousValue = changes['guacScroll'].previousValue as ScrollState | undefined; + + // Update underlying scrollLeft property when left changes + if (currentValue.left !== previousValue?.left) { + this.element.scrollLeft = currentValue.left; + this.guacScroll.left = this.element.scrollLeft; + } + + // Update underlying scrollTop property when top changes + if (currentValue.top !== previousValue?.top) { + this.element.scrollTop = currentValue.top; + this.guacScroll.top = this.element.scrollTop; + } + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-upload.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-upload.directive.ts new file mode 100644 index 0000000000..c909b928de --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/directives/guac-upload.directive.ts @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { ChangeDetectorRef, Directive, ElementRef, Inject, Input, OnInit } from '@angular/core'; + +/** + * A directive which allows files to be uploaded. Clicking on the associated + * element will result in a file selector dialog, which then calls the provided + * callback function with any chosen files. + */ +@Directive({ + selector: '[guacUpload]', + standalone: false +}) +export class GuacUploadDirective implements OnInit { + + /** + * The function to call whenever files are chosen. The callback is + * provided a single parameter: the FileList containing all chosen + * files. + */ + @Input({ required: true }) guacUpload!: (files: FileList) => void; + + /** + * Whether upload of multiple files should be allowed. If false, the + * file dialog will only allow a single file to be chosen at once, + * otherwise any number of files may be chosen. Defaults to true if + * not set. + */ + @Input() guacMultiple = true; + + /** + * The element which will register the click. + */ + element: Element; + + /** + * Internal form, containing a single file input element. + */ + readonly form: HTMLFormElement; + + /** + * Internal file input element. + */ + readonly input: HTMLInputElement; + + constructor(private el: ElementRef, + @Inject(DOCUMENT) private document: Document, + private cdr: ChangeDetectorRef) { + this.element = el.nativeElement; + this.form = this.document.createElement('form'); + this.input = this.document.createElement('input'); + } + + ngOnInit(): void { + // Init input element + this.input.type = 'file'; + this.input.multiple = this.guacMultiple; + + // Add input element to internal form + this.form.appendChild(this.input); + + // Notify of any chosen files + this.input.addEventListener('change', () => { + + // Only set chosen files selection is not canceled + if (this.input.files && this.input.files.length > 0) + this.guacUpload(this.input.files); + + // Reset selection + this.form.reset(); + this.cdr.detectChanges(); + }); + + // Open file chooser when element is clicked + this.element.addEventListener('click', () => { + this.input.click(); + }); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/element.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/element.module.ts new file mode 100644 index 0000000000..81515de25f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/element.module.ts @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { GuacClickDirective } from './directives/guac-click.directive'; +import { GuacDropDirective } from './directives/guac-drop.directive'; +import { GuacFocusDirective } from './directives/guac-focus.directive'; +import { GuacResizeDirective } from './directives/guac-resize.directive'; +import { GuacScrollDirective } from './directives/guac-scroll.directive'; +import { GuacUploadDirective } from './directives/guac-upload.directive'; + +/** + * Module for manipulating element state, such as focus or scroll position, as + * well as handling browser events. + */ +@NgModule({ + declarations: [ + GuacClickDirective, + GuacFocusDirective, + GuacResizeDirective, + GuacScrollDirective, + GuacUploadDirective, + GuacDropDirective + ], + imports : [ + CommonModule + ], + exports : [ + GuacClickDirective, + GuacFocusDirective, + GuacResizeDirective, + GuacScrollDirective, + GuacUploadDirective, + GuacDropDirective + ] +}) +export class ElementModule { +} diff --git a/guacamole/src/main/frontend/src/app/element/styles/resize-sensor.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/styles/resize-sensor.css similarity index 100% rename from guacamole/src/main/frontend/src/app/element/styles/resize-sensor.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/styles/resize-sensor.css diff --git a/guacamole/src/main/frontend/src/app/element/types/Marker.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/Marker.ts similarity index 68% rename from guacamole/src/main/frontend/src/app/element/types/Marker.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/Marker.ts index c1b47acb5f..79aec92a57 100644 --- a/guacamole/src/main/frontend/src/app/element/types/Marker.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/Marker.ts @@ -20,28 +20,24 @@ /** * Provides the Marker class definition. */ -angular.module('element').factory('Marker', [function defineMarker() { +export class Marker { /** - * Creates a new Marker which allows its associated element to be scolled + * Creates a new Marker which allows its associated element to be scrolled * into view as desired. * - * @constructor - * @param {Element} element + * @param element * The element to associate with this marker. */ - var Marker = function Marker(element) { + constructor(private element: Element) { + } - /** - * Scrolls scrollable elements, or the window, as needed to bring the - * element associated with this marker into view. - */ - this.scrollIntoView = function scrollIntoView() { - element.scrollIntoView(); - }; - - }; - - return Marker; + /** + * Scrolls scrollable elements, or the window, as needed to bring the + * element associated with this marker into view. + */ + scrollIntoView() { + this.element.scrollIntoView(); + } -}]); +} diff --git a/guacamole/src/main/frontend/src/app/element/types/ScrollState.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/ScrollState.ts similarity index 65% rename from guacamole/src/main/frontend/src/app/element/types/ScrollState.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/ScrollState.ts index 89562432d0..807900797a 100644 --- a/guacamole/src/main/frontend/src/app/element/types/ScrollState.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/element/types/ScrollState.ts @@ -20,7 +20,19 @@ /** * Provides the ScrollState class definition. */ -angular.module('element').factory('ScrollState', [function defineScrollState() { +export class ScrollState { + + /** + * The left edge of the view rectangle within the scrollable area. This + * value naturally increases as the user scrolls right. + */ + left: number; + + /** + * The top edge of the view rectangle within the scrollable area. This + * value naturally increases as the user scrolls down. + */ + top: number; /** * Creates a new ScrollState, representing the current scroll position of @@ -28,33 +40,12 @@ angular.module('element').factory('ScrollState', [function defineScrollState() { * new ScrollState with the corresponding properties of the given template. * * @constructor - * @param {ScrollState|Object} [template={}] + * @param template * The object whose properties should be copied within the new * ScrollState. */ - var ScrollState = function ScrollState(template) { - - // Use empty object by default - template = template || {}; - - /** - * The left edge of the view rectangle within the scrollable area. This - * value naturally increases as the user scrolls right. - * - * @type Number - */ + constructor(template: Partial = {}) { this.left = template.left || 0; - - /** - * The top edge of the view rectangle within the scrollable area. This - * value naturally increases as the user scrolls down. - * - * @type Number - */ this.top = template.top || 0; - - }; - - return ScrollState; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.spec.ts new file mode 100644 index 0000000000..7fd334b1b5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.spec.ts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from '../../../test-utils/test-scheduler'; +import { GuacEvent } from '../types/GuacEvent'; +import { GuacEventArguments } from '../types/GuacEventArguments'; +import { GuacEventService } from './guac-event.service'; + +interface TestEventArgs extends GuacEventArguments { + test: { age: number; }; +} + +describe('GuacEventService', () => { + let service: GuacEventService; + let testScheduler: TestScheduler; + + beforeEach(() => { + testScheduler = getTestScheduler(); + TestBed.configureTestingModule({ + providers: [GuacEventService], + }); + service = TestBed.inject(GuacEventService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should emit test event', () => { + testScheduler.run(({ expectObservable, cold }) => { + + const age = 25; + const expected = { + a: { + age : age, + event: new GuacEvent('test') + } + }; + const events = service.on('test'); + + // Delay the broadcast for one time unit to allow expectObservable to subscribe to the subject + cold('-a').subscribe(() => service.broadcast('test', { age })); + + expectObservable(events).toBe('-a', expected); + + }); + + }); + + it('handles multiple subscribers to the same event', () => { + testScheduler.run(({ expectObservable, cold }) => { + const age = 25; + const expected = { + a: { + age : age, + event: new GuacEvent('test') + } + }; + + const events1 = service.on('test'); + const events2 = service.on('test'); + + // Delay the broadcast for one time unit to allow expectObservable to subscribe to the subject + cold('-a').subscribe(() => service.broadcast('test', { age })); + + expectObservable(events1).toBe('-a', expected); + expectObservable(events2).toBe('-a', expected); + + }); + }); + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.ts new file mode 100644 index 0000000000..9c7f3905b2 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/services/guac-event.service.ts @@ -0,0 +1,74 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { filter, Observable, Subject } from 'rxjs'; +import { GuacEvent, GuacEventName } from '../types/GuacEvent'; +import { GuacEventArguments } from '../types/GuacEventArguments'; + +/** + * Service for broadcasting and subscribing to Guacamole frontend events. + * + * @template Args + * Type which specifies the set of all possible events and their + * arguments. + */ +@Injectable({ + providedIn: 'root' +}) +export class GuacEventService { + + /** + * Subject for all Guacamole frontend events. + */ + private events = new Subject(); + + /** + * TODO: Document + * + * @template T + * Union type of all possible event names. + * + * @param eventName + * @param args + */ + broadcast>(eventName: T, args?: Args[T]): GuacEvent { + const guacEvent = { event: new GuacEvent(eventName) }; + const eventArgs = args ? { ...args, ...guacEvent } : guacEvent; + this.events.next(eventArgs); + return guacEvent.event; + } + + /** + * TODO: Document + * + * @template T + * Union type of all possible event names. + * + * @param eventName + * @returns + */ + on>(eventName: T): Observable<{ + event: GuacEvent + } & Args[T]> { + return this.events.pipe( + filter(({ event }) => eventName === event.name) + ); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEvent.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEvent.ts new file mode 100644 index 0000000000..7575a93ab0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEvent.ts @@ -0,0 +1,63 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { GuacEventArguments } from './GuacEventArguments'; + +/** + * Type which represents the names of all possible Guacamole events. + * + * @template T + * Type which specifies the set of all possible events and their + * arguments. + */ +export type GuacEventName = keyof T; + +/** + * An event which is emitted by the GuacEventService. + * + * @template T + * Type which specifies the set of all possible events and their + * arguments. + */ +export class GuacEvent { + name: GuacEventName; + + /** + * Creates a new GuacEvent having the given name. + * + * @param name + * The name of the event. + */ + constructor(name: GuacEventName) { + this.name = name; + } + + /** + * Flag which is true if preventDefault() was called on this event, and + * false otherwise. + */ + defaultPrevented = false; + + /** + * Sets the defaultPrevented flag to true. + */ + preventDefault() { + this.defaultPrevented = true; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEventArguments.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEventArguments.ts new file mode 100644 index 0000000000..d666aeaba1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/events/types/GuacEventArguments.ts @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * TODO + */ +export interface GuacEventArguments { + + // Keyboard events + guacKeydown: { keysym: number; keyboard: Guacamole.Keyboard; }; + guacKeyup: { keysym: number; keyboard: Guacamole.Keyboard; }; + + // OSK events + guacSyntheticKeydown: { keysym: number; }; + guacSyntheticKeyup: { keysym: number; }; + + // Menu events + guacToggleMenu: Record; + guacShowMenu: Record; + guacHideMenu: Record; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.html new file mode 100644 index 0000000000..7a574cac8c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.html @@ -0,0 +1,21 @@ + + +
    +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.spec.ts new file mode 100644 index 0000000000..fcc0c1db25 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.spec.ts @@ -0,0 +1,94 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { SimpleChange } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { simulateMouseClick } from '../../../../test-utils/click-helper'; +import { ElementModule } from '../../../element/element.module'; +import { GuacEventService } from '../../../events/services/guac-event.service'; +import { GuacEventArguments } from '../../../events/types/GuacEventArguments'; +import { OskComponent } from './osk.component'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('OskComponent', () => { + let component: OskComponent; + let fixture: ComponentFixture; + + let httpTestingController: HttpTestingController; + let guacEventService: GuacEventService; + const layout = 'assets/de-de-qwertz.json'; + const layoutResponse = '{"language":"de_DE","type":"qwertz","width":23,"keys":{"0":[{"title":"0","requires":[]},{"title":"=","requires":["shift"]},{"title":"}","requires":["alt-gr"]}],"1":[{"title":"1","requires":[]},{"title":"!","requires":["shift"]}],"2":[{"title":"2","requires":[]},{"title":"\\"","requires":["shift"]},{"title":"²","requires":["alt-gr"]}],"3":[{"title":"3","requires":[]},{"title":"§","requires":["shift"]},{"title":"³","requires":["alt-gr"]}],"4":[{"title":"4","requires":[]},{"title":"$","requires":["shift"]}],"5":[{"title":"5","requires":[]},{"title":"%","requires":["shift"]}],"6":[{"title":"6","requires":[]},{"title":"&","requires":["shift"]}],"7":[{"title":"7","requires":[]},{"title":"/","requires":["shift"]},{"title":"{","requires":["alt-gr"]}],"8":[{"title":"8","requires":[]},{"title":"(","requires":["shift"]},{"title":"[","requires":["alt-gr"]}],"9":[{"title":"9","requires":[]},{"title":")","requires":["shift"]},{"title":"]","requires":["alt-gr"]}],"Esc":65307,"F1":65470,"F2":65471,"F3":65472,"F4":65473,"F5":65474,"F6":65475,"F7":65476,"F8":65477,"F9":65478,"F10":65479,"F11":65480,"F12":65481,"Space":" ","Back":[{"title":"⟵","keysym":65288}],"Tab":[{"title":"Tab ↹","keysym":65289}],"Enter":[{"title":"↵","keysym":65293}],"Home":[{"title":"Pos 1","keysym":65360}],"PgUp":[{"title":"Bild ↑","keysym":65365}],"PgDn":[{"title":"Bild ↓","keysym":65366}],"End":[{"title":"Ende","keysym":65367}],"Ins":[{"title":"Einfg","keysym":65379}],"Del":[{"title":"Entf","keysym":65535}],"Left":[{"title":"←","keysym":65361}],"Up":[{"title":"↑","keysym":65362}],"Right":[{"title":"→","keysym":65363}],"Down":[{"title":"↓","keysym":65364}],"Menu":[{"title":"Menu","keysym":65383}],"LShift":[{"title":"Shift","modifier":"shift","keysym":65505}],"RShift":[{"title":"Shift","modifier":"shift","keysym":65506}],"LCtrl":[{"title":"Strg","modifier":"control","keysym":65507}],"RCtrl":[{"title":"Strg","modifier":"control","keysym":65508}],"Caps":[{"title":"Caps","modifier":"caps","keysym":65509}],"LAlt":[{"title":"Alt","modifier":"alt","keysym":65513}],"AltGr":[{"title":"AltGr","modifier":"alt-gr","keysym":65027}],"Meta":[{"title":"Meta","modifier":"meta","keysym":65511}],"^":[{"title":"^","requires":[]},{"title":"°","requires":["shift"]}],"ß":[{"title":"ß","requires":[]},{"title":"?","requires":["shift"]},{"title":"\\\\","requires":["alt-gr"]}],"´":[{"title":"´","requires":[]},{"title":"`","requires":["shift"]}],"+":[{"title":"+","requires":[]},{"title":"*","requires":["shift"]},{"title":"~","requires":["alt-gr"]}],"#":[{"title":"#","requires":[]},{"title":"\'","requires":["shift"]}],"<":[{"title":"<","requires":[]},{"title":">","requires":["shift"]},{"title":"|","requires":["alt-gr"]}],",":[{"title":",","requires":[]},{"title":";","requires":["shift"]}],".":[{"title":".","requires":[]},{"title":":","requires":["shift"]}],"-":[{"title":"-","requires":[]},{"title":"_","requires":["shift"]}],"q":[{"title":"q","requires":[]},{"title":"Q","requires":["caps"]},{"title":"Q","requires":["shift"]},{"title":"q","requires":["caps","shift"]},{"title":"@","requires":["alt-gr"]}],"w":[{"title":"w","requires":[]},{"title":"W","requires":["caps"]},{"title":"W","requires":["shift"]},{"title":"w","requires":["caps","shift"]}],"e":[{"title":"e","requires":[]},{"title":"E","requires":["caps"]},{"title":"E","requires":["shift"]},{"title":"e","requires":["caps","shift"]},{"title":"€","requires":["alt-gr"]}],"r":[{"title":"r","requires":[]},{"title":"R","requires":["caps"]},{"title":"R","requires":["shift"]},{"title":"r","requires":["caps","shift"]}],"t":[{"title":"t","requires":[]},{"title":"T","requires":["caps"]},{"title":"T","requires":["shift"]},{"title":"t","requires":["caps","shift"]}],"z":[{"title":"z","requires":[]},{"title":"Z","requires":["caps"]},{"title":"Z","requires":["shift"]},{"title":"z","requires":["caps","shift"]}],"u":[{"title":"u","requires":[]},{"title":"U","requires":["caps"]},{"title":"U","requires":["shift"]},{"title":"u","requires":["caps","shift"]}],"i":[{"title":"i","requires":[]},{"title":"I","requires":["caps"]},{"title":"I","requires":["shift"]},{"title":"i","requires":["caps","shift"]}],"o":[{"title":"o","requires":[]},{"title":"O","requires":["caps"]},{"title":"O","requires":["shift"]},{"title":"o","requires":["caps","shift"]}],"p":[{"title":"p","requires":[]},{"title":"P","requires":["caps"]},{"title":"P","requires":["shift"]},{"title":"p","requires":["caps","shift"]}],"ü":[{"title":"ü","requires":[]},{"title":"Ü","requires":["caps"]},{"title":"Ü","requires":["shift"]},{"title":"ü","requires":["caps","shift"]}],"a":[{"title":"a","requires":[]},{"title":"A","requires":["caps"]},{"title":"A","requires":["shift"]},{"title":"a","requires":["caps","shift"]}],"s":[{"title":"s","requires":[]},{"title":"S","requires":["caps"]},{"title":"S","requires":["shift"]},{"title":"s","requires":["caps","shift"]}],"d":[{"title":"d","requires":[]},{"title":"D","requires":["caps"]},{"title":"D","requires":["shift"]},{"title":"d","requires":["caps","shift"]}],"f":[{"title":"f","requires":[]},{"title":"F","requires":["caps"]},{"title":"F","requires":["shift"]},{"title":"f","requires":["caps","shift"]}],"g":[{"title":"g","requires":[]},{"title":"G","requires":["caps"]},{"title":"G","requires":["shift"]},{"title":"g","requires":["caps","shift"]}],"h":[{"title":"h","requires":[]},{"title":"H","requires":["caps"]},{"title":"H","requires":["shift"]},{"title":"h","requires":["caps","shift"]}],"j":[{"title":"j","requires":[]},{"title":"J","requires":["caps"]},{"title":"J","requires":["shift"]},{"title":"j","requires":["caps","shift"]}],"k":[{"title":"k","requires":[]},{"title":"K","requires":["caps"]},{"title":"K","requires":["shift"]},{"title":"k","requires":["caps","shift"]}],"l":[{"title":"l","requires":[]},{"title":"L","requires":["caps"]},{"title":"L","requires":["shift"]},{"title":"l","requires":["caps","shift"]}],"ö":[{"title":"ö","requires":[]},{"title":"Ö","requires":["caps"]},{"title":"Ö","requires":["shift"]},{"title":"ö","requires":["caps","shift"]}],"ä":[{"title":"ä","requires":[]},{"title":"Ä","requires":["caps"]},{"title":"Ä","requires":["shift"]},{"title":"ä","requires":["caps","shift"]}],"y":[{"title":"y","requires":[]},{"title":"Y","requires":["caps"]},{"title":"Y","requires":["shift"]},{"title":"y","requires":["caps","shift"]}],"x":[{"title":"x","requires":[]},{"title":"X","requires":["caps"]},{"title":"X","requires":["shift"]},{"title":"x","requires":["caps","shift"]}],"c":[{"title":"c","requires":[]},{"title":"C","requires":["caps"]},{"title":"C","requires":["shift"]},{"title":"c","requires":["caps","shift"]}],"v":[{"title":"v","requires":[]},{"title":"V","requires":["caps"]},{"title":"V","requires":["shift"]},{"title":"v","requires":["caps","shift"]}],"b":[{"title":"b","requires":[]},{"title":"B","requires":["caps"]},{"title":"B","requires":["shift"]},{"title":"b","requires":["caps","shift"]}],"n":[{"title":"n","requires":[]},{"title":"N","requires":["caps"]},{"title":"N","requires":["shift"]},{"title":"n","requires":["caps","shift"]}],"m":[{"title":"m","requires":[]},{"title":"M","requires":["caps"]},{"title":"M","requires":["shift"]},{"title":"m","requires":["caps","shift"]},{"title":"µ","requires":["alt-gr"]}]},"layout":[["Esc",0.7,"F1","F2","F3","F4",0.7,"F5","F6","F7","F8",0.7,"F9","F10","F11","F12"],[0.1],{"main":{"alpha":[["^","1","2","3","4","5","6","7","8","9","0","ß","´","Back"],["Tab","q","w","e","r","t","z","u","i","o","p","ü","+",1,0.6],["Caps","a","s","d","f","g","h","j","k","l","ö","ä","#","Enter"],["LShift","<","y","x","c","v","b","n","m",",",".","-","RShift"],["LCtrl","Meta","LAlt","Space","AltGr","Menu","RCtrl"]],"movement":[["Ins","Home","PgUp"],["Del","End","PgDn"],[1],["Up"],["Left","Down","Right"]]}}],"keyWidths":{"Back":2,"Tab":1.5,"\\\\":1.5,"Caps":1.75,"Enter":1.25,"LShift":2,"RShift":2.1,"LCtrl":1.6,"Meta":1.6,"LAlt":1.6,"Space":6.1,"AltGr":1.6,"Menu":1.6,"RCtrl":1.6,"Ins":1.6,"Home":1.6,"PgUp":1.6,"Del":1.6,"End":1.6,"PgDn":1.6}}'; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + declarations: [OskComponent], + imports: [ElementModule], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}) + .compileComponents(); + + httpTestingController = TestBed.inject(HttpTestingController); + guacEventService = TestBed.inject(GuacEventService); + + fixture = TestBed.createComponent(OskComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('loads layout and displays osk', async () => { + component.layout = layout; + fixture.detectChanges(); + component.ngOnChanges({ layout: new SimpleChange(undefined, layout, true) }); + + const req = httpTestingController.expectOne(layout); + expect(req.request.method).toEqual('GET'); + req.flush(JSON.parse(layoutResponse)); + + expect(fixture.debugElement.query(By.css('.guac-keyboard'))).toBeTruthy(); + }); + + it('outputs emit events', () => { + spyOn(guacEventService, 'broadcast'); + + component.layout = layout; + fixture.detectChanges(); + component.ngOnChanges({ layout: new SimpleChange(undefined, layout, true) }); + + const req = httpTestingController.expectOne(layout); + expect(req.request.method).toEqual('GET'); + req.flush(JSON.parse(layoutResponse)); + + // click div with class 'guac-keyboard-cap' and text 'a' + const divs = fixture.debugElement.queryAll(By.css('.guac-keyboard-cap')); + const divA = divs.find((div) => div.nativeElement.textContent === 'a')?.nativeElement; + + simulateMouseClick(divA); + + expect(guacEventService.broadcast).toHaveBeenCalledWith('guacSyntheticKeydown', { keysym: 97 }); + expect(guacEventService.broadcast).toHaveBeenCalledWith('guacSyntheticKeyup', { keysym: 97 }); + + }); + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.ts new file mode 100644 index 0000000000..9f6ee6fdc3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/components/osk/osk.component.ts @@ -0,0 +1,124 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient } from '@angular/common/http'; +import { Component, ElementRef, Input, OnChanges, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { GuacEventService } from '../../../events/services/guac-event.service'; +import { GuacEventArguments } from '../../../events/types/GuacEventArguments'; + +/** + * A component which displays the Guacamole on-screen keyboard. + */ +@Component({ + selector: 'guac-osk', + templateUrl: './osk.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class OskComponent implements OnChanges { + + /** + * The URL for the Guacamole on-screen keyboard layout to use. + */ + @Input({ required: true }) layout!: string; + + /** + * The current on-screen keyboard, if any. + */ + private keyboard: Guacamole.OnScreenKeyboard | null = null; + + /** + * The main containing element for the entire directive. + */ + @ViewChild('osk') main!: ElementRef; + + /** + * Inject required services. + */ + constructor(private http: HttpClient, + private guacEventService: GuacEventService) { + } + + /** + * Load the new keyboard when the layout changes. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['layout']) { + this.onLayoutChange(changes['layout'].currentValue); + } + + } + + // Size keyboard to same size as main element + keyboardResized(): void { + + // Resize keyboard, if defined + if (this.keyboard) + this.keyboard.resize(this.main.nativeElement.offsetWidth); + + } + + /** + * Load the new keyboard. + * + * @param url + * The URL for the Guacamole on-screen keyboard layout to use. + */ + private onLayoutChange(url: string): void { + + // Remove current keyboard + if (this.keyboard) { + this.main.nativeElement.removeChild(this.keyboard.getElement()); + this.keyboard = null; + } + + // Load new keyboard + if (url) { + + // Retrieve layout JSON + this.http.get(url).subscribe( + // Build OSK with retrieved layout + (layout) => { + + // Abort if the layout changed while we were waiting for a response + if (this.layout !== url) + return; + + // Add OSK element + this.keyboard = new Guacamole.OnScreenKeyboard(layout); + this.main.nativeElement.appendChild(this.keyboard.getElement()); + + // Init size + this.keyboard.resize(this.main.nativeElement.offsetWidth); + + // Broadcast keydown for each key pressed + this.keyboard.onkeydown = (keysym: number) => { + this.guacEventService.broadcast('guacSyntheticKeydown', { keysym }); + }; + + // Broadcast keydown for each key released + this.keyboard.onkeyup = (keysym: number) => { + this.guacEventService.broadcast('guacSyntheticKeyup', { keysym }); + }; + }); + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/osk.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/osk.module.ts new file mode 100644 index 0000000000..8490630781 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/osk.module.ts @@ -0,0 +1,43 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { ElementModule } from '../element/element.module'; +import { OskComponent } from './components/osk/osk.component'; + +/** + * Module for displaying the Guacamole on-screen keyboard. + */ +@NgModule({ + declarations: [ + OskComponent + ], + exports: [ + OskComponent + ], + imports: [ + CommonModule, + ElementModule + ], + providers: [provideHttpClient(withInterceptorsFromDi())] +}) +export class OskModule { +} diff --git a/guacamole/src/main/frontend/src/app/osk/styles/osk.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/styles/osk.css similarity index 94% rename from guacamole/src/main/frontend/src/app/osk/styles/osk.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/styles/osk.css index 23a5a1d73a..5f56c560f6 100644 --- a/guacamole/src/main/frontend/src/app/osk/styles/osk.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/osk/styles/osk.css @@ -24,7 +24,7 @@ .guac-keyboard { display: inline-block; width: 100%; - + margin: 0; padding: 0; cursor: default; @@ -68,9 +68,9 @@ white-space: pre; text-shadow: 1px 1px 0 rgba(0, 0, 0, 0.25), - 1px -1px 0 rgba(0, 0, 0, 0.25), - -1px 1px 0 rgba(0, 0, 0, 0.25), - -1px -1px 0 rgba(0, 0, 0, 0.25); + 1px -1px 0 rgba(0, 0, 0, 0.25), + -1px 1px 0 rgba(0, 0, 0, 0.25), + -1px -1px 0 rgba(0, 0, 0, 0.25); } @@ -155,7 +155,7 @@ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; @@ -199,8 +199,8 @@ .guac-keyboard:not(.guac-keyboard-modifier-lat) .guac-keyboard-cap.guac-keyboard-requires-lat, -/* Hide keycaps NOT requiring modifiers which ARE currently active, where that - modifier is used to determine which cap is displayed for the current key. */ + /* Hide keycaps NOT requiring modifiers which ARE currently active, where that + modifier is used to determine which cap is displayed for the current key. */ .guac-keyboard.guac-keyboard-modifier-shift .guac-keyboard-key.guac-keyboard-uses-shift .guac-keyboard-cap:not(.guac-keyboard-requires-shift), @@ -218,7 +218,7 @@ .guac-keyboard-cap:not(.guac-keyboard-requires-lat) { display: none; - + } /* Fade out keys which do not use AltGr if AltGr is active */ diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-drag.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-drag.directive.ts new file mode 100644 index 0000000000..36ab944479 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-drag.directive.ts @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, HostListener, Input } from '@angular/core'; + +/** + * A directive which allows handling of drag gestures on a particular element. + */ +@Directive({ + selector: '[guacTouchDrag]', + standalone: false +}) +export class GuacTouchDragDirective { + + /** + * Called during a drag gesture as the user's finger is placed upon + * the element, moves, and is lifted from the element. + * + * @param inProgress + * Whether the gesture is currently in progress. This will + * always be true except when the gesture has ended, at which + * point one final call will occur with this parameter set to + * false. + * + * @param startX + * The X location at which the drag gesture began. + * + * @param startY + * The Y location at which the drag gesture began. + * + * @param currentX + * The current X location of the user's finger. + * + * @param currentY + * The current Y location of the user's finger. + * + * @param deltaX + * The difference in X location relative to the start of the + * gesture. + * + * @param deltaY + * The difference in Y location relative to the start of the + * gesture. + * + * @return {boolean} + * false if the default action of the touch event should be + * prevented, any other value otherwise. + */ + @Input({ required: true }) guacTouchDrag!: (inProgress: boolean, + startX: number, + startY: number, + currentX: number, + currentY: number, + deltaX: number, + deltaY: number) => boolean; + + /** + * Whether a drag gesture is in progress. + */ + inProgress = false; + + /** + * The starting X location of the drag gesture. + */ + startX?: number = undefined; + + /** + * The starting Y location of the drag gesture. + */ + startY?: number = undefined; + + /** + * The current X location of the drag gesture. + */ + currentX?: number = undefined; + + /** + * The current Y location of the drag gesture. + */ + currentY?: number = undefined; + + /** + * The change in X relative to drag start. + */ + deltaX = 0; + + /** + * The change in X relative to drag start. + */ + deltaY = 0; + + /** + * The element which will register the drag gesture. + */ + element: Element; + + constructor(private el: ElementRef) { + this.element = el.nativeElement; + } + + // When there is exactly one touch, monitor the change in location + @HostListener('touchmove', ['$event']) dragTouchMove(e: TouchEvent): void { + if (e.touches.length === 1) { + + // Get touch location + const x = e.touches[0].clientX; + const y = e.touches[0].clientY; + + // Init start location and deltas if gesture is starting + if (!this.startX || !this.startY) { + this.startX = this.currentX = x; + this.startY = this.currentY = y; + this.deltaX = 0; + this.deltaY = 0; + this.inProgress = true; + } + + // Update deltas if gesture is in progress + else if (this.inProgress) { + this.deltaX = x - (this.currentX as number); + this.deltaY = y - (this.currentY as number); + this.currentX = x; + this.currentY = y; + } + + // Signal start/change in drag gesture + if (this.inProgress) { + + if (this.guacTouchDrag(true, this.startX, this.startY, (this.currentX as number), + (this.currentY as number), this.deltaX, this.deltaY) === false) { + e.preventDefault(); + } + + } + + } + } + + @HostListener('touchend', ['$event']) dragTouchEnd(e: TouchEvent): void { + if (this.startX && this.startY && e.touches.length === 0) { + + // Signal end of drag gesture + if (this.inProgress && this.guacTouchDrag) { + + if (this.guacTouchDrag(true, this.startX, this.startY, (this.currentX as number), + (this.currentY as number), this.deltaX, this.deltaY) === false) { + e.preventDefault(); + } + + } + + this.startX = this.currentX = undefined; + this.startY = this.currentY = undefined; + this.deltaX = 0; + this.deltaY = 0; + this.inProgress = false; + + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-pinch.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-pinch.directive.ts new file mode 100644 index 0000000000..b4f66635b6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/directives/guac-touch-pinch.directive.ts @@ -0,0 +1,201 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, HostListener, Input } from '@angular/core'; + +/** + * A directive which allows handling of pinch gestures (pinch-to-zoom, for + * example) on a particular element. + */ +@Directive({ + selector: '[guacTouchPinch]', + standalone: false +}) +export class GuacTouchPinchDirective { + + /** + * Called when a pinch gesture begins, changes, or ends. + * + * @param inProgress + * Whether the gesture is currently in progress. This will + * always be true except when the gesture has ended, at which + * point one final call will occur with this parameter set to + * false. + * + * @param startLength + * The initial distance between the two touches of the + * pinch gesture, in pixels. + * + * @param currentLength + * The current distance between the two touches of the + * pinch gesture, in pixels. + * + * @param centerX + * The current X coordinate of the center of the pinch gesture. + * + * @param centerY + * The current Y coordinate of the center of the pinch gesture. + * + * @return {boolean} + * false if the default action of the touch event should be + * prevented, any other value otherwise. + */ + @Input({ required: true }) guacTouchPinch!: (inProgress: boolean, + startLength: number, + currentLength: number, + centerX: number, + centerY: number) => boolean; + + /** + * The starting pinch distance, or null if the gesture has not yet + * started. + */ + startLength?: number = undefined; + + /** + * The current pinch distance, or null if the gesture has not yet + * started. + */ + currentLength?: number = undefined; + + /** + * The X coordinate of the current center of the pinch gesture. + */ + centerX = 0; + + /** + * The Y coordinate of the current center of the pinch gesture. + */ + centerY = 0; + + /** + * The element which will register the pinch gesture. + */ + element: Element; + + constructor(private el: ElementRef) { + this.element = el.nativeElement; + } + + // When there are exactly two touches, monitor the distance between + // them, firing zoom events as appropriate + @HostListener('touchmove', ['$event']) pinchTouchMove(e: TouchEvent) { + if (e.touches.length === 2) { + + // Calculate current zoom level + this.currentLength = this.pinchDistance(e); + + // Calculate center + this.centerX = this.pinchCenterX(e); + this.centerY = this.pinchCenterY(e); + + // Init start length if pinch is not in progress + if (!this.startLength) + this.startLength = this.currentLength; + + // Notify of pinch status + if (this.guacTouchPinch) { + if (this.guacTouchPinch(true, this.startLength, this.currentLength, + this.centerX, this.centerY) === false) + e.preventDefault(); + } + + } + } + + // Reset monitoring and fire end event when done + @HostListener('touchend', ['$event']) pinchTouchEnd(e: TouchEvent) { + if (this.startLength && e.touches.length < 2) { + + // Notify of pinch end + if (this.guacTouchPinch) { + if (this.guacTouchPinch(false, this.startLength, (this.currentLength as number), + this.centerX, this.centerY) === false) { + e.preventDefault(); + } + } + + this.startLength = undefined; + + } + + } + + /** + * Given a touch event, calculates the distance between the first + * two touches in pixels. + * + * @param e + * The touch event to use when performing distance calculation. + * + * @return + * The distance in pixels between the first two touches. + */ + pinchDistance(e: TouchEvent): number { + + const touchA = e.touches[0]; + const touchB = e.touches[1]; + + const deltaX = touchA.clientX - touchB.clientX; + const deltaY = touchA.clientY - touchB.clientY; + + return Math.sqrt(deltaX * deltaX + deltaY * deltaY); + + } + + /** + * Given a touch event, calculates the center between the first two + * touches in pixels, returning the X coordinate of this center. + * + * @param e + * The touch event to use when performing center calculation. + * + * @return + * The X coordinate of the center of the first two touches. + */ + pinchCenterX(e: TouchEvent): number { + + const touchA = e.touches[0]; + const touchB = e.touches[1]; + + return (touchA.clientX + touchB.clientX) / 2; + + } + + /** + * Given a touch event, calculates the center between the first two + * touches in pixels, returning the Y coordinate of this center. + * + * @param e + * The touch event to use when performing center calculation. + * + * @return + * The Y coordinate of the center of the first two touches. + */ + pinchCenterY(e: TouchEvent): number { + + const touchA = e.touches[0]; + const touchB = e.touches[1]; + + return (touchA.clientY + touchB.clientY) / 2; + + } + + +} diff --git a/guacamole/src/main/frontend/src/app/touch/touchModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/touch.module.ts similarity index 62% rename from guacamole/src/main/frontend/src/app/touch/touchModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/touch.module.ts index 751c9aee13..51050ef9e3 100644 --- a/guacamole/src/main/frontend/src/app/touch/touchModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/lib/touch/touch.module.ts @@ -17,7 +17,27 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { GuacTouchDragDirective } from './directives/guac-touch-drag.directive'; +import { GuacTouchPinchDirective } from './directives/guac-touch-pinch.directive'; + + /** * Module for handling common touch gestures, like panning or pinch-to-zoom. */ -angular.module('touch', []); +@NgModule({ + declarations: [ + GuacTouchPinchDirective, + GuacTouchDragDirective + ], + imports : [ + CommonModule + ], + exports : [ + GuacTouchPinchDirective, + GuacTouchDragDirective + ] +}) +export class TouchModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/public-api.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/public-api.ts new file mode 100644 index 0000000000..d8f640acd9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/public-api.ts @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* + * Public API Surface of guacamole-frontend-lib + */ + +// Client lib +export * from './lib/client/client.module'; +export * from './lib/client/components/viewport.component'; +export * from './lib/client/services/guac-audio.service'; +export * from './lib/client/services/guac-image.service'; +export * from './lib/client/services/guac-video.service'; + +// Element +export * from './lib/element/element.module'; +export * from './lib/element/directives/guac-click.directive'; +export * from './lib/element/directives/guac-drop.directive'; +export * from './lib/element/directives/guac-focus.directive'; +export * from './lib/element/directives/guac-resize.directive'; +export * from './lib/element/directives/guac-scroll.directive'; +export * from './lib/element/directives/guac-upload.directive'; +export * from './lib/element/types/ScrollState'; + +// Events +export * from './lib/events/types/GuacEvent'; +export * from './lib/events/types/GuacEventArguments'; +export * from './lib/events/services/guac-event.service'; + +// OSK +export * from './lib/osk/osk.module'; +export * from './lib/osk/components/osk/osk.component'; + +// Touch +export * from './lib/touch/touch.module'; +export * from './lib/touch/directives/guac-touch-pinch.directive'; +export * from './lib/touch/directives/guac-touch-drag.directive'; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/click-helper.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/click-helper.ts new file mode 100644 index 0000000000..a046246495 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/click-helper.ts @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * TODO: Document + * @param element + * @param eventName + * @param coordX + * @param coordY + */ +const dispatchEvent = (element: any, eventName: string, coordX: any, coordY: any) => { + element.dispatchEvent(new MouseEvent(eventName, { + view : window, + bubbles : true, + cancelable: true, + clientX : coordX, + clientY : coordY, + button : 0 + })); +}; + +/** + * TODO: Document + * @param element + */ +export const simulateMouseClick = (element: any) => { + const box = element.getBoundingClientRect(); + const coordX = box.left + (box.right - box.left) / 2; + const coordY = box.top + (box.bottom - box.top) / 2; + + dispatchEvent(element, 'mousedown', coordX, coordY); + dispatchEvent(element, 'mouseup', coordX, coordY); + dispatchEvent(element, 'click', coordX, coordY); +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/test-scheduler.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/test-scheduler.ts new file mode 100644 index 0000000000..78eb457836 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/src/test-utils/test-scheduler.ts @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestScheduler } from 'rxjs/testing'; + +/** + * Provides a TestScheduler that can be used for testing RxJS observables ("marble tests"). + */ +export const getTestScheduler = () => new TestScheduler((actual, expected) => { + // asserting the two objects are equal - required + // for TestScheduler assertions to work via your test framework + // e.g. using chai. + expect(actual).toEqual(expected); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.json new file mode 100644 index 0000000000..46d2b3a167 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.json @@ -0,0 +1,20 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/lib", + "declaration": true, + "declarationMap": true, + "inlineSources": true, + "types": [ + "global" + ] + }, + "exclude": [ + "**/*.spec.ts" + ], + "include": [ + "**/*.ts", + "**/*.d.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.prod.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.prod.json new file mode 100644 index 0000000000..560c23915f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.lib.prod.json @@ -0,0 +1,10 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.lib.json", + "compilerOptions": { + "declarationMap": false + }, + "angularCompilerOptions": { + "compilationMode": "partial" + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.spec.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.spec.json new file mode 100644 index 0000000000..44207f23a6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend-lib/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "global" + ] + }, + "include": [ + "**/*.spec.ts", + "**/*.d.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/federation.config.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/federation.config.js new file mode 100644 index 0000000000..57a6953308 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/federation.config.js @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +const {withNativeFederation, shareAll} = require('@angular-architects/native-federation/config'); + +module.exports = withNativeFederation({ + + name: 'guacamole-frontend', + + shared: { + ...shareAll({singleton: true, strictVersion: true, requiredVersion: 'auto'}), + }, + + // Packages that should not be shared or are not needed at runtime + skip: [ + 'rxjs/ajax', + 'rxjs/fetch', + 'rxjs/testing', + 'rxjs/webSocket', + 'csv' + ] + + // Please read our FAQ about sharing libs: + // https://shorturl.at/jmzH0 + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.ci.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.ci.js new file mode 100644 index 0000000000..4c2a098b2b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.ci.js @@ -0,0 +1,73 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Karma configuration file for CI. +// Run the tests once in headless Firefox and exit. +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-firefox-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + random: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/guacamole'), + subdir: '.', + reporters: [ + {type: 'html'}, + {type: 'text-summary'} + ] + }, + reporters: ['progress', 'kjhtml'], + + // Run the tests once and exit + singleRun: true, + + // Disable automatic test running on changed files + autoWatch: false, + + // Use a headless firefox browser to run the tests + browsers: ['FirefoxHeadless'], + customLaunchers: { + 'FirefoxHeadless': { + base: 'Firefox', + flags: [ + '--headless' + ] + } + } + }); +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.js new file mode 100644 index 0000000000..7d2b90d4de --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/karma.conf.js @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +// Karma configuration file for development. +module.exports = function (config) { + config.set({ + basePath: '', + frameworks: ['jasmine', '@angular-devkit/build-angular'], + plugins: [ + require('karma-jasmine'), + require('karma-firefox-launcher'), + require('karma-jasmine-html-reporter'), + require('karma-coverage'), + require('@angular-devkit/build-angular/plugins/karma') + ], + client: { + jasmine: { + // you can add configuration options for Jasmine here + // the possible options are listed at https://jasmine.github.io/api/edge/Configuration.html + // for example, you can disable the random execution with `random: false` + // or set a specific seed with `seed: 4321` + random: false + }, + clearContext: false // leave Jasmine Spec Runner output visible in browser + }, + jasmineHtmlReporter: { + suppressAll: true // removes the duplicated traces + }, + coverageReporter: { + dir: require('path').join(__dirname, '../../coverage/guacamole'), + subdir: '.', + reporters: [ + {type: 'html'}, + {type: 'text-summary'} + ] + }, + reporters: ['progress', 'kjhtml'], + browsers: ['Firefox'], + restartOnFileChange: true + }); +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app-routing.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app-routing.module.ts new file mode 100644 index 0000000000..e009835a8e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app-routing.module.ts @@ -0,0 +1,353 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { inject, NgModule } from '@angular/core'; +import { ActivatedRouteSnapshot, CanActivateFn, ResolveFn, Route, Router, RouterModule, Routes } from '@angular/router'; +import { TranslocoService } from '@ngneat/transloco'; +import { catchError, map, Observable, of, switchMap, take } from 'rxjs'; +import { AuthenticationService } from './auth/service/authentication.service'; +import { AuthenticationResult } from './auth/types/AuthenticationResult'; +import { ClientPageComponent } from './client/components/client-page/client-page.component'; +import { HomeComponent } from './home/components/home/home.component'; +import { + ConnectionImportFileHelpComponent +} from './import/components/connection-import-file-help/connection-import-file-help.component'; +import { ImportConnectionsComponent } from './import/components/import-connections/import-connections.component'; +import { + ManageConnectionGroupComponent +} from './manage/components/manage-connection-group/manage-connection-group.component'; +import { ManageConnectionComponent } from './manage/components/manage-connection/manage-connection.component'; +import { + ManageSharingProfileComponent +} from './manage/components/manage-sharing-profile/manage-sharing-profile.component'; +import { ManageUserGroupComponent } from './manage/components/manage-user-group/manage-user-group.component'; +import { ManageUserComponent } from './manage/components/manage-user/manage-user.component'; +import { UserPageService } from './manage/services/user-page.service'; +import { + ConnectionHistoryPlayerComponent +} from './settings/components/connection-history-player/connection-history-player.component'; +import { + GuacSettingsConnectionHistoryComponent +} from './settings/components/guac-settings-connection-history/guac-settings-connection-history.component'; +import { + GuacSettingsConnectionsComponent +} from './settings/components/guac-settings-connections/guac-settings-connections.component'; +import { + GuacSettingsPreferencesComponent +} from './settings/components/guac-settings-preferences/guac-settings-preferences.component'; +import { + GuacSettingsSessionsComponent +} from './settings/components/guac-settings-sessions/guac-settings-sessions.component'; +import { + GuacSettingsUserGroupsComponent +} from './settings/components/guac-settings-user-groups/guac-settings-user-groups.component'; +import { GuacSettingsUsersComponent } from './settings/components/guac-settings-users/guac-settings-users.component'; +import { SettingsComponent } from './settings/components/settings/settings.component'; + + +/** + * Redirects the user to their home page. This necessarily requires + * attempting to re-authenticate with the Guacamole server, as the user's + * credentials may have changed, and thus their most-appropriate home page + * may have changed as well. + * + * @param route + * The route which was requested. + * + * @returns {Promise} + * A promise which resolves successfully only after an attempt to + * re-authenticate and determine the user's proper home page has been + * made. + */ +const routeToUserHomePage: CanActivateFn = (route: ActivatedRouteSnapshot) => { + + // Required services + const router = inject(Router); + const userPageService = inject(UserPageService); + + + // Re-authenticate including any parameters in URL + return updateCurrentToken(route) + .pipe( + switchMap(() => + + // Redirect to home page + userPageService.getHomePage() + .pipe( + map(homePage => { + // If home page is the requested location, allow through + if (route.root.url.join('/') || '/' === homePage.url) + return true; + + // Otherwise, reject and reroute + else { + return router.parseUrl(homePage.url); + } + } + ), + + // If retrieval of home page fails, assume requested page is OK + catchError(() => of(true)) + ) + ), + catchError(() => of(false)), + ); + +}; + + +/** + * Attempts to re-authenticate with the Guacamole server, sending any + * query parameters in the URL, along with the current auth token, and + * updating locally stored token if necessary. + * + * @param route + * The route which was requested. + * + * @returns + * An observable which completes only after an attempt to + * re-authenticate has been made. If the authentication attempt fails, + * the observable will emit an error. + */ +const updateCurrentToken = (route: ActivatedRouteSnapshot): Observable => { + + // Required services + const authenticationService = inject(AuthenticationService); + + // Re-authenticate including any parameters in URL + return authenticationService.updateCurrentToken(route.queryParams); + +}; + +/** + * Guard which prevents access to routes which require authentication if + * the user is not authenticated. + * + * @param route + * The route which was requested. + * + * @returns + * An observable which emits true if the user is authenticated, or false + * if the user is not authenticated. + */ +const authGuard: CanActivateFn = (route: ActivatedRouteSnapshot): Observable => { + + return updateCurrentToken(route) + .pipe( + map(() => true), + catchError(() => of(false)) + ); +}; + +/** + * Uses the TranslocoService to resolve the title of the route for the + * current language. + * + * @param route + * The route which was requested. + */ +export const titleResolver: ResolveFn = (route) => { + + const translocoService = inject(TranslocoService); + const titleKey = route.data['titleKey'] || 'APP.NAME'; + + return translocoService.selectTranslate(titleKey).pipe(take(1)); + +}; + +/** + * Configure each possible route. + */ +export const appRoutes: Routes = [ + + // Home screen + { + path : '', + component: HomeComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'home' }, + // Run the canActivate guard on every navigation, even if the route hasn't changed + runGuardsAndResolvers: 'always', + canActivate : [routeToUserHomePage] + }, + + // Connection import page + { + path : 'import/:dataSource/connection', + component : ImportConnectionsComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'settings' }, + canActivate: [authGuard] + }, + + // Connection import file format help page + { + path : 'import/connection/file-format-help', + component : ConnectionImportFileHelpComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'settings' }, + canActivate: [authGuard] + }, + + // Management screen + { + path : 'settings', + component : SettingsComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'settings' }, + canActivate: [authGuard], + children : [ + { path: 'users', component: GuacSettingsUsersComponent }, + { path: 'userGroups', component: GuacSettingsUserGroupsComponent }, + { path: ':dataSource/connections', component: GuacSettingsConnectionsComponent }, + { path: ':dataSource/history', component: GuacSettingsConnectionHistoryComponent }, + { path: 'sessions', component: GuacSettingsSessionsComponent }, + { path: 'preferences', component: GuacSettingsPreferencesComponent } + ] + }, + + // Connection editor + { + path : 'manage/:dataSource/connections/:id', + component : ManageConnectionComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Connection editor for creating a new connection + { + path : 'manage/:dataSource/connections', + component : ManageConnectionComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Sharing profile editor + { + path : 'manage/:dataSource/sharingProfiles/:id', + component : ManageSharingProfileComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Sharing profile editor for creating a new sharing profile + { + path : 'manage/:dataSource/sharingProfiles', + component : ManageSharingProfileComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Connection group editor + { + path : 'manage/:dataSource/connectionGroups/:id', + component : ManageConnectionGroupComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Connection group editor for creating a new connection group + { + path : 'manage/:dataSource/connectionGroups', + component : ManageConnectionGroupComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // User editor + { + path : 'manage/:dataSource/users/:id', + component : ManageUserComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // User editor for creating a new user + { + path : 'manage/:dataSource/users', + component : ManageUserComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // User group editor + { + path : 'manage/:dataSource/userGroups/:id', + component : ManageUserGroupComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // User group editor for creating a new user group + { + path : 'manage/:dataSource/userGroups', + component : ManageUserGroupComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'manage' }, + canActivate: [authGuard] + }, + + // Recording player + { + path : 'settings/:dataSource/recording/:identifier/:name', + component : ConnectionHistoryPlayerComponent, + title : titleResolver, + data : { titleKey: 'APP.NAME', bodyClassName: 'settings' }, + canActivate: [authGuard] + }, + + // Client view + { + path : 'client/:id', + component : ClientPageComponent, + data : { titleKey: 'APP.NAME', bodyClassName: 'client' }, + canActivate: [authGuard] + } + +]; + +/** + * Route to the home page for the current user. + * Defined as a separate route so that extension routes + * can be added before the redirect. + */ +export const fallbackRoute: Route = { + path: '**', redirectTo: '' +}; + +@NgModule({ + imports: [RouterModule.forRoot([...appRoutes, fallbackRoute], { + // Bind route parameters to component inputs + bindToComponentInputs: true, + // Disable HTML5 mode (use # for routing) + useHash: true + })], + exports: [RouterModule] +}) +export class AppRoutingModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.html new file mode 100644 index 0000000000..d266330e9d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.html @@ -0,0 +1,83 @@ + + +
    + + +
    + +
    +

    +

    + +

    +
    +
    +
    + + +
    + +
    +

    +

    +
    +
    +
    + + + + + + + + +
    + + + + + + + + + +
    + +
    + +
    +
    + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.ts new file mode 100644 index 0000000000..d1d6c2bb79 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.component.ts @@ -0,0 +1,427 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { AfterViewChecked, Component, DestroyRef, Inject, OnInit, Renderer2, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { NavigationEnd, NavigationStart, Router, RoutesRecognized } from '@angular/router'; +import { TranslocoService } from '@ngneat/transloco'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { distinctUntilChanged, filter, map, pairwise, startWith, take } from 'rxjs'; +import { AuthenticationService } from './auth/service/authentication.service'; +import { GuacClientManagerService } from './client/services/guac-client-manager.service'; +import { ManagedClientGroup } from './client/types/ManagedClientGroup'; +import { ManagedClientState } from './client/types/ManagedClientState'; +import { ClipboardService } from './clipboard/services/clipboard.service'; +import { GuacFrontendEventArguments } from './events/types/GuacFrontendEventArguments'; +import { ApplyPatchesService } from './index/services/apply-patches.service'; +import { StyleLoaderService } from './index/services/style-loader.service'; +import { GuacNotificationService } from './notification/services/guac-notification.service'; +import { Error } from './rest/types/Error'; +import { Field } from './rest/types/Field'; +import { TranslatableMessage } from './rest/types/TranslatableMessage'; +import { ApplicationState } from './util/ApplicationState'; + +/** + * The number of milliseconds that should elapse between client-side + * session checks. This DOES NOT impact whether a session expires at all; + * such checks will always be server-side. This only affects how quickly + * the client-side view can recognize that a user's session has expired + * absent any action taken by the user. + */ +const SESSION_VALIDITY_RECHECK_INTERVAL = 15000; + +/** + * Name of the file that contains additional styles provided by extensions. + */ +const EXTENSION_STYLES_FILENAME = 'app.css'; + +/** + * The Component for the root of the application. + */ +@Component({ + selector: 'guac-root', + templateUrl: './app.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class AppComponent implements OnInit, AfterViewChecked { + + /** + * The error that prevents the current page from rendering at all. If no + * such error has occurred, this will be null. + */ + fatalError: Error | null = null; + + /** + * The message to display to the user as instructions for the login + * process. + */ + loginHelpText: TranslatableMessage | null = null; + + /** + * Whether the user has selected to log back in after having logged out. + */ + reAuthenticating = false; + + /** + * The credentials that the authentication service is has already accepted, + * pending additional credentials, if any. If credentials have + * been accepted, this will contain a form control for each of + * the parameters submitted in a previous authentication attempt. + */ + acceptedCredentials: FormGroup = new FormGroup({}); + + /** + * The credentials that the authentication service is currently expecting, + * if any. If the user is logged in, this will be null. + */ + expectedCredentials: Field[] | null = null; + + /** + * The current overall state of the client side of the application. + * Possible values are defined by {@link ApplicationState}. + */ + applicationState: ApplicationState = ApplicationState.LOADING; + + /** + * Provide access to the ApplicationState enum within the template. + */ + readonly ApplicationState = ApplicationState; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + protected notificationService: GuacNotificationService, + private guacEventService: GuacEventService, + private guacClientManager: GuacClientManagerService, + private clipboardService: ClipboardService, + private applyPatchesService: ApplyPatchesService, + private styleLoaderService: StyleLoaderService, + private translocoService: TranslocoService, + private router: Router, + private title: Title, + @Inject(DOCUMENT) private document: Document, + private renderer: Renderer2, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + + // Load extension styles + this.styleLoaderService.loadStyle(EXTENSION_STYLES_FILENAME); + + // Add default destination for input events + const sink = new Guacamole.InputSink(); + this.document.body.appendChild(sink.getElement()); + + // Create event listeners at the global level + const keyboard = new Guacamole.Keyboard(this.document); + keyboard.listenTo(sink.getElement()); + + // Broadcast keydown events + keyboard.onkeydown = keysym => { + + // Do not handle key events if not logged in + if (this.applicationState !== ApplicationState.READY) + return true; + + // Warn of pending keydown + const guacBeforeKeydownEvent = this.guacEventService.broadcast('guacBeforeKeydown', + { keysym, keyboard } + ); + if (guacBeforeKeydownEvent.defaultPrevented) + return true; + + // If not prevented via guacBeforeKeydown, fire corresponding keydown event + const guacKeydownEvent = this.guacEventService.broadcast('guacKeydown', { + keysym, + keyboard + }); + return !guacKeydownEvent.defaultPrevented; + + }; + + // Broadcast keyup events + keyboard.onkeyup = keysym => { + + // Do not handle key events if not logged in or if a notification is + // shown + if (this.applicationState !== ApplicationState.READY) + return; + + // Warn of pending keyup + const guacBeforeKeydownEvent = this.guacEventService.broadcast('guacBeforeKeyup', { + keysym, + keyboard + }); + if (guacBeforeKeydownEvent.defaultPrevented) + return; + + // If not prevented via guacBeforeKeyup, fire corresponding keydown event + this.guacEventService.broadcast('guacKeyup', { keysym, keyboard }); + + }; + + // Release all keys when window loses focus + window.onblur = () => { + keyboard.reset(); + }; + + // If we're logged in and not connected to anything, periodically check + // whether the current session is still valid. If the session has expired, + // refresh the auth state to reshow the login screen (rather than wait for + // the user to take some action and discover that they are not logged in + // after all). There is no need to do this if a connection is active as + // that connection activity will already automatically check session + // validity. + window.setInterval(() => { + if (!!this.authenticationService.getCurrentToken() && !this.hasActiveTunnel()) { + this.authenticationService.getValidity().subscribe((valid) => { + if (!valid) + this.reAuthenticate(); + }); + } + }, SESSION_VALIDITY_RECHECK_INTERVAL); + + // Release all keys upon form submission (there may not be corresponding + // keyup events for key presses involved in submitting a form) + this.document.addEventListener('submit', () => { + keyboard.reset(); + }); + + // Attempt to read the clipboard if it may have changed + window.addEventListener('load', this.clipboardService.resyncClipboard, true); + window.addEventListener('copy', this.clipboardService.resyncClipboard); + window.addEventListener('cut', this.clipboardService.resyncClipboard); + window.addEventListener('focus', (e: FocusEvent) => { + + // Only recheck clipboard if it's the window itself that gained focus + if (e.target === window) + this.clipboardService.resyncClipboard(); + + }, true); + + + // Display login screen if a whole new set of credentials is needed + this.guacEventService + .on('guacInvalidCredentials') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ error }) => { + + this.setApplicationState(ApplicationState.AWAITING_CREDENTIALS); + + this.loginHelpText = null; + this.acceptedCredentials = new FormGroup({}); + this.expectedCredentials = error.expected; + + }); + + // Prompt for remaining credentials if provided credentials were not enough + this.guacEventService + .on('guacInsufficientCredentials') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ parameters, error }) => { + + this.setApplicationState(ApplicationState.AWAITING_CREDENTIALS); + + this.loginHelpText = error.translatableMessage; + this.acceptedCredentials = parameters as any; // TODO: Map to FormGroup + this.expectedCredentials = error.expected; + + }); + + // Alert user to authentication errors that occur in the absence of an + // interactive login form + this.guacEventService.on('guacLoginFailed') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ error }) => { + + // All errors related to an interactive login form are handled elsewhere + if (this.applicationState === ApplicationState.AWAITING_CREDENTIALS + || error.type === Error.Type.INSUFFICIENT_CREDENTIALS + || error.type === Error.Type.INVALID_CREDENTIALS) + return; + + this.setApplicationState(ApplicationState.AUTOMATIC_LOGIN_REJECTED); + this.reAuthenticating = false; + this.fatalError = error; + + }); + + // Replace absolutely all content with an error message if the page itself + // cannot be displayed due to an error + this.guacEventService.on('guacFatalPageError') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ error }) => { + this.setApplicationState(ApplicationState.FATAL_ERROR); + this.fatalError = error; + }); + + // Replace the overall user interface with an informational message if the + // user has manually logged out + this.guacEventService.on('guacLogout') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.setApplicationState(ApplicationState.LOGGED_OUT); + this.reAuthenticating = false; + }); + + // Set the initial title manually when the application starts + this.translocoService.selectTranslate('APP.NAME') + .pipe(take(1)) + .subscribe((title) => this.title.setTitle(title)); + + // Ensure new pages always start with clear keyboard state + this.router.events + .pipe(filter(event => event instanceof NavigationStart)) + .subscribe(() => keyboard.reset()); + + + // Clear login screen if route change was successful (and thus + // login was either successful or not required) + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => this.setApplicationState(ApplicationState.READY)); + + // Set body CSS class based on current route + this.router.events.pipe( + // Check if a new class needs to be applied every time the route changes + filter((event): event is RoutesRecognized => event instanceof RoutesRecognized), + // Get the current route data + map((event) => event.state.root.firstChild?.data || {}), + // Extract the bodyClassName property + filter((data) => !!data['bodyClassName']), + map((data) => data['bodyClassName']), + // Only proceed if the class has changed + distinctUntilChanged(), + // Start with null so that the first class is always applied + startWith(null), + // Emit previous and current class name as a pair + pairwise() + ) + .subscribe(([previousClass, nextClass]) => { + this.updateBodyClass(previousClass, nextClass); + }); + + } + + + /** + * Returns whether the current user has at least one active connection + * running within the current tab. + * + * @returns + * true if the current user has at least one active connection running + * in the current browser tab, false otherwise. + */ + private hasActiveTunnel(): boolean { + + const clients = this.guacClientManager.getManagedClients(); + for (const id in clients) { + + switch (clients[id].clientState.connectionState) { + case ManagedClientState.ConnectionState.CONNECTING: + case ManagedClientState.ConnectionState.WAITING: + case ManagedClientState.ConnectionState.CONNECTED: + return true; + } + + } + + return false; + + } + + /** + * Sets the current overall state of the client side of the + * application to the given value. Possible values are defined by + * {@link ApplicationState}. + * + * @param state + * The state to assign, as defined by {@link ApplicationState}. + */ + private setApplicationState(state: ApplicationState): void { + this.applicationState = state; + + + // TODO: In which cases is this necessary? + // The title and class associated with the + // current page are automatically reset to the standard values applicable + // to the application as a whole (rather than any specific page). + // this.page.bodyClassName = ''; + // this.title.setTitle('APP.NAME'); + } + + /** + * Navigates the user back to the root of the application (or reloads the + * current route and controller if the user is already there), effectively + * forcing reauthentication. If the user is not logged in, this will result + * in the login screen appearing. + */ + reAuthenticate(): void { + + this.reAuthenticating = true; + + // Clear out URL state to conveniently bring user back to home screen + // upon relogin + this.router.navigate(['/'], { onSameUrlNavigation: 'reload' }); + } + + /** + * Updates the classes of the body element. The previous class is removed + * and the next class is added. + * + * @param previousClass + * The class to remove from the body element. + * + * @param nextClass + * The class to add to the body element. + */ + private updateBodyClass(previousClass?: string | null, nextClass?: string | null): void { + // Remove previous class + if (previousClass) + this.renderer.removeClass(this.document.body, previousClass); + + // Add the new class + if (nextClass) + this.renderer.addClass(this.document.body, nextClass); + + } + + /** + * @borrows GuacClientManagerService.getManagedClientGroups + */ + getManagedClientGroups(): ManagedClientGroup[] { + return this.guacClientManager.getManagedClientGroups(); + } + + /** + * Apply HTML patches each time the DOM could be updated. + */ + ngAfterViewChecked(): void { + this.applyPatchesService.applyPatches(); + } + +} + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.module.ts new file mode 100644 index 0000000000..ea69296a61 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/app.module.ts @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { AppRoutingModule } from './app-routing.module'; +import { AppComponent } from './app.component'; +import { AuthenticationInterceptor } from './auth/interceptor/authentication.interceptor'; +import { ClientModule } from './client/client.module'; +import { FormModule } from './form/form.module'; +import { HomeModule } from './home/home.module'; +import { ImportModule } from './import/import.module'; +import { DefaultHeadersInterceptor } from './index/config/default-headers.interceptor'; +import { ExtensionLoaderService } from './index/services/extension-loader.service'; +import { ListModule } from './list/list.module'; +import { LocaleModule } from './locale/locale.module'; +import { LoginModule } from './login/login.module'; +import { ManageModule } from './manage/manage.module'; +import { NavigationModule } from './navigation/navigation.module'; +import { NotificationModule } from './notification/notification.module'; +import { ErrorHandlingInterceptor } from './rest/interceptor/error-handling.interceptor'; +import { SettingsModule } from './settings/settings.module'; + +@NgModule({ declarations: [ + AppComponent, + ], + bootstrap: [AppComponent], imports: [BrowserModule, + CommonModule, + LocaleModule, + AppRoutingModule, + NavigationModule, + NotificationModule, + FormModule, + LoginModule, + ListModule, + SettingsModule, + ManageModule, + HomeModule, + ClientModule, + ImportModule], providers: [ + // Uses the extension loader service to load the extension and set the router config + // before the app is initialized. + provideAppInitializer(() => { + const initializerFn = ((extensionLoaderService: ExtensionLoaderService) => () => extensionLoaderService.loadExtensionAndSetRouterConfig())(inject(ExtensionLoaderService)); + return initializerFn(); + }), + { provide: HTTP_INTERCEPTORS, useClass: DefaultHeadersInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }, + { provide: HTTP_INTERCEPTORS, useClass: ErrorHandlingInterceptor, multi: true }, + provideHttpClient(withInterceptorsFromDi()), + ] }) +export class AppModule { +} diff --git a/guacamole/src/main/frontend/src/app/auth/authModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/auth.module.ts similarity index 81% rename from guacamole/src/main/frontend/src/app/auth/authModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/auth.module.ts index 84c3e33175..c46db42923 100644 --- a/guacamole/src/main/frontend/src/app/auth/authModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/auth.module.ts @@ -17,10 +17,17 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + /** * The module for authentication and management of tokens. */ -angular.module('auth', [ - 'rest', - 'storage' -]); +@NgModule({ + declarations: [], + imports : [ + CommonModule + ] +}) +export class AuthModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/interceptor/authentication.interceptor.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/interceptor/authentication.interceptor.ts new file mode 100644 index 0000000000..2f387c5bb0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/interceptor/authentication.interceptor.ts @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { InterceptorService } from '../../util/interceptor.service'; +import { AuthenticationService } from '../service/authentication.service'; + +/** + * Interceptor which automatically includes the user's current authentication token. + */ +@Injectable() +export class AuthenticationInterceptor implements HttpInterceptor { + + /** + * Inject required services. + */ + constructor(private interceptorService: InterceptorService, + private authenticationService: AuthenticationService) { + } + + /** + * Makes an HTTP request automatically including the given authentication + * token via {@link AUTHENTICATION_TOKEN} using the "Guacamole-Token" header. + * If no token is provided, the user's current authentication token is used instead. + * If the user is not logged in, the "Guacamole-Token" header is simply omitted. + */ + intercept(request: HttpRequest, next: HttpHandler): Observable> { + + if (this.interceptorService.skipAuthenticationInterceptor(request)) { + return next.handle(request); + } + + // Attempt to use current token if none is provided in the request context + const token = this.interceptorService.getAuthenticationToken(request) || this.authenticationService.getCurrentToken(); + + // Add "Guacamole-Token" header if an authentication token is available + if (token) { + request = request.clone({ + setHeaders: { + 'Guacamole-Token': token + } + }); + } + + return next.handle(request); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.spec.ts new file mode 100644 index 0000000000..b9031e4a60 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.spec.ts @@ -0,0 +1,66 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HTTP_INTERCEPTORS, provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { AuthenticationInterceptor } from '../interceptor/authentication.interceptor'; + +import { AuthenticationService } from './authentication.service'; + +describe('AuthenticationService', () => { + let service: AuthenticationService; + let httpMock: HttpTestingController; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [ + { provide: HTTP_INTERCEPTORS, useClass: AuthenticationInterceptor, multi: true }, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting() + ] +}); + service = TestBed.inject(AuthenticationService); + httpMock = TestBed.inject(HttpTestingController); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('revokeToken()', () => { + it('request should contain the authentication token as a HTTP header', (done) => { + const AUTHENTICATION_TOKEN = 'Guacamole-Token'; + const token = 'test-token'; + + service.revokeToken(token).subscribe(() => { + done(); + }); + + const req = httpMock.expectOne('api/session'); + expect(req.request.method).toEqual('DELETE'); + expect(req.request.headers.has(AUTHENTICATION_TOKEN)).toBeTruthy(); + expect(req.request.headers.get(AUTHENTICATION_TOKEN)).toEqual(token); + req.flush(null); + }); + + }); + +}); diff --git a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/auth/service/authenticationService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.ts index af5a9ab6c3..00e2aa2149 100644 --- a/guacamole/src/main/frontend/src/app/auth/service/authenticationService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/service/authentication.service.ts @@ -17,6 +17,23 @@ * under the License. */ +import { HttpClient, HttpContext, HttpHeaders, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { catchError, from, map, Observable, of, switchMap, throwError } from 'rxjs'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { RequestService } from '../../rest/service/request.service'; +import { Error } from '../../rest/types/Error'; +import { LocalStorageService } from '../../storage/local-storage.service'; +import { AUTHENTICATION_TOKEN } from '../../util/interceptor.service'; +import { AuthenticationResult } from '../types/AuthenticationResult'; + +/** + * The unique identifier of the local storage key which stores the latest + * authentication token. + */ +const AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN'; + /** * A service for authenticating a user against the REST API. Invoking the * authenticate() or login() functions of this service will automatically @@ -60,36 +77,22 @@ * invalid. This event receives two parameters: the HTTP parameters * submitted and the Error object received from the REST endpoint. */ -angular.module('auth').factory('authenticationService', ['$injector', - function authenticationService($injector) { - - // Required types - var AuthenticationResult = $injector.get('AuthenticationResult'); - var Error = $injector.get('Error'); - - // Required services - var $q = $injector.get('$q'); - var $rootScope = $injector.get('$rootScope'); - var localStorageService = $injector.get('localStorageService'); - var requestService = $injector.get('requestService'); - - var service = {}; +@Injectable({ + providedIn: 'root' +}) +export class AuthenticationService { /** * The most recent authentication result, or null if no authentication * result is cached. - * - * @type AuthenticationResult */ - var cachedResult = null; + cachedResult: AuthenticationResult | null = null; - /** - * The unique identifier of the local storage key which stores the latest - * authentication token. - * - * @type String - */ - var AUTH_TOKEN_STORAGE_KEY = 'GUAC_AUTH_TOKEN'; + constructor(private localStorageService: LocalStorageService, + private guacEventService: GuacEventService, + private requestService: RequestService, + private http: HttpClient) { + } /** * Retrieves the authentication result cached in memory. If the user has not @@ -99,65 +102,65 @@ angular.module('auth').factory('authenticationService', ['$injector', * NOTE: setAuthenticationResult() will be called upon page load, so the * cache should always be populated after the page has successfully loaded. * - * @returns {AuthenticationResult} + * @returns * The last successful authentication result, or null if the user is not * currently authenticated. */ - var getAuthenticationResult = function getAuthenticationResult() { + getAuthenticationResult(): AuthenticationResult | null { // Use cached result, if any - if (cachedResult) - return cachedResult; + if (this.cachedResult) + return this.cachedResult; - // Return explicit null if no auth data is currently stored + // Return explicit undefined if no auth data is currently stored return null; - }; + } /** * Stores the given authentication result for future retrieval. The given * result MUST be the result of the most recent authentication attempt. * - * @param {AuthenticationResult} data + * @param data * The last successful authentication result, or null if the last * authentication attempt failed. */ - var setAuthenticationResult = function setAuthenticationResult(data) { + setAuthenticationResult(data: AuthenticationResult | null): void { // Clear the currently-stored result and auth token if the last // attempt failed if (!data) { - cachedResult = null; - localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY); + this.cachedResult = null; + this.localStorageService.removeItem(AUTH_TOKEN_STORAGE_KEY); } - // Otherwise, store the authentication attempt directly. - // Note that only the auth token is stored in persistent local storage. - // To re-obtain an autentication result upon a fresh page load, - // reauthenticate with the persistent token, which can be obtained by + // Otherwise, store the authentication attempt directly. + // Note that only the auth token is stored in persistent local storage. + // To re-obtain an authentication result upon a fresh page load, + // reauthenticate with the persistent token, which can be obtained by // calling getCurrentToken(). else { // Always store in cache - cachedResult = data; + this.cachedResult = data; // Persist only the auth token past tab/window closure, and only // if not anonymous if (data.username !== AuthenticationResult.ANONYMOUS_USERNAME) - localStorageService.setItem( - AUTH_TOKEN_STORAGE_KEY, data.authToken); + this.localStorageService.setItem( + AUTH_TOKEN_STORAGE_KEY, data.authToken); } - }; + } /** * Clears the stored authentication result, if any. If no authentication * result is currently stored, this function has no effect. */ - var clearAuthenticationResult = function clearAuthenticationResult() { - setAuthenticationResult(null); - }; + clearAuthenticationResult() { + this.setAuthenticationResult(null); + } /** * Makes a request to authenticate a user using the token REST API endpoint @@ -166,56 +169,49 @@ angular.module('auth').factory('authenticationService', ['$injector', * authentication data can be retrieved later via getCurrentToken() or * getCurrentUsername(). Invoking this function will affect the UI, * including the login screen if visible. - * + * * The provided parameters can be virtually any object, as each property * will be sent as an HTTP parameter in the authentication request. * Standard parameters include "username" for the user's username, * "password" for the user's associated password, and "token" for the * auth token to check/update. - * + * * If a token is provided, it will be reused if possible. - * - * @param {Object|Promise} parameters + * + * @param parameters * Arbitrary parameters to authenticate with. If a Promise is provided, * that Promise must resolve with the parameters to be submitted when * those parameters are available, and any error will be handled as if * from the authentication endpoint of the REST API itself. * - * @returns {Promise} - * A promise which succeeds only if the login operation was successful. + * @returns + * An Observable which emits the authentication result only if the login operation was successful. */ - service.authenticate = function authenticate(parameters) { + authenticate(parameters: object | Promise): Observable { // Coerce received parameters object into a Promise, if it isn't // already a Promise - parameters = $q.resolve(parameters); + const parametersPromise = Promise.resolve(parameters); // Notify that a fresh authentication request is underway - $rootScope.$broadcast('guacLoginPending', parameters); + this.guacEventService.broadcast('guacLoginPending', { parameters: parametersPromise }); // Attempt authentication after auth parameters are available ... - return parameters.then(function requestParametersReady(requestParams) { - - // Strip any properties that are from AngularJS core, such as the - // '$$state' property added by $q. Properties added by AngularJS - // core will have a '$' prefix. The '$$state' property is - // particularly problematic, as it is self-referential and explodes - // the stack when fed to $.param(). - requestParams = _.omitBy(requestParams, (value, key) => key.startsWith('$')); - - return requestService({ - method: 'POST', - url: 'api/tokens', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded' - }, - data: $.param(requestParams) - }) + return from(parametersPromise).pipe( + switchMap((requestParams: any) => { + return this.http.post( + 'api/tokens', + this.toHttpParams(requestParams), + { + headers: new HttpHeaders({ 'Content-Type': 'application/x-www-form-urlencoded' }), + } + ); + } + ), // ... if authentication succeeds, handle received auth data ... - .then(function authenticationSuccessful(data) { - - var currentToken = service.getCurrentToken(); + map((data: AuthenticationResult) => { + const currentToken = this.getCurrentToken(); // If a new token was received, ensure the old token is invalidated, // if any, and notify listeners of the new token @@ -223,56 +219,77 @@ angular.module('auth').factory('authenticationService', ['$injector', // If an old token existed, request that the token be revoked if (currentToken) { - service.revokeToken(currentToken).catch(angular.noop); + this.revokeToken(currentToken).subscribe(); } // Notify of login and new token - setAuthenticationResult(new AuthenticationResult(data)); - $rootScope.$broadcast('guacLogin', data.authToken); + this.setAuthenticationResult(new AuthenticationResult(data)); + this.guacEventService.broadcast('guacLogin', { authToken: data.authToken }); } - - // Update cached authentication result, even if the token remains + // Update cached authentication result, even if the token remains // the same else - setAuthenticationResult(new AuthenticationResult(data)); + this.setAuthenticationResult(new AuthenticationResult(data)); // Authentication was successful return data; + }), + + // ... if authentication fails, propagate failure to returned promise + catchError(this.requestService.createErrorCallback((error: Error): Observable => { + + // Notify of generic login failure, for any event consumers that + // wish to handle all types of failures at once + this.guacEventService.broadcast('guacLoginFailed', { parameters: parametersPromise, error }); + + // Request credentials if provided credentials were invalid + if (error.type === Error.Type.INVALID_CREDENTIALS) { + this.guacEventService.broadcast('guacInvalidCredentials', { + parameters: parametersPromise, + error + }); + this.clearAuthenticationResult(); + } + + // Request more credentials if provided credentials were not enough + else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) { + this.guacEventService.broadcast('guacInsufficientCredentials', { + parameters: parametersPromise, + error + }); + this.clearAuthenticationResult(); + } + + // Abort rendering of page if an internal error occurs + else if (error.type === Error.Type.INTERNAL_ERROR) + this.guacEventService.broadcast('guacFatalPageError', { error }); + + // Authentication failed + return throwError(() => error); - }); - - }) - - // ... if authentication fails, propogate failure to returned promise - ['catch'](requestService.createErrorCallback(function authenticationFailed(error) { - - // Notify of generic login failure, for any event consumers that - // wish to handle all types of failures at once - $rootScope.$broadcast('guacLoginFailed', parameters, error); - - // Request credentials if provided credentials were invalid - if (error.type === Error.Type.INVALID_CREDENTIALS) { - $rootScope.$broadcast('guacInvalidCredentials', parameters, error); - clearAuthenticationResult(); - } - - // Request more credentials if provided credentials were not enough - else if (error.type === Error.Type.INSUFFICIENT_CREDENTIALS) { - $rootScope.$broadcast('guacInsufficientCredentials', parameters, error); - clearAuthenticationResult(); - } - - // Abort rendering of page if an internal error occurs - else if (error.type === Error.Type.INTERNAL_ERROR) - $rootScope.$broadcast('guacFatalPageError', error); - - // Authentication failed - throw error; + } + ) + ) + ); - })); + } - }; + /** + * Converts the given object into an equivalent HttpParams object by adding + * a parameter for each property of the given object. + * + * @param object + * The object to convert into HttpParams. + * + * @returns + * An HttpParams object containing a parameter for each property of the + * given object. + */ + private toHttpParams(object: any): HttpParams { + return Object.getOwnPropertyNames(object) + .reduce((p, key) => p.set(key, object[key]), new HttpParams()); + } /** * Makes a request to update the current auth token, if any, using the @@ -281,34 +298,34 @@ angular.module('auth').factory('authenticationService', ['$injector', * This function returns a promise that succeeds only if the authentication * operation was successful. The resulting authentication data can be * retrieved later via getCurrentToken() or getCurrentUsername(). - * + * * If there is no current auth token, this function behaves identically to * authenticate(), and makes a general authentication request. - * - * @param {Object} [parameters] + * + * @param parameters * Arbitrary parameters to authenticate with, if any. * - * @returns {Promise} + * @returns * A promise which succeeds only if the login operation was successful. */ - service.updateCurrentToken = function updateCurrentToken(parameters) { + updateCurrentToken(parameters: object): Observable { // HTTP parameters for the authentication request - var httpParameters = {}; + let httpParameters: any = {}; // Add token parameter if current token is known - var token = service.getCurrentToken(); + const token = this.getCurrentToken(); if (token) - httpParameters.token = service.getCurrentToken(); + httpParameters.token = this.getCurrentToken(); // Add any additional parameters if (parameters) - angular.extend(httpParameters, parameters); + httpParameters = { ...httpParameters, ...parameters }; // Make the request - return service.authenticate(httpParameters); + return this.authenticate(httpParameters); - }; + } /** * Determines whether the session associated with a particular token is @@ -316,78 +333,76 @@ angular.module('auth').factory('authenticationService', ['$injector', * session being marked as active. If no token is provided, the session of * the current user is checked. * - * @param {string} [token] + * @param token * The authentication token to pass with the "Guacamole-Token" header. * If omitted, and the user is logged in, the user's current * authentication token will be used. * - * @returns {Promise.} + * @returns * A promise that resolves with the boolean value "true" if the session * is valid, and resolves with the boolean value "false" otherwise, * including if an error prevents session validity from being * determined. The promise is never rejected. */ - service.getValidity = function getValidity(token) { + getValidity(token?: string): Observable { // NOTE: Because this is a HEAD request, we will not receive a JSON // response body. We will only have a simple yes/no regarding whether // the auth token can be expected to be usable. - return service.request({ - method: 'HEAD', - url: 'api/session' - }, token) - - .then(function sessionIsValid() { - return true; - }) - - ['catch'](function sessionIsNotValid() { - return false; - }); - - }; + return this.http.head( + 'api/session', + { + context: new HttpContext().set(AUTHENTICATION_TOKEN, token) + }) + .pipe( + map(() => true), + catchError(() => of(false)) + ); + } /** * Makes a request to revoke an authentication token using the token REST * API endpoint, returning a promise that succeeds only if the token was * successfully revoked. * - * @param {string} token + * @param token * The authentication token to revoke. * - * @returns {Promise} + * @returns * A promise which succeeds only if the token was successfully revoked. */ - service.revokeToken = function revokeToken(token) { - return service.request({ - method: 'DELETE', - url: 'api/session' - }, token); - }; + revokeToken(token: string | null): Observable { + return this.http.delete( + 'api/session', + { + context: new HttpContext().set(AUTHENTICATION_TOKEN, token) + } + ); + } /** * Makes a request to authenticate a user using the token REST API endpoint - * with a username and password, ignoring any currently-stored token, + * with a username and password, ignoring any currently-stored token, * returning a promise that succeeds only if the login operation was * successful. The resulting authentication data can be retrieved later * via getCurrentToken() or getCurrentUsername(). Invoking this function * will affect the UI, including the login screen if visible. - * - * @param {String} username + * + * @param username * The username to log in with. * - * @param {String} password + * @param password * The password to log in with. * - * @returns {Promise} + * @returns * A promise which succeeds only if the login operation was successful. */ - service.login = function login(username, password) { - return service.authenticate({ + login(username: string, password: string): Observable { + return this.authenticate({ username: username, password: password }); - }; + } /** * Makes a request to logout a user using the token REST API endpoint, @@ -395,24 +410,24 @@ angular.module('auth').factory('authenticationService', ['$injector', * successful. Invoking this function will affect the UI, causing the * visible components of the application to be replaced with a status * message noting that the user has been logged out. - * - * @returns {Promise} + * + * @returns * A promise which succeeds only if the logout operation was * successful. */ - service.logout = function logout() { + logout(): Observable { // Clear authentication data - var token = service.getCurrentToken(); - clearAuthenticationResult(); + const token = this.getCurrentToken(); + this.clearAuthenticationResult(); // Notify listeners that a token is being destroyed - $rootScope.$broadcast('guacLogout', token); + this.guacEventService.broadcast('guacLogout', { token }); // Delete old token - return service.revokeToken(token); + return this.revokeToken(token); - }; + } /** * Returns whether the current user has authenticated anonymously. An @@ -423,9 +438,9 @@ angular.module('auth').factory('authenticationService', ['$injector', * true if the current user has authenticated anonymously, false * otherwise. */ - service.isAnonymous = function isAnonymous() { - return service.getCurrentUsername() === ''; - }; + isAnonymous(): boolean { + return this.getCurrentUsername() === ''; + } /** * Returns the username of the current user. If the current user is not @@ -435,116 +450,76 @@ angular.module('auth').factory('authenticationService', ['$injector', * The username of the current user, or null if no authentication data * is present. */ - service.getCurrentUsername = function getCurrentUsername() { + getCurrentUsername(): string | null { // Return username, if available - var authData = getAuthenticationResult(); + const authData = this.getAuthenticationResult(); if (authData) return authData.username; // No auth data present return null; - }; + } /** * Returns the auth token associated with the current user. If the current * user is not logged in, this token may not be valid. * - * @returns {String} + * @returns * The auth token associated with the current user, or null if no * authentication data is present. */ - service.getCurrentToken = function getCurrentToken() { + getCurrentToken(): string | null { // Return cached auth token, if available - var authData = getAuthenticationResult(); + const authData = this.getAuthenticationResult(); if (authData) return authData.authToken; // Fall back to the value from local storage if not found in cache - return localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY); + return this.localStorageService.getItem(AUTH_TOKEN_STORAGE_KEY) as string | null; - }; + } /** * Returns the identifier of the data source that authenticated the current * user. If the current user is not logged in, this value may not be valid. * - * @returns {String} + * @returns * The identifier of the data source that authenticated the current * user, or null if no authentication data is present. */ - service.getDataSource = function getDataSource() { + getDataSource(): string | null { // Return data source, if available - var authData = getAuthenticationResult(); + const authData = this.getAuthenticationResult(); if (authData) return authData.dataSource; // No auth data present return null; - }; + } /** * Returns the identifiers of all data sources available to the current * user. If the current user is not logged in, this value may not be valid. * - * @returns {String[]} - * The identifiers of all data sources availble to the current user, + * @returns + * The identifiers of all data sources available to the current user, * or an empty array if no authentication data is present. */ - service.getAvailableDataSources = function getAvailableDataSources() { + getAvailableDataSources(): string[] { // Return data sources, if available - var authData = getAuthenticationResult(); + const authData = this.getAuthenticationResult(); if (authData) - return authData.availableDataSources; + return authData.availableDataSources || []; // No auth data present return []; - }; - - /** - * Makes an HTTP request leveraging the requestService(), automatically - * including the given authentication token using the "Guacamole-Token" - * header. If no token is provided, the user's current authentication token - * is used instead. If the user is not logged in, the "Guacamole-Token" - * header is simply omitted. The provided configuration object is not - * modified by this function. - * - * @param {Object} object - * A configuration object describing the HTTP request to be made by - * requestService(). As described by requestService(), this object must - * be a configuration object accepted by AngularJS' $http service. - * - * @param {string} [token] - * The authentication token to pass with the "Guacamole-Token" header. - * If omitted, and the user is logged in, the user's current - * authentication token will be used. - * - * @returns {Promise.} - * A promise that will resolve with the data from the HTTP response for - * the underlying requestService() call if successful, or reject with - * an @link{Error} describing the failure. - */ - service.request = function request(object, token) { - - // Attempt to use current token if none is provided - token = token || service.getCurrentToken(); - - // Add "Guacamole-Token" header if an authentication token is available - if (token) { - object = _.merge({ - headers : { 'Guacamole-Token' : token } - }, object); - } - - return requestService(object); - - }; + } - return service; -}]); +} diff --git a/guacamole/src/main/frontend/src/app/auth/types/AuthenticationResult.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/types/AuthenticationResult.ts similarity index 51% rename from guacamole/src/main/frontend/src/app/auth/types/AuthenticationResult.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/types/AuthenticationResult.ts index e2774619f3..c77d53ce5f 100644 --- a/guacamole/src/main/frontend/src/app/auth/types/AuthenticationResult.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/auth/types/AuthenticationResult.ts @@ -18,64 +18,51 @@ */ /** - * Service which defines the AuthenticationResult class. + * Returned by REST API calls when representing the successful + * result of an authentication attempt. */ -angular.module('auth').factory('AuthenticationResult', [function defineAuthenticationResult() { - +export class AuthenticationResult { + /** - * The object returned by REST API calls when representing the successful - * result of an authentication attempt. - * - * @constructor - * @param {AuthenticationResult|Object} [template={}] - * The object whose properties should be copied within the new - * AuthenticationResult. + * The unique token generated for the user that authenticated. */ - var AuthenticationResult = function AuthenticationResult(template) { + authToken: string; - // Use empty object by default - template = template || {}; + /** + * The name which uniquely identifies the user that authenticated. + */ + username: string; - /** - * The unique token generated for the user that authenticated. - * - * @type String - */ - this.authToken = template.authToken; + /** + * The unique identifier of the data source which authenticated the + * user. + */ + dataSource: string; - /** - * The name which uniquely identifies the user that authenticated. - * - * @type String - */ - this.username = template.username; + /** + * The identifiers of all data sources available to the user that + * authenticated. + */ + availableDataSources?: string[]; - /** - * The unique identifier of the data source which authenticated the - * user. - * - * @type String - */ + /** + * Creates a new AuthenticationResult object. This constructor initializes the properties of the + * new AuthenticationResult with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * AuthenticationResult. + */ + constructor(template: AuthenticationResult) { + this.authToken = template.authToken; + this.username = template.username; this.dataSource = template.dataSource; - - /** - * The identifiers of all data sources available to the user that - * authenticated. - * - * @type String[] - */ this.availableDataSources = template.availableDataSources; - - }; + } /** * The username reserved by the Guacamole extension API for users which have * authenticated anonymously. - * - * @type String */ - AuthenticationResult.ANONYMOUS_USERNAME = ''; - - return AuthenticationResult; - -}]); + static readonly ANONYMOUS_USERNAME: string = ''; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/client.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/client.module.ts new file mode 100644 index 0000000000..a1ae55f36c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/client.module.ts @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ClientLibModule, ElementModule, OskModule, TouchModule } from 'guacamole-frontend-lib'; +import { ClipboardModule } from '../clipboard/clipboard.module'; +import { FormModule } from '../form/form.module'; +import { GroupListModule } from '../group-list/group-list.module'; +import { NavigationModule } from '../navigation/navigation.module'; +import { NotificationModule } from '../notification/notification.module'; +import { TextInputModule } from '../text-input/text-input.module'; +import { OrderByPipe } from '../util/order-by.pipe'; +import { ClientPageComponent } from './components/client-page/client-page.component'; +import { ConnectionGroupComponent } from './components/connection-group/connection-group.component'; +import { ConnectionComponent } from './components/connection/connection.component'; +import { FileComponent } from './components/file/file.component'; +import { + GuacClientNotificationComponent +} from './components/guac-client-notification/guac-client-notification.component'; +import { GuacClientPanelComponent } from './components/guac-client-panel/guac-client-panel.component'; +import { GuacClientUserCountComponent } from './components/guac-client-user-count/guac-client-user-count.component'; +import { GuacClientZoomComponent } from './components/guac-client-zoom/guac-client-zoom.component'; +import { GuacClientComponent } from './components/guac-client/guac-client.component'; +import { GuacFileBrowserComponent } from './components/guac-file-browser/guac-file-browser.component'; +import { + GuacFileTransferManagerComponent +} from './components/guac-file-transfer-manager/guac-file-transfer-manager.component'; +import { GuacFileTransferComponent } from './components/guac-file-transfer/guac-file-transfer.component'; +import { GuacThumbnailComponent } from './components/guac-thumbnail/guac-thumbnail.component'; +import { GuacTiledClientsComponent } from './components/guac-tiled-clients/guac-tiled-clients.component'; +import { GuacTiledThumbnailsComponent } from './components/guac-tiled-thumbnails/guac-tiled-thumbnails.component'; +import { GuacZoomCtrlDirective } from './directives/guac-zoom-ctrl.directive'; + +/** + * The module for code used to connect to a connection or balancing group. + */ +@NgModule({ + declarations: [ + GuacZoomCtrlDirective, + GuacClientZoomComponent, + GuacThumbnailComponent, + GuacTiledThumbnailsComponent, + GuacFileTransferComponent, + GuacFileTransferManagerComponent, + ClientPageComponent, + GuacClientUserCountComponent, + GuacClientNotificationComponent, + GuacClientComponent, + GuacTiledClientsComponent, + ConnectionComponent, + ConnectionGroupComponent, + GuacClientPanelComponent, + GuacFileBrowserComponent, + FileComponent + ], + imports : [ + CommonModule, + TranslocoModule, + FormsModule, + ElementModule, + TextInputModule, + OskModule, + TouchModule, + ClientLibModule, + NavigationModule, + RouterLink, + ClipboardModule, + FormModule, + GroupListModule, + NotificationModule, + OrderByPipe + ], + exports : [ + GuacClientPanelComponent + ] +}) +export class ClientModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.html new file mode 100644 index 0000000000..a809900962 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.html @@ -0,0 +1,284 @@ + + + + + +
    +
    + + +
    + + + + + +
    + + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    +
    + + +
    + +
    + + +
    + {{ 'CLIENT.TEXT_CLIENT_STATUS_UNSTABLE' | transloco }} +
    + + + + + + + +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.ts new file mode 100644 index 0000000000..3a1958203e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/client-page/client-page.component.ts @@ -0,0 +1,1015 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Component, + computed, + DestroyRef, + DoCheck, + Input, + OnChanges, + OnDestroy, + OnInit, + Signal, + signal, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { Title } from '@angular/platform-browser'; +import { ActivatedRoute, Router } from '@angular/router'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import _ from 'lodash'; +import { catchError, pairwise, startWith } from 'rxjs'; +import { ScrollState } from '../../../../../../guacamole-frontend-lib/src/lib/element/types/ScrollState'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { ClipboardService } from '../../../clipboard/services/clipboard.service'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { FormService } from '../../../form/service/form.service'; +import { + GuacGroupListFilterComponent +} from '../../../group-list/components/guac-group-list-filter/guac-group-list-filter.component'; +import { ConnectionGroupDataSource } from '../../../group-list/types/ConnectionGroupDataSource'; +import { IconService } from '../../../index/services/icon.service'; +import { FilterService } from '../../../list/services/filter.service'; +import { UserPageService } from '../../../manage/services/user-page.service'; +import { NotificationAction } from '../../../notification/types/NotificationAction'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { TunnelService } from '../../../rest/service/tunnel.service'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { Protocol } from '../../../rest/types/Protocol'; +import { SharingProfile } from '../../../rest/types/SharingProfile'; +import { PreferenceService } from '../../../settings/services/preference.service'; +import { GuacClientManagerService } from '../../services/guac-client-manager.service'; +import { GuacFullscreenService } from '../../services/guac-fullscreen.service'; +import { ManagedClientService } from '../../services/managed-client.service'; +import { ManagedFilesystemService } from '../../services/managed-filesystem.service'; +import { ClientMenu } from '../../types/ClientMenu'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedClientGroup } from '../../types/ManagedClientGroup'; +import { ManagedClientState } from '../../types/ManagedClientState'; +import { ManagedFilesystem } from '../../types/ManagedFilesystem'; + +/** + * The Component for the page used to connect to a connection or balancing group. + */ +@Component({ + selector: 'guac-client-page', + templateUrl: './client-page.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ClientPageComponent implements OnInit, OnChanges, DoCheck, OnDestroy { + + /** + * TODO + */ + @Input({ required: true, alias: 'id' }) groupId!: string; + + /** + * The minimum number of pixels a drag gesture must move to result in the + * menu being shown or hidden. + */ + private readonly MENU_DRAG_DELTA: number = 64; + + /** + * The maximum X location of the start of a drag gesture for that gesture + * to potentially show the menu. + */ + private readonly MENU_DRAG_MARGIN: number = 64; + + /** + * When showing or hiding the menu via a drag gesture, the maximum number + * of pixels the touch can move vertically and still affect the menu. + */ + private readonly MENU_DRAG_VERTICAL_TOLERANCE: number = 10; + + /** + * In order to open the guacamole menu, we need to hit ctrl-alt-shift. There are + * several possible keysysms for each key. + */ + private readonly SHIFT_KEYS: Record = { + 0xFFE1: true, + 0xFFE2: true + }; + private readonly ALT_KEYS: Record = { + 0xFFE9: true, 0xFFEA: true, 0xFE03: true, + 0xFFE7: true, 0xFFE8: true + }; + private readonly CTRL_KEYS: Record = { + 0xFFE3: true, + 0xFFE4: true + }; + private readonly MENU_KEYS: Record = { + ...this.SHIFT_KEYS, + ...this.ALT_KEYS, + ...this.CTRL_KEYS + }; + + /** + * Keysym for detecting any END key presses, for the purpose of passing through + * the Ctrl-Alt-Del sequence to a remote system. + */ + private readonly END_KEYS: Record = { + 0xFF57: true, + 0xFFB1: true + }; + + /** + * Keysym for sending the DELETE key when the Ctrl-Alt-End hotkey + * combo is pressed. + */ + private readonly DEL_KEY: number = 0xFFFF; + + /** + * Menu-specific properties. + */ + protected menu: ClientMenu = { + shown : signal(false), + inputMethod : signal(this.preferenceService.preferences.inputMethod), + emulateAbsoluteMouse: signal(this.preferenceService.preferences.emulateAbsoluteMouse), + scrollState : signal(new ScrollState()), + connectionParameters: {} + }; + + /** + * Form group for editing connection parameters which may be modified while the connection is open. + */ + protected connectionParameters: FormGroup = new FormGroup({}); + + /** + * The currently-focused client within the current ManagedClientGroup. If + * there is no current group, no client is focused, or multiple clients are + * focused, this will be null. + */ + protected focusedClient: ManagedClient | null = null; + + /** + * The set of clients that should be attached to the client UI. This will + * be immediately initialized by a call to updateAttachedClients() below. + */ + protected clientGroup: ManagedClientGroup | null = null; + + /** + * The root connection groups of the connection hierarchy that should be + * presented to the user for selecting a different connection, as a map of + * data source identifier to the root connection group of that data + * source. This will be null if the connection group hierarchy has not yet + * been loaded or if the hierarchy is inapplicable due to only one + * connection or balancing group being available. + */ + protected rootConnectionGroups: Record | null = null; + + /** + * Filtered view of the root connection groups which satisfy the current + * search string. + */ + protected rootConnectionGroupsDataSource: ConnectionGroupDataSource; + + /** + * Array of all connection properties that are filterable. + */ + private filteredConnectionProperties: string[] = [ + 'name' + ]; + + /** + * Array of all connection group properties that are filterable. + */ + private filteredConnectionGroupProperties: string[] = [ + 'name' + ]; + + /** + * Map of all available sharing profiles for the current connection by + * their identifiers. If this information is not yet available, or no such + * sharing profiles exist, this will be an empty object. + */ + protected sharingProfiles: Record = {}; + + /** + * Map of all substituted key presses. If one key is pressed in place of another + * the value of the substituted key is stored in an object with the keysym of + * the original key. + */ + private substituteKeysPressed: Record = {}; + + /** + * TODO + * @private + */ + private lastThumbnailCanvas?: HTMLCanvasElement; + + /** + * TODO + * @private + */ + private lastTunnelUuid?: string | null; + + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private clipboardService: ClipboardService, + private dataSourceService: DataSourceService, + private guacClientManager: GuacClientManagerService, + private guacFullscreen: GuacFullscreenService, + private iconService: IconService, + private preferenceService: PreferenceService, + private requestService: RequestService, + private tunnelService: TunnelService, + private userPageService: UserPageService, + private managedClientService: ManagedClientService, + private router: Router, + private route: ActivatedRoute, + private destroyRef: DestroyRef, + private guacEventService: GuacEventService, + private managedFilesystemService: ManagedFilesystemService, + private formService: FormService, + private filterService: FilterService, + private title: Title) { + + // Create a new data source for the root connection groups + this.rootConnectionGroupsDataSource = new ConnectionGroupDataSource(this.filterService, + {}, // Start without any data + null, // Start without a search string + this.filteredConnectionProperties, + this.filteredConnectionGroupProperties); + + // Update client state/behavior as visibility of the Guacamole menu changes + toObservable(this.menu.shown) + .pipe( + takeUntilDestroyed(this.destroyRef), + startWith(this.menu.shown()), + pairwise(), + ).subscribe(([menuShownPreviousState, menuShown]) => { + + // Re-update available connection parameters, if there is a focused + // client (parameter information may not have been available at the + // time focus changed) + if (menuShown) + this.menu.connectionParameters = this.focusedClient ? + this.managedClientService.getArgumentModel(this.focusedClient) : {}; + + // Send any argument value data once menu is hidden + else if (menuShownPreviousState) + this.applyParameterChanges(this.focusedClient); + + /* Broadcast changes to the menu display state */ + this.guacEventService.broadcast('guacMenuShown', { menuShown }); + + }); + + // Automatically refresh display when filesystem menu is shown + toObservable(this.menu.shown).pipe(takeUntilDestroyed()).subscribe(() => { + // Refresh filesystem, if defined + const filesystem = this.filesystemMenuContents; + if (filesystem) + managedFilesystemService.refresh(filesystem, filesystem.currentDirectory()); + }); + + } + + ngOnInit(): void { + + // Init sets of clients based on current URL. + this.reparseRoute(); + + // Retrieve root groups and all descendants + this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID), + this.authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(rootConnectionGroups => { + + // Store retrieved groups only if there are multiple connections or + // balancing groups available + const clientPages = this.userPageService.getClientPages(rootConnectionGroups); + if (clientPages.length > 1) { + this.rootConnectionGroups = rootConnectionGroups; + this.rootConnectionGroupsDataSource.updateSource(this.rootConnectionGroups); + } + + }, this.requestService.WARN); + + + // Automatically track and cache the currently-focused client + this.guacEventService.on('guacClientFocused') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ newFocusedClient }) => { + + const oldFocusedClient = this.focusedClient; + this.focusedClient = newFocusedClient; + + // Apply any parameter changes when focus is changing + if (oldFocusedClient) + this.applyParameterChanges(oldFocusedClient); + + // Update available connection parameters, if there is a focused + // client + this.menu.connectionParameters = newFocusedClient ? + this.managedClientService.getArgumentModel(newFocusedClient) : {}; + + }); + + // Opening the Guacamole menu after Ctrl+Alt+Shift, preventing those + // keypresses from reaching any Guacamole client + this.guacEventService.on('guacBeforeKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keyboard }) => { + + // Toggle menu if menu shortcut (Ctrl+Alt+Shift) is pressed + if (this.isMenuShortcutPressed(keyboard)) { + + // Don't send this key event through to the client, and release + // all other keys involved in performing this shortcut + event.preventDefault(); + keyboard.reset(); + + // Toggle the menu + this.menu.shown.update(shown => !shown); + + } + + // Prevent all keydown events while menu is open + else if (this.menu.shown()) + event.preventDefault(); + + }); + + // Automatically update connection parameters that have been modified + // for the current focused client + this.guacEventService.on('guacClientArgumentsUpdated') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ focusedClient }) => { + + // Ignore any updated arguments not for the current focused client + if (this.focusedClient && this.focusedClient === focusedClient) + this.menu.connectionParameters = this.managedClientService.getArgumentModel(focusedClient); + + }); + + // Prevent all keyup events while menu is open + this.guacEventService.on('guacBeforeKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event }) => { + if (this.menu.shown()) + event.preventDefault(); + }); + + // Send Ctrl-Alt-Delete when Ctrl-Alt-End is pressed. + this.guacEventService.on('guacKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym, keyboard }) => { + + // If one of the End keys is pressed, and we have a one keysym from each + // of Ctrl and Alt groups, send Ctrl-Alt-Delete. + if (this.END_KEYS[keysym as any] + && _.findKey(this.ALT_KEYS, (val, keysym) => keyboard.pressed[keysym as any]) + && _.findKey(this.CTRL_KEYS, (val, keysym) => keyboard.pressed[keysym as any]) + ) { + + // Don't send this event through to the client. + event.preventDefault(); + + // Record the substituted key press so that it can be + // properly dealt with later. + this.substituteKeysPressed[keysym] = this.DEL_KEY; + + // Send through the delete key. + this.guacEventService.broadcast('guacSyntheticKeydown', { keysym: this.DEL_KEY }); + } + + }); + + // Update pressed keys as they are released + this.guacEventService.on('guacKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym }) => { + + // Deal with substitute key presses + if (this.substituteKeysPressed[keysym]) { + event.preventDefault(); + this.guacEventService.broadcast('guacSyntheticKeyup', { keysym: this.substituteKeysPressed[keysym] }); + delete this.substituteKeysPressed[keysym]; + } + + }); + + // Toggle the menu when the guacClientToggleMenu event is received + this.guacEventService.on('guacToggleMenu') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.menu.shown.update(shown => !shown)); + + // Show the menu when the guacClientShowMenu event is received + this.guacEventService.on('guacShowMenu') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.menu.shown.set(true)); + + // Hide the menu when the guacClientHideMenu event is received + this.guacEventService.on('guacHideMenu') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => this.menu.shown.set(false)); + + } // ngOnInit() end + + /** + * Add the filter string observable to the data source when the filter + * component is available. + * + * @param filterComponent + * The filter component which will provide the filter string. + */ + @ViewChild(GuacGroupListFilterComponent, { static: false }) set filterComponent(filterComponent: GuacGroupListFilterComponent | undefined) { + + if (filterComponent) { + this.rootConnectionGroupsDataSource.setSearchString(filterComponent.searchStringChange); + } + + } + + /** + * TODO: Document + */ + ngOnChanges(changes: SimpleChanges): void { + + // Re-initialize the client group if the group id has changed without reloading the route + if (changes['groupId']) + this.reparseRoute(); + + + if (changes['focusedClient']) { + + /** + * TODO: Document + */ + if (this.focusedClient) { + this.connectionParameters = this.formService.getFormGroup(this.focusedClient.forms); + this.connectionParameters.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => this.menu.connectionParameters = value); + } else { + this.connectionParameters = new FormGroup({}); + } + + } + + } + + + /** + * Convenience method for closing the menu. + */ + closeMenu() { + this.menu.shown.set(false); + } + + /** + * Applies any changes to connection parameters made by the user within the + * Guacamole menu to the given ManagedClient. If no client is supplied, + * this function has no effect. + * + * @param client + * The client to apply parameter changes to. + */ + applyParameterChanges(client: ManagedClient | null): void { + for (const name in this.menu.connectionParameters) { + const value = this.menu.connectionParameters[name]; + if (client) + this.managedClientService.setArgument(client, name, value); + } + } + + /** + * @borrows ManagedClientGroup.getName + */ + getName(group: ManagedClientGroup): string { + return ManagedClientGroup.getName(group); + } + + /** + * Arbitrary context that should be exposed to the guacGroupList directive + * displaying the dropdown list of available connections within the + * Guacamole menu. + */ + connectionListContext: ConnectionListContext = { + attachedClients : {}, + updateAttachedClients: id => { + this.addRemoveClient(id, !this.connectionListContext.attachedClients[id]); + } + }; + + /** + * Adds or removes the client with the given ID from the set of clients + * within the current view, updating the current URL accordingly. + * + * @param id + * The ID of the client to add or remove from the current view. + * + * @param remove=false + * Whether the specified client should be added (false) or removed + * (true). + */ + addRemoveClient(id: string, remove = false): void { + + // Deconstruct current path into corresponding client IDs + const ids = ManagedClientGroup.getClientIdentifiers(this.groupId); + + // Add/remove ID as requested + if (remove) + _.pull(ids, id); + else + ids.push(id); + + // Reconstruct path, updating attached clients via change in route + this.router.navigate(['/client', ManagedClientGroup.getIdentifier(ids)]); + + } + + /** + * Reloads the contents of this.clientGroup to reflect the client IDs + * currently listed in the URL. + */ + private reparseRoute(): void { + + const previousClients = this.clientGroup ? this.clientGroup.clients.slice() : []; + + // Replace existing group with new group + this.setAttachedGroup(this.guacClientManager.getManagedClientGroup(this.groupId)); + + // Store current set of attached clients for later use within the + // Guacamole menu + this.connectionListContext.attachedClients = {}; + this.clientGroup?.clients.forEach((client) => { + this.connectionListContext.attachedClients[client.id] = true; + }); + + // Ensure menu is closed if updated view is not a modification of the + // current view (has no clients in common). The menu should remain open + // only while the current view is being modified, not when navigating + // to an entirely different view. + if (_.isEmpty(_.intersection(previousClients, this.clientGroup!.clients))) + this.menu.shown.set(false); + + // Update newly-attached clients with current contents of clipboard + this.clipboardService.resyncClipboard(); + + } + + /** + * Replaces the ManagedClientGroup currently attached to the client + * interface via $scope.clientGroup with the given ManagedClientGroup, + * safely cleaning up after the previous group. If no ManagedClientGroup is + * provided, the existing group is simply removed. + * + * @param managedClientGroup + * The ManagedClientGroup to attach to the interface, if any. + */ + private setAttachedGroup(managedClientGroup?: ManagedClientGroup | null): void { + + // Do nothing if group is not actually changing + if (this.clientGroup === managedClientGroup) + return; + + if (this.clientGroup) { + + // Remove all disconnected clients from management (the user has + // seen their status) + _.filter(this.clientGroup.clients, client => { + + const connectionState = client.clientState.connectionState; + return connectionState === ManagedClientState.ConnectionState.DISCONNECTED + || connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR + || connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR; + + }).forEach(client => { + this.guacClientManager.removeManagedClient(client.id); + }); + + // Flag group as detached + this.clientGroup.attached = false; + + } + + if (managedClientGroup) { + this.clientGroup = managedClientGroup; + this.clientGroup.attached = true; + this.clientGroup.lastUsed = new Date().getTime(); + } + + } + + /** + * Returns whether the shortcut for showing/hiding the Guacamole menu + * (Ctrl+Alt+Shift) has been pressed. + * + * @param keyboard + * The Guacamole.Keyboard object tracking the local keyboard state. + * + * @returns + * true if Ctrl+Alt+Shift has been pressed, false otherwise. + */ + private isMenuShortcutPressed(keyboard: Guacamole.Keyboard): boolean { + + // Ctrl+Alt+Shift has NOT been pressed if any key is currently held + // down that isn't Ctrl, Alt, or Shift + if (_.findKey(keyboard.pressed, (val, keysym) => !this.MENU_KEYS[Number(keysym)])) + return false; + + // Verify that one of each required key is held, regardless of + // left/right location on the keyboard + return !!( + _.findKey(this.SHIFT_KEYS, (val, keysym: any) => keyboard.pressed[keysym]) + && _.findKey(this.ALT_KEYS, (val, keysym: any) => keyboard.pressed[keysym]) + && _.findKey(this.CTRL_KEYS, (val, keysym: any) => keyboard.pressed[keysym]) + ); + + } + + // Show menu if the user swipes from the left, hide menu when the user + // swipes from the right, scroll menu while visible + menuDrag(inProgress: boolean, startX: number, startY: number, currentX: number, currentY: number, deltaX: number, deltaY: number): boolean { + + if (this.menu.shown()) { + + // Hide menu if swipe-from-right gesture is detected + if (Math.abs(currentY - startY) < this.MENU_DRAG_VERTICAL_TOLERANCE + && startX - currentX >= this.MENU_DRAG_DELTA) + this.menu.shown.set(false); + + // Scroll menu by default + else { + this.menu.scrollState.update(({top, left}) => new ScrollState({left: left - deltaX, top: top-deltaY})); + } + + } + + // Show menu if swipe-from-left gesture is detected + else if (startX <= this.MENU_DRAG_MARGIN) { + if (Math.abs(currentY - startY) < this.MENU_DRAG_VERTICAL_TOLERANCE + && currentX - startX >= this.MENU_DRAG_DELTA) + this.menu.shown.set(true); + } + + return false; + + } + + /** + * Controls whether the on-screen keyboard is shown. + */ + showOSK: Signal = computed(() => this.menu.inputMethod() === 'osk'); + + /** + * Controls whether the text input field is shown. + */ + showTextInput: Signal = computed(() => this.menu.inputMethod() === 'text'); + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + const newThumbnailCanvas = this.focusedClient?.thumbnail?.canvas; + + // Update page icon when thumbnail changes + if (this.lastThumbnailCanvas !== newThumbnailCanvas) { + + this.iconService.setIcons(newThumbnailCanvas); + this.lastThumbnailCanvas = newThumbnailCanvas; + + } + + const newTunnelUuid = this.focusedClient?.tunnel.uuid; + + // Pull sharing profiles once the tunnel UUID is known + if (this.lastTunnelUuid !== newTunnelUuid) { + + // Only pull sharing profiles if tunnel UUID is actually available + if (!newTunnelUuid) { + + this.sharingProfiles = {}; + + } else { + + // Pull sharing profiles for the current connection + this.tunnelService.getSharingProfiles(newTunnelUuid) + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(this.requestService.WARN) + ) + .subscribe(sharingProfiles => { + this.sharingProfiles = sharingProfiles; + }); + + } + + this.lastTunnelUuid = newTunnelUuid; + + } + + // Update page title when client title changes + if (this.clientGroup) { + + const newTitle = ManagedClientGroup.getTitle(this.clientGroup); + + if (this.title.getTitle() !== newTitle) { + + this.title.setTitle(newTitle!); + + } + + } + + } + + /** + * Produces a sharing link for the current connection using the given + * sharing profile. The resulting sharing link, and any required login + * information, will be displayed to the user within the Guacamole menu. + * + * @param sharingProfile + * The sharing profile to use to generate the sharing link. + */ + share(sharingProfile: SharingProfile): void { + if (this.focusedClient) + this.managedClientService.createShareLink(this.focusedClient, sharingProfile); + } + + /** + * Returns whether the current connection has any associated share links. + * + * @returns + * true if the current connection has at least one associated share + * link, false otherwise. + */ + isShared(): boolean { + return !!this.focusedClient && this.managedClientService.isShared(this.focusedClient); + } + + /** + * Returns the total number of share links associated with the current + * connection. + * + * @returns + * The total number of share links associated with the current + * connection. + */ + getShareLinkCount(): number { + + if (!this.focusedClient) + return 0; + + // Count total number of links within the ManagedClient's share link map + let linkCount = 0; + for (const dummy in this.focusedClient.shareLinks) + linkCount++; + + return linkCount; + + } + + /** + * Returns whether the current connection has been flagged as unstable due + * to an apparent network disruption. + * + * @returns + * true if the current connection has been flagged as unstable, false + * otherwise. + */ + isConnectionUnstable(): boolean { + return _.findIndex(this.clientGroup?.clients, client => client.clientState.tunnelUnstable) !== -1; + } + + /** + * Immediately disconnects all currently-focused clients, if any. + */ + disconnect(): void { + + // Disconnect if client is available + if (this.clientGroup) { + this.clientGroup.clients.forEach(client => { + if (client.clientProperties.focused) + client.client.disconnect(); + }); + } + + // Hide menu + this.menu.shown.set(false); + + } + + /** + * Disconnects the given ManagedClient, removing it from the current + * view. + * + * @param client + * The client to disconnect. + */ + closeClientTile(client: ManagedClient): void { + + this.addRemoveClient(client.id, true); + this.guacClientManager.removeManagedClient(client.id); + + // Ensure at least one client has focus (the only client with + // focus may just have been removed) + ManagedClientGroup.verifyFocus(this.clientGroup); + + } + + /** + * Action which immediately disconnects the currently-connected client, if + * any. + */ + private DISCONNECT_MENU_ACTION: NotificationAction = { + name : 'CLIENT.ACTION_DISCONNECT', + className: 'danger disconnect', + callback : () => this.disconnect() + }; + + /** + * Action that toggles fullscreen mode within the + * currently-connected client and then closes the menu. + */ + private FULLSCREEN_MENU_ACTION: NotificationAction = { + name : 'CLIENT.ACTION_FULLSCREEN', + className: 'fullscreen action', + callback : () => { + + this.guacFullscreen.toggleFullscreenMode(); + this.menu.shown.set(false); + } + }; + + // Set client-specific menu actions + clientMenuActions: NotificationAction[] = [this.DISCONNECT_MENU_ACTION, this.FULLSCREEN_MENU_ACTION]; + + /** + * @borrows Protocol.getNamespace + */ + getProtocolNamespace(protocolName: string): string | undefined { + return Protocol.getNamespace(protocolName); + } + + /** + * The currently-visible filesystem within the filesystem menu, if the + * filesystem menu is open. If no filesystem is currently visible, this + * will be null. + */ + filesystemMenuContents: ManagedFilesystem | null = null; + + /** + * Hides the filesystem menu. + */ + hideFilesystemMenu(): void { + this.filesystemMenuContents = null; + } + + /** + * Shows the filesystem menu, displaying the contents of the given + * filesystem within it. + * + * @param filesystem + * The filesystem to show within the filesystem menu. + */ + showFilesystemMenu(filesystem: ManagedFilesystem): void { + this.filesystemMenuContents = filesystem; + } + + /** + * Returns whether the filesystem menu should be visible. + * + * @returns + * true if the filesystem menu is shown, false otherwise. + */ + isFilesystemMenuShown(): boolean { + return !!this.filesystemMenuContents && this.menu.shown(); + } + + /** + * Returns the full path to the given file as an ordered array of parent + * directories. + * + * @param file + * The file whose full path should be retrieved. + * + * @returns + * An array of directories which make up the hierarchy containing the + * given file, in order of increasing depth. + */ + getPath(file: ManagedFilesystem.File): ManagedFilesystem.File[] { + + const path = []; + + // Add all files to path in ascending order of depth + while (file && file.parent) { + path.unshift(file); + file = file.parent; + } + + return path; + + } + + /** + * Changes the current directory of the given filesystem to the given + * directory. + * + * @param filesystem + * The filesystem whose current directory should be changed. + * + * @param file + * The directory to change to. + */ + changeDirectory(filesystem: ManagedFilesystem, file: ManagedFilesystem.File): void { + this.managedFilesystemService.changeDirectory(filesystem, file); + } + + /** + * Begins a file upload through the attached Guacamole client for + * each file in the given FileList. + * + * @param files + * The files to upload. + */ + uploadFiles(files: FileList): void { + + // Upload each file + for (let i = 0; i < files.length; i++) + this.managedClientService.uploadFile(this.filesystemMenuContents!.client, files[i], this.filesystemMenuContents!); + + } + + /** + * Determines whether the attached client group has any associated file + * transfers, regardless of those file transfers' state. + * + * @returns + * true if there are any file transfers associated with the + * attached client group, false otherise. + */ + hasTransfers(): boolean { + + // There are no file transfers if there is no client group + if (!this.clientGroup) + return false; + + return _.findIndex(this.clientGroup.clients, this.managedClientService.hasTransfers) !== -1; + + } + + /** + * Returns whether the current user can share the current connection with + * other users. A connection can be shared if and only if there is at least + * one associated sharing profile. + * + * @returns + * true if the current user can share the current connection with other + * users, false otherwise. + */ + canShareConnection(): boolean { + + // If there is at least one sharing profile, the connection can be shared + for (const dummy in this.sharingProfiles) + return true; + + // Otherwise, sharing is not possible + return false; + + } + + /** + * Clean up when component is destroyed. + */ + ngOnDestroy(): void { + this.setAttachedGroup(null); + + // always unset fullscreen mode to not confuse user + this.guacFullscreen.setFullscreenMode(false); + } + +} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.html new file mode 100644 index 0000000000..dfe4fdaf1b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.html @@ -0,0 +1,30 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.ts new file mode 100644 index 0000000000..bee202bf71 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection-group/connection-group.component.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + +/** + * TODO + */ +@Component({ + selector: 'guac-connection-group', + templateUrl: './connection-group.component.html', + styles: [], + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionGroupComponent { + + /** + * TODO + */ + @Input({ required: true }) context!: ConnectionListContext; + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.html new file mode 100644 index 0000000000..98adbfdc4f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.html @@ -0,0 +1,28 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.ts new file mode 100644 index 0000000000..bc74d1027c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/connection/connection.component.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + +/** + * TODO + */ +@Component({ + selector: 'guac-connection', + templateUrl: './connection.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionComponent { + + /** + * TODO + */ + @Input({ required: true }) context!: ConnectionListContext; + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.html new file mode 100644 index 0000000000..a9039bb3d4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.html @@ -0,0 +1,28 @@ + + +
    + + +
    +
    + {{ file?.name }} +
    + +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.ts new file mode 100644 index 0000000000..491510621c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/file/file.component.ts @@ -0,0 +1,205 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnInit, + Output, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ManagedFilesystem } from '../../types/ManagedFilesystem'; + +/** + * This component displays a single file in a file browser. It shows the + * file's name, icon, and type, and allows the user to interact with the + * file by clicking on it. + * + * If the file is a directory, the user can click on it to change the + * current directory of the file browser. If the file is a normal file, + * the user can click on it to initiate a download. + */ +@Component({ + selector: 'guac-file', + templateUrl: './file.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class FileComponent implements OnInit, OnChanges { + + /** + * The file to be displayed by the component. + */ + @Input({ required: true }) file: ManagedFilesystem.File | null = null; + + /** + * Indicates whether the file is currently focused. If true, the next + * click on the file will trigger a changeDirectory or downloadFile + * event based on the type of the file. + */ + @Input({ required: true }) isFocused = false; + + /** + * Event emitted when the file is clicked and should be focused. + */ + @Output() focusFile: EventEmitter = new EventEmitter(); + + /** + * Event emitted when the current directory should be changed. + * The event payload is the directory to change to. + */ + @Output() changeDirectory: EventEmitter = new EventEmitter(); + + /** + * Event emitted when a file should be downloaded. + * The event payload is the file to download. + */ + @Output() downloadFile: EventEmitter = new EventEmitter(); + + /** + * Reference to the HTML element backing the component. + */ + @ViewChild('listItem', { static: true }) elementRef?: ElementRef; + + /** + * The HTML element backing the component. + */ + element?: HTMLDivElement; + + /** + * The file action to execute when a focused file is clicked. + */ + fileAction: (() => void) | null = null; + + /** + * The CSS class to apply to the file depending on its type. + */ + clazz: string | null = null; + + /** + * Initializes the component when it's created. + */ + ngOnInit(): void { + this.element = this.elementRef?.nativeElement; + this.setupComponent(); + } + + /** + * Calls setupComponent() when the file changes. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['file']) { + + const file = changes['file'].currentValue as ManagedFilesystem.File | null; + + if (file) + this.setupComponent(); + } + + } + + /** + * Sets up the component by adding event listeners and setting up the file action. + */ + setupComponent(): void { + + if (!this.file || !this.element) + return; + + // Double-clicking on unknown file types will do nothing + // eslint-disable-next-line @typescript-eslint/no-empty-function + let fileAction = () => { + }; + + let clazz = ''; + + // Change current directory when directories are clicked + if (this.isDirectory(this.file)) { + clazz = 'directory'; + fileAction = () => { + if (this.file) + this.changeDirectory.emit(this.file); + }; + } + + // Initiate downloads when normal files are clicked + else if (this.isNormalFile(this.file)) { + clazz = 'normal-file'; + fileAction = () => { + if (this.file) + this.downloadFile.emit(this.file); + }; + } + + this.clazz = clazz; + this.fileAction = fileAction; + + // Mark file as focused upon click + this.element.addEventListener('click', () => { + + // Mark file as focused + this.focusFile.emit(); + + // Execute file action + if (this.fileAction && this.isFocused) + this.fileAction(); + + }); + + // Prevent text selection during navigation + this.element.addEventListener('selectstart', e => { + e.preventDefault(); + e.stopPropagation(); + }); + } + + /** + * Returns whether the given file is a normal file. + * + * @param file + * The file to test. + * + * @returns + * true if the given file is a normal file, false otherwise. + */ + isNormalFile(file: ManagedFilesystem.File): boolean { + return file.type === ManagedFilesystem.File.Type.NORMAL; + } + + /** + * Returns whether the given file is a directory. + * + * @param file + * The file to test. + * + * @returns + * true if the given file is a directory, false otherwise. + */ + isDirectory(file: ManagedFilesystem.File): boolean { + return file.type === ManagedFilesystem.File.Type.DIRECTORY; + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.html new file mode 100644 index 0000000000..81b6f9af3f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.html @@ -0,0 +1,24 @@ + + +
    + + + +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.ts new file mode 100644 index 0000000000..3e9b0a77f3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-notification/guac-client-notification.component.ts @@ -0,0 +1,474 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, DoCheck, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { GuacEvent, GuacEventService } from 'guacamole-frontend-lib'; +import { finalize } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { UserPageService } from '../../../manage/services/user-page.service'; +import { Notification } from '../../../notification/types/Notification'; +import { NotificationAction } from '../../../notification/types/NotificationAction'; +import { NotificationCountdown } from '../../../notification/types/NotificationCountdown'; +import { RequestService } from '../../../rest/service/request.service'; +import { Form } from '../../../rest/types/Form'; +import { Protocol } from '../../../rest/types/Protocol'; +import { GuacClientManagerService } from '../../services/guac-client-manager.service'; +import { GuacTranslateService } from '../../services/guac-translate.service'; +import { ManagedClientService } from '../../services/managed-client.service'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedClientState } from '../../types/ManagedClientState'; + + +/** + * All error codes for which automatic reconnection is appropriate when a + * client error occurs. + */ +const CLIENT_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0207: true, + 0x0208: true, + 0x0301: true, + 0x0308: true +}; + +/** + * All error codes for which automatic reconnection is appropriate when a + * tunnel error occurs. + */ +const TUNNEL_AUTO_RECONNECT = { + 0x0200: true, + 0x0202: true, + 0x0203: true, + 0x0207: true, + 0x0208: true, + 0x0308: true +}; + +/** + * Connection status of specific Guacamole client. + */ +type ClientState = [string, Record | null, string | null, Form[]]; + + +/** + * A Component for displaying a non-global notification describing the status + * of a specific Guacamole client, including prompts for any information + * necessary to continue the connection. + */ +@Component({ + selector: 'guac-client-notification', + templateUrl: './guac-client-notification.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacClientNotificationComponent implements OnInit, DoCheck { + + /** + * The client whose status should be displayed. + */ + @Input({ required: true }) client!: ManagedClient; + + /** + * A Notification object describing the client status to display as a + * dialog or prompt, as would be accepted by guacNotification.showStatus(), + * or false if no status should be shown. + */ + status: Notification | object | boolean = false; + + /** + * Action which logs out from Guacamole entirely. + */ + private readonly LOGOUT_ACTION: NotificationAction = { + name : 'CLIENT.ACTION_LOGOUT', + className: 'logout button', + callback : () => this.logout() + }; + + /** + * Action which returns the user to the home screen. If the home page has + * not yet been determined, this will be null. + */ + private NAVIGATE_HOME_ACTION: NotificationAction | null = null; + + /** + * Action which replaces the current client with a newly-connected client. + */ + private readonly RECONNECT_ACTION: NotificationAction = { + name : 'CLIENT.ACTION_RECONNECT', + className: 'reconnect button', + callback : () => { + this.client = this.guacClientManager.replaceManagedClient(this.client.id); + this.status = false; + } + }; + + /** + * The reconnect countdown to display if an error or status warrants an + * automatic, timed reconnect. + */ + private readonly RECONNECT_COUNTDOWN: NotificationCountdown = { + text : 'CLIENT.TEXT_RECONNECT_COUNTDOWN', + callback : this.RECONNECT_ACTION.callback, + remaining: 15 + }; + + /** + * The client state to react to changes. + */ + private lastClientState: ClientState = ['', null, null, []]; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private guacClientManager: GuacClientManagerService, + private guacTranslate: GuacTranslateService, + private requestService: RequestService, + private userPageService: UserPageService, + private managedClientService: ManagedClientService, + private guacEventService: GuacEventService, + private router: Router, + private route: ActivatedRoute, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + + // Assign home page action once user's home page has been determined + this.userPageService.getHomePage() + .subscribe({ + next : homePage => { + + // Define home action only if different from current location + if (this.route.snapshot.root.url.join('/') || '/' === homePage.url) { + this.NAVIGATE_HOME_ACTION = { + name : 'CLIENT.ACTION_NAVIGATE_HOME', + className: 'home button', + callback : () => { + this.router.navigate([homePage.url]); + } + }; + } + + }, error: this.requestService.WARN + }); + + // Block internal handling of key events (by the client) if a + // notification is visible + this.guacEventService.on('guacBeforeKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event }) => this.preventDefaultDuringNotification(event)); + this.guacEventService.on('guacBeforeKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event }) => this.preventDefaultDuringNotification(event)); + } + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + const currentClientState: ClientState = [ + this.client.clientState.connectionState, + this.client.requiredParameters, + this.client.protocol, + this.client.forms + ]; + + // Compare the current state with the last known state + if (!this.compareClientState(this.lastClientState, currentClientState)) { + + this.lastClientState = [...currentClientState]; + this.clientStateChanged(currentClientState); + + } + + } + + /** + * Compares two client states to determine if they are identical. + * + * @param prevValues - The previous client state. + * @param currValues - The current client state. + * @returns true if the states are identical, false otherwise. + */ + private compareClientState(prevValues: ClientState, currValues: ClientState): boolean { + if (prevValues.length !== currValues.length) return false; + return prevValues.every((value, index) => value === currValues[index]); + } + + /** + * Show status dialog when connection status changes + */ + private clientStateChanged(newValues: ClientState): void { + const connectionState = newValues[0]; + const requiredParameters = newValues[1]; + + // Prompt for parameters only if parameters can actually be submitted + if (requiredParameters && this.canSubmitParameters(connectionState)) + this.notifyParametersRequired(requiredParameters); + + // Otherwise, just show general connection state + else + this.notifyConnectionState(connectionState); + } + + /** + * Displays a notification at the end of a Guacamole connection, whether + * that connection is ending normally or due to an error. As the end of + * a Guacamole connection may be due to changes in authentication status, + * this will also implicitly perform a re-authentication attempt to check + * for such changes, possibly resulting in auth-related events like + * guacInvalidCredentials. + * + * @param status + * The status notification to show, as would be accepted by + * guacNotification.showStatus(). + */ + private notifyConnectionClosed(status: Notification | object | boolean): void { + + // Re-authenticate to verify auth status at end of connection + this.authenticationService.updateCurrentToken(this.route.snapshot.queryParams) + .pipe( + // Show the requested status once the authentication check has finished + finalize(() => { + this.status = status; + }) + ) + .subscribe({ + error: this.requestService.IGNORE + }); + } + + /** + * Notifies the user that the connection state has changed. + * + * @param connectionState + * The current connection state, as defined by + * ManagedClientState.ConnectionState. + */ + private notifyConnectionState(connectionState: string): void { + + // Hide any existing status + this.status = false; + + // Do not display status if status not known + if (!connectionState) + return; + + // Build array of available actions + let actions: NotificationAction[]; + if (this.NAVIGATE_HOME_ACTION) + actions = [this.NAVIGATE_HOME_ACTION, this.RECONNECT_ACTION, this.LOGOUT_ACTION]; + else + actions = [this.RECONNECT_ACTION, this.LOGOUT_ACTION]; + + // Get any associated status code + const status = this.client.clientState.statusCode; + + // Connecting + if (connectionState === ManagedClientState.ConnectionState.CONNECTING + || connectionState === ManagedClientState.ConnectionState.WAITING) { + this.status = { + className: 'connecting', + title : 'CLIENT.DIALOG_HEADER_CONNECTING', + text : { + key: 'CLIENT.TEXT_CLIENT_STATUS_' + connectionState.toUpperCase() + } + }; + } + + // Client error + else if (connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) { + + // Translation IDs for this error code + const errorPrefix = 'CLIENT.ERROR_CLIENT_'; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + 'DEFAULT'; + + // Determine whether the reconnect countdown applies + const countdown = (status in CLIENT_AUTO_RECONNECT) ? this.RECONNECT_COUNTDOWN : null; + + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + this.guacTranslate.translateWithFallback(errorId, defaultErrorId).subscribe( + // Show error status + translationResult => this.notifyConnectionClosed({ + className: 'error', + title : 'CLIENT.DIALOG_HEADER_CONNECTION_ERROR', + text : { + key: translationResult.id + }, + countdown: countdown, + actions : actions + }) + ); + + } + + // Tunnel error + else if (connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR) { + + // Translation IDs for this error code + const errorPrefix = 'CLIENT.ERROR_TUNNEL_'; + const errorId = errorPrefix + status.toString(16).toUpperCase(); + const defaultErrorId = errorPrefix + 'DEFAULT'; + + // Determine whether the reconnect countdown applies + const countdown = (status in TUNNEL_AUTO_RECONNECT) ? this.RECONNECT_COUNTDOWN : null; + + // Use the guacTranslate service to determine if there is a translation for + // this error code; if not, use the default + this.guacTranslate.translateWithFallback(errorId, defaultErrorId).subscribe( + // Show error status + translationResult => this.notifyConnectionClosed({ + className: 'error', + title : 'CLIENT.DIALOG_HEADER_CONNECTION_ERROR', + text : { + key: translationResult.id + }, + countdown: countdown, + actions : actions + }) + ); + + } + + // Disconnected + else if (connectionState === ManagedClientState.ConnectionState.DISCONNECTED) { + this.notifyConnectionClosed({ + title : 'CLIENT.DIALOG_HEADER_DISCONNECTED', + text : { + key: 'CLIENT.TEXT_CLIENT_STATUS_' + connectionState.toUpperCase() + }, + actions: actions + }); + } + + // Hide status for all other states + else + this.status = false; + + } + + /** + * Prompts the user to enter additional connection parameters. If the + * protocol and associated parameters of the underlying connection are not + * yet known, this function has no effect and should be re-invoked once + * the parameters are known. + * + * @param requiredParameters + * The set of all parameters requested by the server via "required" + * instructions, where each object key is the name of a requested + * parameter and each value is the current value entered by the user. + */ + private notifyParametersRequired(requiredParameters: Record): void { + + /** + * Action which submits the current set of parameter values, requesting + * that the connection continue. + */ + const SUBMIT_PARAMETERS = { + name : 'CLIENT.ACTION_CONTINUE', + className: 'button', + callback : () => { + if (this.client) { + const params = this.client.requiredParameters; + this.client.requiredParameters = null; + this.managedClientService.sendArguments(this.client, params); + } + } + }; + + /** + * Action which cancels submission of additional parameters and + * disconnects from the current connection. + */ + const CANCEL_PARAMETER_SUBMISSION = { + name : 'CLIENT.ACTION_CANCEL', + className: 'button', + callback : () => { + this.client.requiredParameters = null; + this.client.client.disconnect(); + } + }; + + // Attempt to prompt for parameters only if the parameters that apply + // to the underlying connection are known + if (!this.client.protocol || !this.client.forms) + return; + + // Prompt for parameters + this.status = { + className : 'parameters-required', + formNamespace : Protocol.getNamespace(this.client.protocol), + forms : this.client.forms, + formModel : requiredParameters, + formSubmitCallback: SUBMIT_PARAMETERS.callback, + actions : [SUBMIT_PARAMETERS, CANCEL_PARAMETER_SUBMISSION] + }; + + } + + /** + * Returns whether the given connection state allows for submission of + * connection parameters via "argv" instructions. + * + * @param connectionState + * The connection state to test, as defined by + * ManagedClientState.ConnectionState. + * + * @returns + * true if the given connection state allows submission of connection + * parameters via "argv" instructions, false otherwise. + */ + private canSubmitParameters(connectionState: string): boolean { + return (connectionState === ManagedClientState.ConnectionState.WAITING || + connectionState === ManagedClientState.ConnectionState.CONNECTED); + } + + /** + * Prevents the default behavior of the given AngularJS event if a + * notification is currently shown and the client is focused. + * + * @param e + * The GuacEvent to selectively prevent. + */ + private preventDefaultDuringNotification(e: GuacEvent): void { + if (this.status && this.client.clientProperties.focused) + e.preventDefault(); + } + + /** + * Logs out the current user, redirecting them to back to the root + * after logout completes. + */ + logout(): void { + this.authenticationService.logout().subscribe({ + error: this.requestService.IGNORE + }); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.html new file mode 100644 index 0000000000..5cc35087a0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.html @@ -0,0 +1,51 @@ + + +
    + + +
    + + + + +
    diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.ts new file mode 100644 index 0000000000..193dbaf6ec --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-panel/guac-client-panel.component.ts @@ -0,0 +1,175 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { AfterViewInit, Component, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; +import _ from 'lodash'; +import { SortService } from '../../../list/services/sort.service'; +import { SessionStorageEntry, SessionStorageFactory } from '../../../storage/session-storage-factory.service'; +import { GuacClientManagerService } from '../../services/guac-client-manager.service'; +import { ManagedClientGroup } from '../../types/ManagedClientGroup'; +import { ManagedClientState } from '../../types/ManagedClientState'; + +/** + * A toolbar/panel which displays a list of active Guacamole connections. The + * panel is fixed to the bottom-right corner of its container and can be + * manually hidden/exposed by the user. + */ +@Component({ + selector: 'guac-client-panel', + templateUrl: './guac-client-panel.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacClientPanelComponent implements AfterViewInit { + + /** + * The ManagedClientGroup instances associated with the active + * connections to be displayed within this panel. + */ + @Input({ required: true }) clientGroups!: ManagedClientGroup[]; + + /** + * Getter/setter for the boolean flag controlling whether the client panel + * is currently hidden. This flag is maintained in session-local storage to + * allow the state of the panel to persist despite navigation within the + * same tab. When hidden, the panel will be collapsed against the right + * side of the container. By default, the panel is visible. + */ + panelHidden: SessionStorageEntry = this.sessionStorageFactory.create(false); + + /** + * Reference to the DOM element containing the scrollable portion of the client + * panel. + */ + @ViewChild('clientPanelConnectionList') private scrollableAreaRef?: ElementRef; + + /** + * The DOM element containing the scrollable portion of the client + * panel. + */ + scrollableArea?: HTMLUListElement; + + /** + * Inject required services. + */ + constructor(private sessionStorageFactory: SessionStorageFactory, + private guacClientManager: GuacClientManagerService, + protected sortService: SortService) { + } + + ngAfterViewInit(): void { + this.scrollableArea = this.scrollableAreaRef?.nativeElement; + + const scrollableArea = this.scrollableArea; + + // Override vertical scrolling, scrolling horizontally instead + scrollableArea?.addEventListener('wheel', function reorientVerticalScroll(e) { + + const deltaMultiplier = { + /* DOM_DELTA_PIXEL */ 0x00: 1, + /* DOM_DELTA_LINE */ 0x01: 15, + /* DOM_DELTA_PAGE */ 0x02: scrollableArea.offsetWidth + } as any; + + if (e.deltaY) { + this.scrollLeft += e.deltaY * (deltaMultiplier[e.deltaMode] || deltaMultiplier(0x01)); + e.preventDefault(); + } + + }); + } + + /** + * Returns whether this panel currently has any client groups + * associated with it. + * + * @return + * true if at least one client group is associated with this + * panel, false otherwise. + */ + hasClientGroups(): boolean { + return this.clientGroups && this.clientGroups.length > 0; + } + + /** + * @borrows ManagedClientGroup.getIdentifier + */ + getIdentifier(group: ManagedClientGroup | string[]): string { + return ManagedClientGroup.getIdentifier(group); + } + + /** + * @borrows ManagedClientGroup.getTitle + */ + getTitle(group: ManagedClientGroup): string | undefined { + return ManagedClientGroup.getTitle(group); + } + + /** + * Returns whether the status of any client within the given client + * group has changed in a way that requires the user's attention. + * This may be due to an error, or due to a server-initiated + * disconnect. + * + * @param clientGroup + * The client group to test. + * + * @returns + * true if the given client requires the user's attention, + * false otherwise. + */ + hasStatusUpdate(clientGroup: ManagedClientGroup): boolean { + return _.findIndex(clientGroup.clients, (client) => { + + // Test whether the client has encountered an error + switch (client.clientState.connectionState) { + // TODO: case ManagedClientState.ConnectionState.CONNECTION_ERROR: + // I cant find a reference to the above state. Should this be something else or should the state be + // added? + case ManagedClientState.ConnectionState.TUNNEL_ERROR: + case ManagedClientState.ConnectionState.DISCONNECTED: + return true; + } + + return false; + + }) !== -1; + } + + /** + * Initiates an orderly disconnect of all clients within the given + * group. The clients are removed from management such that + * attempting to connect to any of the same connections will result + * in new connections being established, rather than displaying a + * notification that the connection has ended. + * + * @param clientGroup + * The group of clients to disconnect. + */ + disconnect(clientGroup: ManagedClientGroup): void { + this.guacClientManager.removeManagedClientGroup(ManagedClientGroup.getIdentifier(clientGroup)); + } + + /** + * Toggles whether the client panel is currently hidden. + */ + togglePanel(): void { + this.panelHidden(!this.panelHidden()); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.html new file mode 100644 index 0000000000..4d3efbd9dd --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.html @@ -0,0 +1,30 @@ + + +
    + {{ client.userCount }} +
      +
        +
      • +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.ts new file mode 100644 index 0000000000..b0820c8178 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-user-count/guac-client-user-count.component.ts @@ -0,0 +1,268 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + AfterViewInit, + Component, + DoCheck, + ElementRef, + Input, + Renderer2, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; + +import { take } from 'rxjs'; +import { AuthenticationResult } from '../../../auth/types/AuthenticationResult'; +import { ManagedClient } from '../../types/ManagedClient'; + + +/** + * A component that displays a status indicator showing the number of users + * joined to a connection. The specific usernames of those users are visible in + * a tooltip on mouseover, and small notifications are displayed as users + * join/leave the connection. + */ +@Component({ + selector: 'guac-client-user-count', + templateUrl: './guac-client-user-count.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacClientUserCountComponent implements AfterViewInit, DoCheck { + + /** + * The client whose current users should be displayed. + */ + @Input({ required: true }) client!: ManagedClient; + + /** + * The maximum number of messages displayed by this directive at any + * given time. Old messages will be discarded as necessary to ensure + * the number of messages displayed never exceeds this value. + */ + private readonly MAX_MESSAGES: number = 3; + + /** + * A reference to the list element that should contain any notifications regarding users + * joining or leaving the connection. + */ + @ViewChild('clientUserCountMessages') + private readonly messagesRef!: ElementRef; + + /** + * The list that should contain any notifications regarding users + * joining or leaving the connection. + */ + private messages!: HTMLUListElement; + + /** + * Map of the usernames of all users of the current connection to the + * number of concurrent connections those users have to the current + * connection. + */ + userCounts: Record = {}; + + /** + * The ManagedClient attached to this directive at the last change + * detection run. + */ + private oldClient: ManagedClient | null = null; + + /** + * The user count of the ManagedClient attached to this directive + * at the last change detection run. + */ + private oldUserCount: number | null = null; + + /** + * Inject required services. + */ + constructor(private translocoService: TranslocoService, + private renderer: Renderer2) { + } + + ngAfterViewInit(): void { + this.messages = this.messagesRef.nativeElement; + } + + /** + * Displays a message noting that a change related to a particular user + * of this connection has occurred. + * + * @param str + * The key of the translation string containing the message to + * display. This translation key must accept "USERNAME" as the + * name of the translation parameter containing the username of + * the user in question. + * + * @param username + * The username of the user in question. + */ + private notify(str: string, username: string): void { + this.translocoService.selectTranslate(str, { 'USERNAME': username }) + .pipe(take(1)) + .subscribe(text => { + + if (this.messages.childNodes.length === 3) + this.renderer.removeChild(this.messages, this.messages.lastChild); + + + const message = this.renderer.createElement('li'); + this.renderer.addClass(message, 'client-user-count-message'); + this.renderer.setProperty(message, 'textContent', text); + this.messages.insertBefore(message, this.messages.firstChild); + + // Automatically remove the notification after its "fadeout" + // animation ends. NOTE: This will not fire if the element is + // not visible at all. + this.renderer.listen(message, 'animationend', () => { + this.renderer.removeChild(this.messages, message); + }); + + }); + } + + /** + * Displays a message noting that a particular user has joined the + * current connection. + * + * @param username + * The username of the user that joined. + */ + private notifyUserJoined(username: string): void { + if (this.isAnonymous(username)) + this.notify('CLIENT.TEXT_ANONYMOUS_USER_JOINED', username); + else + this.notify('CLIENT.TEXT_USER_JOINED', username); + } + + /** + * Displays a message noting that a particular user has left the + * current connection. + * + * @param username + * The username of the user that left. + */ + private notifyUserLeft(username: string): void { + if (this.isAnonymous(username)) + this.notify('CLIENT.TEXT_ANONYMOUS_USER_LEFT', username); + else + this.notify('CLIENT.TEXT_USER_LEFT', username); + } + + /** + * Returns whether the given username represents an anonymous user. + * + * @param username + * The username of the user to check. + * + * @returns + * true if the given username represents an anonymous user, false + * otherwise. + */ + isAnonymous(username: string): boolean { + return username === AuthenticationResult.ANONYMOUS_USERNAME; + } + + /** + * Returns the translation key of the translation string that should be + * used to render the number of connections a user with the given + * username has to the current connection. The appropriate string will + * vary by whether the user is anonymous. + * + * @param username + * The username of the user to check. + * + * @returns + * The translation key of the translation string that should be + * used to render the number of connections the user with the given + * username has to the current connection. + */ + getUserCountTranslationKey(username: string): string { + return this.isAnonymous(username) ? 'CLIENT.INFO_ANONYMOUS_USER_COUNT' : 'CLIENT.INFO_USER_COUNT'; + } + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + + if (this.client !== this.oldClient || this.client.userCount !== this.oldUserCount) { + + this.usersChanged(); + + } + + } + + /** + * Update visible notifications as users join/leave. + */ + private usersChanged(): void { + + // Resynchronize directive with state of any attached client when + // the client changes, to ensure notifications are only shown for + // future changes in users present + if (this.oldClient !== this.client) { + + this.userCounts = {}; + this.oldClient = this.client; + + for (const username in this.client.users) { + const connections = this.client.users[username]; + const count = Object.keys(connections).length; + this.userCounts[username] = count; + } + + return; + + } + + // Display join/leave notifications for users who are currently + // connected but whose connection counts have changed + for (const username in this.client.users) { + const connections = this.client.users[username]; + + const count = Object.keys(connections).length; + const known = this.userCounts[username] || 0; + + if (count > known) + this.notifyUserJoined(username); + else if (count < known) + this.notifyUserLeft(username); + + this.userCounts[username] = count; + + } + + // Display leave notifications for users who are no longer connected + for (const username in this.userCounts) { + if (!this.client.users[username]) { + this.notifyUserLeft(username); + delete this.userCounts[username]; + } + } + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.html new file mode 100644 index 0000000000..ec9f3cae87 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.html @@ -0,0 +1,35 @@ + + +
      +
      +
      -
      +
      + + % +
      +
      +
      +
      +
      + +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.ts new file mode 100644 index 0000000000..972021eae5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client-zoom/guac-client-zoom.component.ts @@ -0,0 +1,75 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { ManagedClient } from '../../types/ManagedClient'; + +/** + * A component for controlling the zoom level and scale-to-fit behavior of + * a single Guacamole client. + */ +@Component({ + selector: 'guac-client-zoom', + templateUrl: './guac-client-zoom.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacClientZoomComponent { + + /** + * The client to control the zoom/autofit of. + */ + @Input({ required: true }) client!: ManagedClient; + + /** + * Zooms in by 10%, automatically disabling autofit. + */ + zoomIn(): void { + this.client.clientProperties.autoFit = false; + this.client.clientProperties.scale += 0.1; + } + + /** + * Zooms out by 10%, automatically disabling autofit. + */ + zoomOut(): void { + this.client.clientProperties.autoFit = false; + this.client.clientProperties.scale -= 0.1; + } + + /** + * Resets the client autofit setting to false. + */ + clearAutoFit(): void { + this.client.clientProperties.autoFit = false; + } + + /** + * Notifies that the autofit setting has been manually changed by the + * user. + */ + autoFitChanged(): void { + + // Reset to 100% scale when autofit is first disabled + if (!this.client.clientProperties.autoFit) + this.client.clientProperties.scale = 1; + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.html new file mode 100644 index 0000000000..d869d16a75 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.html @@ -0,0 +1,36 @@ + + +
      + + +
      + +
      +
      +
      +
      + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.ts new file mode 100644 index 0000000000..7e0545a468 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-client/guac-client.component.ts @@ -0,0 +1,856 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { + Component, + DestroyRef, + DoCheck, + ElementRef, + Inject, + Input, + OnChanges, + OnInit, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { + GuacFrontendEventArguments, + MouseEventName, + TouchEventName +} from '../../../events/types/GuacFrontendEventArguments'; +import { ManagedClientService } from '../../services/managed-client.service'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedDisplayCursor, ManagedDisplayDimensions } from '../../types/ManagedDisplay'; + + +/** + * A component for the guacamole client. + */ +@Component({ + selector: 'guac-client', + templateUrl: './guac-client.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacClientComponent implements OnInit, OnChanges, DoCheck { + + /** + * The client to display within this guacClient directive. + */ + @Input({ required: true, alias: 'client' }) managedClient!: ManagedClient; + + /** + * Whether translation of touch to mouse events should emulate an + * absolute pointer device, or a relative pointer device. + */ + @Input({ required: true }) emulateAbsoluteMouse!: boolean; + + /** + * Whether the local, hardware mouse cursor is in use. + */ + private localCursor = false; + + /** + * The current Guacamole client instance. + */ + private guacamoleClient: Guacamole.Client | null = null; + + /** + * The display of the current Guacamole client instance. + */ + private display: Guacamole.Display | null = null; + + /** + * The element associated with the display of the current + * Guacamole client instance. + */ + private displayElement: Element | null = null; + + /** + * A reference to the element which must contain the Guacamole display element. + */ + @ViewChild('display', { static: true }) + private readonly displayContainerRef!: ElementRef; + + /** + * The element which must contain the Guacamole display element. + */ + private displayContainer!: HTMLDivElement; + + /** + * TODO + */ + @ViewChild('main', { static: true }) + private readonly mainRef!: ElementRef; + + /** + * The main containing element for the entire directive. + */ + private main!: HTMLDivElement; + + /** + * Guacamole mouse event object, wrapped around the main client + * display. + */ + private mouse!: Guacamole.Mouse; + + /** + * Guacamole absolute mouse emulation object, wrapped around the + * main client display. + */ + private touchScreen!: Guacamole.Mouse.Touchscreen; + + /** + * Guacamole relative mouse emulation object, wrapped around the + * main client display. + */ + private touchPad!: Guacamole.Mouse.Touchpad; + + /** + * Guacamole touch event handling object, wrapped around the main + * client dislay. + */ + private touch!: Guacamole.Touch; + + /** + * The last known horizontal scroll position of the client view. + */ + private lastScrollLeft?: number; + + + /** + * The last known vertical scroll position of the client view. + */ + private lastScrollTop?: number; + + /** + * The last known cursor. + */ + private lastCursor?: ManagedDisplayCursor; + + /** + * The last known display size. + */ + private lastDisplaySize: ManagedDisplayDimensions | undefined; + + /** + * The last known value of multi-touch support. + */ + private lastMultiTouchSupport?: number; + + /** + * The last known value of whether absolute mouse emulation is + * preferred. + */ + private lastEmulateAbsoluteMouse?: boolean; + + /** + * The last known scale. + */ + private lastScale?: number; + + /** + * The last known auto-fit setting. + */ + private lastAutoFit?: boolean; + + + /** + * Inject required services. + */ + constructor(private managedClientService: ManagedClientService, + private guacEventService: GuacEventService, + private destroyRef: DestroyRef, + @Inject(DOCUMENT) private document: Document) { + } + + ngOnInit(): void { + this.main = this.mainRef.nativeElement; + this.displayContainer = this.displayContainerRef.nativeElement; + + + this.mouse = new Guacamole.Mouse(this.displayContainer); + this.touchScreen = new Guacamole.Mouse.Touchscreen(this.displayContainer); + this.touchPad = new Guacamole.Mouse.Touchpad(this.displayContainer); + this.touch = new Guacamole.Touch(this.displayContainer); + + // Ensure focus is regained via mousedown before forwarding event + this.mouse.on('mousedown', () => this.document.body.focus.bind(this.document.body)()); + + // Forward all mouse events + this.mouse.onEach(['mousedown', 'mousemove', 'mouseup'], + (event: Guacamole.Event) => this.handleMouseEvent(event as Guacamole.Mouse.Event)); + + // Hide software cursor when mouse leaves display + this.mouse.on('mouseout', () => { + if (!this.display) return; + this.display.showCursor(false); + }); + + // Update remote clipboard if local clipboard changes + this.guacEventService.on('guacClipboard') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ data }) => { + this.managedClientService.setClipboard(this.managedClient, data); + }); + + // Translate local keydown events to remote keydown events if keyboard is enabled + this.guacEventService.on('guacKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym }) => { + if (this.managedClient.clientProperties.focused) { + this.guacamoleClient?.sendKeyEvent(1, keysym); + event.preventDefault(); + } + }); + + // Translate local keyup events to remote keyup events if keyboard is enabled + this.guacEventService.on('guacKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym }) => { + if (this.managedClient.clientProperties.focused) { + this.guacamoleClient?.sendKeyEvent(0, keysym); + event.preventDefault(); + } + }); + + // Universally handle all synthetic keydown events + this.guacEventService.on('guacSyntheticKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ keysym }) => { + if (this.managedClient.clientProperties.focused) + this.guacamoleClient?.sendKeyEvent(1, keysym); + }); + + // Universally handle all synthetic keyup events + this.guacEventService.on('guacSyntheticKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ keysym }) => { + if (this.managedClient.clientProperties.focused) + this.guacamoleClient?.sendKeyEvent(0, keysym); + }); + + this.main.addEventListener('dragenter', this.notifyDragStart.bind(this), false); + this.main.addEventListener('dragover', this.notifyDragStart.bind(this), false); + this.main.addEventListener('dragleave', this.notifyDragEnd.bind(this), false); + + // File drop event handler + this.main.addEventListener('drop', (e) => { + + this.notifyDragEnd(e); + + // Ignore file drops if no attached client + if (!this.managedClient) + return; + + // Upload each file + const files = (e as DragEvent).dataTransfer?.files; + if (!files) return; + + for (let i = 0; i < files.length; i++) + this.managedClientService.uploadFile(this.managedClient, files[i]); + + }, false); + + + this.attachManagedClient(this.managedClient); + } + + /** + * Updates the scale of the attached Guacamole.Client based on current window + * size and "auto-fit" setting. + */ + private updateDisplayScale(): void { + + if (!this.display) return; + + // Calculate scale to fit screen + this.managedClient.clientProperties.minScale = Math.min( + this.main.offsetWidth / Math.max(this.display.getWidth(), 1), + this.main.offsetHeight / Math.max(this.display.getHeight(), 1) + ); + + // Calculate appropriate maximum zoom level + this.managedClient.clientProperties.maxScale = Math.max(this.managedClient.clientProperties.minScale, 3); + + // Clamp zoom level, maintain auto-fit + if (this.display.getScale() < this.managedClient.clientProperties.minScale || this.managedClient.clientProperties.autoFit) + this.managedClient.clientProperties.scale = this.managedClient.clientProperties.minScale; + + else if (this.display.getScale() > this.managedClient.clientProperties.maxScale) + this.managedClient.clientProperties.scale = this.managedClient.clientProperties.maxScale; + + } + + /** + * Scrolls the client view such that the mouse cursor is visible. + * + * @param mouseState + * The current mouse state. + */ + private scrollToMouse(mouseState: Guacamole.Mouse.State): void { + + // Determine mouse position within view + const mouse_view_x = mouseState.x + this.displayContainer.offsetLeft - this.main.scrollLeft; + const mouse_view_y = mouseState.y + this.displayContainer.offsetTop - this.main.scrollTop; + + // Determine viewport dimensions + const view_width = this.main.offsetWidth; + const view_height = this.main.offsetHeight; + + // Determine scroll amounts based on mouse position relative to document + + let scroll_amount_x; + if (mouse_view_x > view_width) + scroll_amount_x = mouse_view_x - view_width; + else if (mouse_view_x < 0) + scroll_amount_x = mouse_view_x; + else + scroll_amount_x = 0; + + let scroll_amount_y; + if (mouse_view_y > view_height) + scroll_amount_y = mouse_view_y - view_height; + else if (mouse_view_y < 0) + scroll_amount_y = mouse_view_y; + else + scroll_amount_y = 0; + + // Scroll (if necessary) to keep mouse on screen. + this.main.scrollLeft += scroll_amount_x; + this.main.scrollTop += scroll_amount_y; + + } + + /** + * Return the name of the event associated with the provided + * mouse event. + * + * @param event + * The mouse event to determine an event name for. + * + * @returns + * The name of the event associated with the provided + * mouse event. + */ + private getMouseEventName(event: Guacamole.Event): MouseEventName { + switch (event.type) { + case 'mousedown': + return 'guacClientMouseDown'; + case 'mouseup': + return 'guacClientMouseUp'; + default: + return 'guacClientMouseMove'; + } + } + + /** + * Handles a mouse event originating from the user's actual mouse. + * This differs from handleEmulatedMouseEvent() in that the + * software mouse cursor must be shown only if the user's browser + * does not support explicitly setting the hardware mouse cursor. + * + * @param event + * The mouse event to handle. + */ + private handleMouseEvent(event: Guacamole.Mouse.Event): void { + + // Do not attempt to handle mouse state changes if the client + // or display are not yet available + if (!this.guacamoleClient || !this.display) + return; + + event.stopPropagation(); + event.preventDefault(); + + // Send mouse state, show cursor if necessary + this.display.showCursor(!this.localCursor); + this.guacamoleClient.sendMouseState(event.state, true); + + // Broadcast the mouse event + this.guacEventService.broadcast(this.getMouseEventName(event), { event, client: this.managedClient }); + + } + + /** + * Handles a mouse event originating from one of Guacamole's mouse + * emulation objects. This differs from handleMouseState() in that + * the software mouse cursor must always be shown (as the emulated + * mouse device will not have its own cursor). + * + * @param event + * The mouse event to handle. + */ + private handleEmulatedMouseEvent: Guacamole.Event.TargetListener = (event: Guacamole.Event) => { + + // Do not attempt to handle mouse state changes if the client + // or display are not yet available + if (!this.guacamoleClient || !this.display) + return; + + const mouseEvent = event as Guacamole.Mouse.Event; + + mouseEvent.stopPropagation(); + mouseEvent.preventDefault(); + + // Ensure software cursor is shown + this.display.showCursor(true); + + // Send mouse state, ensure cursor is visible + this.scrollToMouse(mouseEvent.state); + this.guacamoleClient.sendMouseState(mouseEvent.state, true); + + // Broadcast the mouse event + this.guacEventService.broadcast(this.getMouseEventName(event), { event, client: this.managedClient }); + + }; + + /** + * Return the name of the event associated with the provided + * touch event. + * + * @param event + * The touch event to determine an event name for. + * + * @returns + * The name of the event associated with the provided + * touch event. + */ + private getTouchEventName(event: Guacamole.Event): TouchEventName { + switch (event.type) { + case 'touchstart': + return 'guacClientTouchStart'; + case 'touchend': + return 'guacClientTouchEnd'; + default: + return 'guacClientTouchMove'; + } + } + + /** + * Handles a touch event originating from the user's device. + * + * @param event + * The touch event. + */ + private handleTouchEvent: Guacamole.Event.TargetListener = (event: Guacamole.Event) => { + + // Do not attempt to handle touch state changes if the client + // or display are not yet available + if (!this.guacamoleClient || !this.display) + return; + + const touchEvent = event as Guacamole.Touch.Event; + touchEvent.preventDefault(); + + // Send touch state, hiding local cursor + this.display.showCursor(false); + this.guacamoleClient.sendTouchState(touchEvent.state, true); + + // Broadcast the touch event + this.guacEventService.broadcast(this.getTouchEventName(event), { event, client: this.managedClient }); + + }; + + /** + * Attach any given managed client. + * + * @param managedClient + * The managed client to attach. + */ + private attachManagedClient(managedClient: ManagedClient): void { + + if (!this.displayContainer) + return; + + // Remove any existing display + this.displayContainer.innerHTML = ''; + + // Only proceed if a client is given + if (!this.managedClient) + return; + + // Get Guacamole client instance + this.guacamoleClient = managedClient.client; + + // Attach possibly new display + this.display = this.guacamoleClient.getDisplay(); + this.display.scale(this.managedClient.clientProperties.scale); + + // Add display element + this.displayElement = this.display.getElement(); + this.displayContainer.appendChild(this.displayElement!); + + // Do nothing when the display element is clicked on + this.display.getElement().onclick = (e: any) => { + e.preventDefault(); + return false; + }; + + // Connect and update interface to match required size, deferring + // connecting until a future element resize if the main element + // size (desired display size) is not known and thus can't be sent + // during the handshake + this.mainElementResized(); + } + + ngOnChanges(changes: SimpleChanges): void { + + // Attach any given managed client + if (changes['managedClient']) { + + const managedClient = changes['managedClient'].currentValue as ManagedClient; + this.attachManagedClient(managedClient); + + } + + } + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + // Update actual view scrollLeft when scroll properties change + const scrollLeft = this.managedClient.clientProperties.scrollLeft; + + if (this.lastScrollLeft !== scrollLeft) { + + this.scrollLeftChanged(scrollLeft); + this.lastScrollLeft = scrollLeft; + + } + + + // Update actual view scrollTop when scroll properties change + const scrollTop = this.managedClient.clientProperties.scrollTop; + + if (this.lastScrollTop !== scrollTop) { + + this.scrollTopChanged(scrollTop); + this.lastScrollTop = scrollTop; + + } + + // Update scale when display is resized + const displaySize = this.managedClient.managedDisplay?.size; + + if (this.lastDisplaySize !== displaySize) { + + Promise.resolve().then(() => this.updateDisplayScale()); + this.lastDisplaySize = displaySize; + + } + + // Keep local cursor up-to-date + const cursor = this.managedClient.managedDisplay?.cursor; + + if (this.lastCursor !== cursor) { + + this.setCursor(cursor); + this.lastCursor = cursor; + + } + + // Update touch event handling depending on remote multi-touch + // support and mouse emulation mode + const multiTouchSupport = this.managedClient.multiTouchSupport; + const emulateAbsoluteMouse = this.emulateAbsoluteMouse; + + if (this.lastMultiTouchSupport !== multiTouchSupport || this.lastEmulateAbsoluteMouse !== emulateAbsoluteMouse) { + + this.touchBehaviorChanged(); + this.lastMultiTouchSupport = multiTouchSupport; + this.lastEmulateAbsoluteMouse = emulateAbsoluteMouse; + + } + + + // Adjust scale if modified externally + const scale = this.managedClient.clientProperties.scale; + + if (this.lastScale !== scale) { + + this.changeScale(scale); + this.lastScale = scale; + + } + + + // If autofit is set, the scale should be set to the minimum scale, filling the screen + const autoFit = this.managedClient.clientProperties.autoFit; + + if (this.lastAutoFit !== autoFit) { + + this.changeAutoFit(autoFit); + this.lastAutoFit = autoFit; + + } + + } + + /** + * Update actual view scrollLeft when scroll properties change. + * + * @param scrollLeft New scrollLeft value + */ + private scrollLeftChanged(scrollLeft: number): void { + this.main.scrollLeft = scrollLeft; + this.managedClient.clientProperties.scrollLeft = this.main.scrollLeft; + } + + /** + * Update actual view scrollTop when scroll properties change. + * + * @param scrollTop New scrollTop value + */ + private scrollTopChanged(scrollTop: number): void { + this.main.scrollTop = scrollTop; + this.managedClient.clientProperties.scrollTop = this.main.scrollTop; + } + + /** + * Keep the local cursor up-to-date. + * @param cursor The cursor to set. + */ + private setCursor(cursor: ManagedDisplayCursor | undefined): void { + if (cursor) + this.localCursor = this.mouse.setCursor(cursor.canvas!, cursor.x!, cursor.y!); + } + + /** + * Update touch event handling depending on remote multi-touch + * support and mouse emulation mode. + */ + private touchBehaviorChanged() { + + // Clear existing event handling + this.touch.offEach(['touchstart', 'touchmove', 'touchend'], this.handleTouchEvent); + this.touchScreen.offEach(['mousedown', 'mousemove', 'mouseup'], this.handleEmulatedMouseEvent); + this.touchPad.offEach(['mousedown', 'mousemove', 'mouseup'], this.handleEmulatedMouseEvent); + + // Directly forward local touch events + if (this.managedClient.multiTouchSupport) + this.touch.onEach(['touchstart', 'touchmove', 'touchend'], this.handleTouchEvent); + + // Switch to touchscreen if mouse emulation is required and + // absolute mouse emulation is preferred + else if (this.emulateAbsoluteMouse) + this.touchScreen.onEach(['mousedown', 'mousemove', 'mouseup'], this.handleEmulatedMouseEvent); + + // Use touchpad for mouse emulation if absolute mouse emulation + // is not preferred + else + this.touchPad.onEach(['mousedown', 'mousemove', 'mouseup'], this.handleEmulatedMouseEvent); + } + + /** + * Adjust scale if modified externally. + * @param scale The new scale value. + */ + private changeScale(scale: number): void { + + // Fix scale within limits + scale = Math.max(scale, this.managedClient.clientProperties.minScale); + scale = Math.min(scale, this.managedClient.clientProperties.maxScale); + + // If at minimum zoom level, hide scroll bars + if (scale === this.managedClient.clientProperties.minScale) + this.main.style.overflow = 'hidden'; + + // If not at minimum zoom level, show scroll bars + else + this.main.style.overflow = 'auto'; + + // Apply scale if client attached + if (this.display) + this.display.scale(scale); + + if (scale !== this.managedClient.clientProperties.scale) + this.managedClient.clientProperties.scale = scale; + + } + + /** + * Update the scale of the attached Guacamole.Client if auto-fit + * is enabled. + */ + private changeAutoFit(autoFit: boolean): void { + if (autoFit) + this.managedClient.clientProperties.scale = this.managedClient.clientProperties.minScale; + } + + /** + * Sends the current size of the main element (the display container) + * to the Guacamole server, requesting that the remote display be + * resized. If the Guacamole client is not yet connected, it will be + * connected and the current size will sent through the initial + * handshake. If the size of the main element is not yet known, this + * function may need to be invoked multiple times until the size is + * known and the client may be connected. + */ + mainElementResized(): void { + + // Send new display size, if changed + if (this.guacamoleClient && this.display && this.main.offsetWidth && this.main.offsetHeight) { + + // Connect, if not already connected + this.managedClientService.connect(this.managedClient, this.main.offsetWidth, this.main.offsetHeight); + + const pixelDensity = window.devicePixelRatio || 1; + const width = this.main.offsetWidth * pixelDensity; + const height = this.main.offsetHeight * pixelDensity; + + if (this.display.getWidth() !== width || this.display.getHeight() !== height) + this.guacamoleClient.sendSize(width, height); + + } + + this.updateDisplayScale(); + + } + + // Scroll client display if absolute mouse is in use (the same drag + // gesture is needed for moving the mouse pointer with relative mouse) + clientDrag(inProgress: boolean, startX: number, startY: number, currentX: number, currentY: number, deltaX: number, deltaY: number): boolean { + + if (this.emulateAbsoluteMouse) { + this.managedClient.clientProperties.scrollLeft -= deltaX; + this.managedClient.clientProperties.scrollTop -= deltaY; + } + + return false; + + } + + /** + * If a pinch gesture is in progress, the scale of the client display when + * the pinch gesture began. + */ + private initialScale: number | null = null; + + /** + * If a pinch gesture is in progress, the X coordinate of the point on the + * client display that was centered within the pinch at the time the + * gesture began. + */ + private initialCenterX = 0; + + /** + * If a pinch gesture is in progress, the Y coordinate of the point on the + * client display that was centered within the pinch at the time the + * gesture began. + */ + private initialCenterY = 0; + + /** + * Zoom and pan client via pinch gestures. + */ + clientPinch(inProgress: boolean, startLength: number, currentLength: number, centerX: number, centerY: number): boolean { + + // Do not handle pinch gestures if they would conflict with remote + // handling of similar gestures + if (this.managedClient.multiTouchSupport > 1) + return false; + + // Do not handle pinch gestures while relative mouse is in use (2+ + // contact point gestures are used by relative mouse emulation to + // support right click, middle click, and scrolling) + if (!this.emulateAbsoluteMouse) + return false; + + // Stop gesture if not in progress + if (!inProgress) { + this.initialScale = null; + return false; + } + + // Set initial scale if gesture has just started + if (!this.initialScale) { + this.initialScale = this.managedClient.clientProperties.scale; + this.initialCenterX = (centerX + this.managedClient.clientProperties.scrollLeft) / this.initialScale; + this.initialCenterY = (centerY + this.managedClient.clientProperties.scrollTop) / this.initialScale; + } + + // Determine new scale absolutely + let currentScale = this.initialScale * currentLength / startLength; + + // Fix scale within limits - scroll will be miscalculated otherwise + currentScale = Math.max(currentScale, this.managedClient.clientProperties.minScale); + currentScale = Math.min(currentScale, this.managedClient.clientProperties.maxScale); + + // Update scale based on pinch distance + this.managedClient.clientProperties.autoFit = false; + this.managedClient.clientProperties.scale = currentScale; + + // Scroll display to keep original pinch location centered within current pinch + this.managedClient.clientProperties.scrollLeft = this.initialCenterX * currentScale - centerX; + this.managedClient.clientProperties.scrollTop = this.initialCenterY * currentScale - centerY; + + return false; + + } + + /** + * Whether a drag/drop operation is currently in progress (the user has + * dragged a file over the Guacamole connection but has not yet + * dropped it). + */ + dropPending = false; + + /** + * Displays a visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param e + * The event related to the in-progress drag/drop operation. + */ + notifyDragStart(e: Event): void { + + e.preventDefault(); + e.stopPropagation(); + + this.dropPending = true; + + } + + /** + * Removes the visual indication that dropping the file currently + * being dragged is possible. Further propagation and default behavior + * of the given event is automatically prevented. + * + * @param e + * The event related to the end of the former drag/drop operation. + */ + private notifyDragEnd(e: Event): void { + + e.preventDefault(); + e.stopPropagation(); + + this.dropPending = false; + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.html new file mode 100644 index 0000000000..631f467244 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.html @@ -0,0 +1,37 @@ + + +
      + + +
      + + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.ts new file mode 100644 index 0000000000..1fca5470a5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-browser/guac-file-browser.component.ts @@ -0,0 +1,202 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Component, + computed, + DestroyRef, + Input, + OnChanges, + OnInit, + Signal, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { ManagedFilesystemService } from '../../services/managed-filesystem.service'; +import { ManagedFilesystem } from '../../types/ManagedFilesystem'; + +/** + * A component which displays the contents of a filesystem received through the + * Guacamole client. + */ +@Component({ + selector: 'guac-file-browser', + templateUrl: './guac-file-browser.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacFileBrowserComponent implements OnInit, OnChanges { + + /** + * The filesystem to display. + */ + @Input({ required: true }) filesystem: ManagedFilesystem | null = null; + + /** + * Signal which fires whenever the files that should be displayed change. + */ + files: Signal | null = null; + + /** + * The index of the file which is currently focused. + */ + protected focusedFileIndex = -1; + + /** + * Inject required services. + */ + constructor(private managedFilesystemService: ManagedFilesystemService, + private guacEventService: GuacEventService, + private destroyRef: DestroyRef) { + } + + /** + * TOOD + */ + ngOnInit(): void { + + // Refresh file browser when any upload completes + this.guacEventService.on('guacUploadComplete') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + + // Refresh filesystem, if it exists + if (this.filesystem) + this.managedFilesystemService.refresh(this.filesystem, this.filesystem.currentDirectory()); + + }); + } + + + /** + * If the filesystem changes, update the files. + */ + ngOnChanges(changes: SimpleChanges): void { + if (changes['filesystem']) { + + const filesystem = changes['filesystem'].currentValue as ManagedFilesystem | null; + + if (!filesystem) + return; + + // Update files when the current directory or the files within it change + this.files = computed(() => { + return this.sortFiles(filesystem.currentDirectory().files()); + }); + + } + } + + /** + * Returns whether the given file is a directory. + * + * @param file + * The file to test. + * + * @returns + * true if the given file is a directory, false otherwise. + */ + isDirectory(file: ManagedFilesystem.File): boolean { + return file.type === ManagedFilesystem.File.Type.DIRECTORY; + } + + /** + * Changes the currently-displayed directory to the given + * directory. + * + * @param file + * The directory to change to. + */ + changeDirectory(file: ManagedFilesystem.File): void { + if (this.filesystem) + this.managedFilesystemService.changeDirectory(this.filesystem, file); + } + + /** + * Initiates a download of the given file. The progress of the + * download can be observed through guacFileTransferManager. + * + * @param file + * The file to download. + */ + downloadFile(file: ManagedFilesystem.File): void { + if (this.filesystem) + this.managedFilesystemService.downloadFile(this.filesystem, file.streamName); + } + + /** + * Sets the given file as focused, such that it will be highlighted. + * + * @param index + * The index of the file to focus. + */ + focusFile(index: number): void { + this.focusedFileIndex = index; + } + + /** + * trackBy Function for the list of files. + */ + trackByStreamName(index: number, file: ManagedFilesystem.File): string { + return file.streamName; + } + + /** + * Sorts the given map of files, returning an array of those files + * grouped by file type (directories first, followed by non- + * directories) and sorted lexicographically. + * + * @param files + * The map of files to sort. + * + * @returns + * An array of all files in the given map, sorted + * lexicographically with directories first, followed by non- + * directories. + */ + private sortFiles(files: Record): ManagedFilesystem.File[] { + + // Get all given files as an array + const unsortedFiles = []; + for (const name in files) + unsortedFiles.push(files[name]); + + // Sort files - directories first, followed by all other files + // sorted by name + return unsortedFiles.sort((a, b) => { + + // Directories come before non-directories + if (this.isDirectory(a) && !this.isDirectory(b)) + return -1; + + // Non-directories come after directories + if (!this.isDirectory(a) && this.isDirectory(b)) + return 1; + + // All other combinations are sorted by name + return a.name!.localeCompare(b.name!); + + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.html new file mode 100644 index 0000000000..c317209690 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.html @@ -0,0 +1,42 @@ + + +
      + + +
      +

      {{ 'CLIENT.SECTION_HEADER_FILE_TRANSFERS' | transloco }}

      + +
      + + +
      +
      +

      {{ client.name }}

      +
      + + +
      +
      +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.ts new file mode 100644 index 0000000000..600ff8c75c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer-manager/guac-file-transfer-manager.component.ts @@ -0,0 +1,105 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { ManagedClientService } from '../../services/managed-client.service'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedClientGroup } from '../../types/ManagedClientGroup'; +import { ManagedFileTransferState } from '../../types/ManagedFileTransferState'; + +/** + * Component which displays all active file transfers. + */ +@Component({ + selector: 'guac-file-transfer-manager', + templateUrl: './guac-file-transfer-manager.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacFileTransferManagerComponent { + + /** + * The client group whose file transfers should be managed by this + * directive. + */ + @Input({ required: true }) clientGroup!: ManagedClientGroup | null; + + /** + * Inject required services. + */ + constructor(private managedClientService: ManagedClientService) { + } + + /** + * Determines whether the given file transfer state indicates an + * in-progress transfer. + * + * @param transferState + * The file transfer state to check. + * + * @returns + * true if the given file transfer state indicates an in- + * progress transfer, false otherwise. + */ + private isInProgress(transferState: ManagedFileTransferState): boolean { + switch (transferState.streamState) { + + // IDLE or OPEN file transfers are active + case ManagedFileTransferState.StreamState.IDLE: + case ManagedFileTransferState.StreamState.OPEN: + return true; + + // All others are not active + default: + return false; + + } + } + + /** + * Removes all file transfers which are not currently in-progress. + */ + clearCompletedTransfers(): void { + + // Nothing to clear if no client group attached + if (!this.clientGroup) + return; + + // Remove completed uploads + this.clientGroup.clients.forEach(client => { + client.uploads = client.uploads.filter(upload => this.isInProgress(upload.transferState)); + }); + + } + + /** + * @borrows ManagedClientGroup.hasMultipleClients + */ + hasMultipleClients(group: ManagedClientGroup | null): boolean { + return ManagedClientGroup.hasMultipleClients(group); + } + + /** + * @borrows ManagedClientService.hasTransfers + */ + hasTransfers(client: ManagedClient): boolean { + return this.managedClientService.hasTransfers(client); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.html new file mode 100644 index 0000000000..bdcdd1742d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.html @@ -0,0 +1,44 @@ + + +
      + + +
      + + +
      +
      +
      +
      + {{ transfer.filename }} +
      + + +

      {{ translatedErrorMessage }}

      + +
      + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.ts new file mode 100644 index 0000000000..3cbbfbd800 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-file-transfer/guac-file-transfer.component.ts @@ -0,0 +1,234 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DoCheck, Input, ViewEncapsulation } from '@angular/core'; +import FileSaver from 'file-saver'; +import { GuacTranslateService } from '../../services/guac-translate.service'; +import { ManagedFileTransferState } from '../../types/ManagedFileTransferState'; +import { ManagedFileUpload } from '../../types/ManagedFileUpload'; + +/** + * Component which displays an active file transfer, providing links for + * downloads, if applicable. + */ +@Component({ + selector: 'guac-file-transfer', + templateUrl: './guac-file-transfer.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacFileTransferComponent implements DoCheck { + + /** + * The file transfer to display. + */ + @Input({ required: true }) transfer!: ManagedFileUpload; + + /** + * The translated error message for the current status code. + */ + translatedErrorMessage?: string = ''; + + /** + * TODO + * @private + */ + private lastTransferStatusCode?: number; + + /** + * Inject required services. + */ + constructor(private guacTranslate: GuacTranslateService) { + } + + /** + * Returns the unit string that is most appropriate for the + * number of bytes transferred thus far - either 'gb', 'mb', 'kb', + * or 'b'. + * + * @returns + * The unit string that is most appropriate for the number of + * bytes transferred thus far. + */ + getProgressUnit(): string { + + const bytes = this.transfer.progress || 0; + + // Gigabytes + if (bytes > 1000000000) + return 'gb'; + + // Megabytes + if (bytes > 1000000) + return 'mb'; + + // Kilobytes + if (bytes > 1000) + return 'kb'; + + // Bytes + return 'b'; + + } + + /** + * Returns the amount of data transferred thus far, in the units + * returned by getProgressUnit(). + * + * @returns + * The amount of data transferred thus far, in the units + * returned by getProgressUnit(). + */ + getProgressValue(): number | string | undefined { + + const bytes = this.transfer.progress; + if (!bytes) + return bytes; + + // Convert bytes to necessary units + switch (this.getProgressUnit()) { + + // Gigabytes + case 'gb': + return (bytes / 1000000000).toFixed(1); + + // Megabytes + case 'mb': + return (bytes / 1000000).toFixed(1); + + // Kilobytes + case 'kb': + return (bytes / 1000).toFixed(1); + + // Bytes + case 'b': + default: + return bytes; + + } + + } + + /** + * Returns the percentage of bytes transferred thus far, if the + * overall length of the file is known. + * + * @returns + * The percentage of bytes transferred thus far, if the + * overall length of the file is known. + */ + getPercentDone(): number { + return this.transfer.progress! / this.transfer.length! * 100; + } + + /** + * Determines whether the associated file transfer is in progress. + * + * @returns + * true if the file transfer is in progress, false otherwise. + */ + isInProgress(): boolean { + + // Not in progress if there is no transfer + if (!this.transfer) + return false; + + // Determine in-progress status based on stream state + switch (this.transfer.transferState.streamState) { + + // IDLE or OPEN file transfers are active + case ManagedFileTransferState.StreamState.IDLE: + case ManagedFileTransferState.StreamState.OPEN: + return true; + + // All others are not active + default: + return false; + + } + + } + + /** + * Returns whether the file associated with this file transfer can + * be saved locally via a call to save(). + * + * @returns + * true if a call to save() will result in the file being + * saved, false otherwise. + */ + isSavable(): boolean { + return !!(this.transfer as any).blob; + } + + /** + * Saves the downloaded file, if any. If this transfer is an upload + * or the download is not yet complete, this function has no + * effect. + */ + save(): void { + + // Ignore if no blob exists + if (!(this.transfer as any).blob) + return; + + // Save file + FileSaver.saveAs((this.transfer as any).blob, this.transfer.filename); + + } + + /** + * Returns whether an error has occurred. If an error has occurred, + * the transfer is no longer active, and the text of the error can + * be read from getErrorText(). + * + * @returns + * true if an error has occurred during transfer, false + * otherwise. + */ + hasError(): boolean { + return this.transfer.transferState.streamState === ManagedFileTransferState.StreamState.ERROR; + } + + /** + * Handle changes to the transfer status code. + */ + ngDoCheck(): void { + + const currentTransferStatusCode = this.transfer.transferState.statusCode; + + if (this.lastTransferStatusCode !== currentTransferStatusCode) { + + // Determine translation name of error + const errorName = 'CLIENT.ERROR_UPLOAD_' + currentTransferStatusCode.toString(16).toUpperCase(); + + // Use translation string, or the default if no translation is found for this error code + this.guacTranslate.translateWithFallback(errorName, 'CLIENT.ERROR_UPLOAD_DEFAULT').subscribe( + translationResult => this.translatedErrorMessage = translationResult.message + ); + + // Remember the new status code + this.lastTransferStatusCode = currentTransferStatusCode; + + } + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.html new file mode 100644 index 0000000000..a048eae43b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.html @@ -0,0 +1,30 @@ + + +
      + + +
      +
      +
      +
      +
      +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.ts new file mode 100644 index 0000000000..eb76461599 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-thumbnail/guac-thumbnail.component.ts @@ -0,0 +1,149 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + AfterViewInit, + Component, + ElementRef, + Input, + OnChanges, + Renderer2, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { ManagedClient } from '../../types/ManagedClient'; + +/** + * A component for displaying a Guacamole client as a non-interactive + * thumbnail. + */ +@Component({ + selector: 'guac-thumbnail', + templateUrl: './guac-thumbnail.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacThumbnailComponent implements AfterViewInit, OnChanges { + + /** + * The client to display within this guacThumbnail directive. + */ + @Input() client?: ManagedClient; + + /** + * The display of the current Guacamole client instance. + */ + private display: Guacamole.Display | null = null; + + /** + * The element associated with the display of the current + * Guacamole client instance. + */ + private displayElement: Element | null = null; + + /** + * The element which must contain the Guacamole display element. + */ + @ViewChild('display') private displayContainer?: ElementRef; + + /** + * Reference to the main containing element for the component. + */ + @ViewChild('main') private mainRef!: ElementRef; + + /** + * The main containing element for the component. + */ + private main!: HTMLDivElement; + + /** + * Inject required services. + */ + constructor(private renderer: Renderer2) { + } + + /** + * Attach the given client after the view has been initialized. + */ + ngAfterViewInit(): void { + this.main = this.mainRef.nativeElement; + this.attachClient(this.client); + } + + /** + * When the client changes, attach the new client. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['client']) { + const managedClient = changes['client'].currentValue as ManagedClient | undefined; + this.attachClient(managedClient); + } + + } + + /** + * Updates the scale of the attached Guacamole.Client based on current window + * size and "auto-fit" setting. + */ + updateDisplayScale(): void { + + if (!this.display) return; + + // Fit within available area + this.display.scale(Math.min( + this.main.offsetWidth / Math.max(this.display.getWidth(), 1), + this.main.offsetHeight / Math.max(this.display.getHeight(), 1) + )); + + } + + /** + * Attach any given managed client. + * + * @param managedClient + * The managed client to attach. + */ + attachClient(managedClient?: ManagedClient): void { + + // Only proceed if a display container present + if (!this.displayContainer) + return; + + // Remove any existing display + this.renderer.setProperty(this.displayContainer.nativeElement, 'innerHTML', ''); + + // Only proceed if a client is given + if (!managedClient) + return; + + // Get Guacamole client instance + const client: Guacamole.Client = managedClient.client; + + // Attach possibly new display + this.display = client.getDisplay(); + + // Add display element + this.displayElement = this.display.getElement(); + this.renderer.appendChild(this.displayContainer.nativeElement, this.displayElement); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.html new file mode 100644 index 0000000000..894882547c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.html @@ -0,0 +1,55 @@ + + +
      +
      +
      + +
      +

      + + {{ client.title }} + + +

      + + + + + + + + +
      + +
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.ts new file mode 100644 index 0000000000..0c5bf7f2d0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-clients/guac-tiled-clients.component.ts @@ -0,0 +1,230 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DoCheck, Input, ViewEncapsulation } from '@angular/core'; + +import { GuacClickCallback, GuacEventService } from 'guacamole-frontend-lib'; +import _ from 'lodash'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { ManagedClientService } from '../../services/managed-client.service'; +import { ManagedArgument } from '../../types/ManagedArgument'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedClientGroup } from '../../types/ManagedClientGroup'; + +/** + * A component which displays one or more Guacamole clients in an evenly-tiled + * view. The number of rows and columns used for the arrangement of tiles is + * automatically determined by the number of clients present. + */ +@Component({ + selector: 'guac-tiled-clients', + templateUrl: './guac-tiled-clients.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacTiledClientsComponent implements DoCheck { + + /** + * The function to invoke when the "close" button in the header of a + * client tile is clicked. The ManagedClient that is closed will be + * supplied as the function argument. + */ + @Input({ required: true }) onClose!: (client: ManagedClient) => void; + + /** + * The group of Guacamole clients that should be displayed in an + * evenly-tiled grid arrangement. + */ + @Input({ required: true }) clientGroup!: ManagedClientGroup | null; + + /** + * Whether translation of touch to mouse events should emulate an + * absolute pointer device, or a relative pointer device. + */ + @Input({ required: true }) emulateAbsoluteMouse!: boolean; + + /** + * TODO + * @private + */ + private lastFocusedClient?: ManagedClient | null; + + /** + * TODO + * @private + */ + private lastFocusedClientArguments?: Record; + + /** + * TODO + * @private + */ + private lastProtocol: string | undefined | null; + + /** + * Inject required services. + */ + constructor(private guacEventService: GuacEventService, + private managedClientService: ManagedClientService) { + } + + /** + * Returns the currently-focused ManagedClient. If there is no such + * client, or multiple clients are focused, null is returned. + * + * @returns + * The currently-focused client, or null if there are no focused + * clients or if multiple clients are focused. + */ + private getFocusedClient(): ManagedClient | null { + + const managedClientGroup = this.clientGroup; + if (managedClientGroup) { + const focusedClients = _.filter(managedClientGroup.clients, client => client.clientProperties.focused); + if (focusedClients.length === 1) + return focusedClients[0]; + } + + return null; + + } + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + // Notify whenever identify of currently-focused client changes + const newFocusedClient: ManagedClient | null = this.getFocusedClient(); + + if (this.lastFocusedClient !== newFocusedClient) { + + this.guacEventService.broadcast('guacClientFocused', { newFocusedClient }); + + // Remember the new focused Client + this.lastFocusedClient = newFocusedClient; + + } + + // Notify whenever arguments of currently-focused client changes + const newArguments: Record | undefined = this.getFocusedClient()?.arguments; + + if (!_.isEqual(this.lastFocusedClientArguments, newArguments)) { + + this.guacEventService.broadcast('guacClientArgumentsUpdated', { focusedClient: newFocusedClient }); + + // Remember the new arguments + this.lastFocusedClientArguments = newArguments; + } + + // Notify whenever protocol of currently-focused client changes + const newProtocol: string | undefined | null = this.getFocusedClient()?.protocol; + + if (this.lastProtocol !== newProtocol) { + + this.guacEventService.broadcast('guacClientProtocolUpdated', { focusedClient: newFocusedClient }); + + // Remember the new protocol + this.lastProtocol = newProtocol; + } + + } + + /** + * Returns a callback for guacClick that assigns or updates keyboard + * focus to the given client, allowing that client to receive and + * handle keyboard events. Multiple clients may have keyboard focus + * simultaneously. + * + * @param client + * The client that should receive keyboard focus. + * + * @return + * The callback that guacClient should invoke when the given client + * has been clicked. + */ + getFocusAssignmentCallback(client: ManagedClient): GuacClickCallback { + return (shift, ctrl) => { + + // Clear focus of all other clients if not selecting multiple + if (!shift && !ctrl) { + this.clientGroup?.clients.forEach(client => { + client.clientProperties.focused = false; + }); + } + + client.clientProperties.focused = true; + + // Fill in any gaps if performing rectangular multi-selection + // via shift-click + if (shift && this.clientGroup) { + + let minRow = this.clientGroup.rows - 1; + let minColumn = this.clientGroup.columns - 1; + let maxRow = 0; + let maxColumn = 0; + + // Determine extents of selected area + ManagedClientGroup.forEach(this.clientGroup, (client, row, column) => { + if (client.clientProperties.focused) { + minRow = Math.min(minRow, row); + minColumn = Math.min(minColumn, column); + maxRow = Math.max(maxRow, row); + maxColumn = Math.max(maxColumn, column); + } + }); + + ManagedClientGroup.forEach(this.clientGroup, (client, row, column) => { + client.clientProperties.focused = + row >= minRow + && row <= maxRow + && column >= minColumn + && column <= maxColumn; + }); + + } + + }; + } + + /** + * @borrows ManagedClientGroup.hasMultipleClients + */ + hasMultipleClients(group: ManagedClientGroup | null): boolean { + return ManagedClientGroup.hasMultipleClients(group); + } + + /** + * @borrows ManagedClientGroup.getClientGrid + */ + getClientGrid(group: ManagedClientGroup | null): ManagedClient[][] { + if (group === null) + return []; + + return ManagedClientGroup.getClientGrid(group); + } + + /** + * @borrows ManagedClientService~isShared + */ + isShared(client: ManagedClient): boolean { + return this.managedClientService.isShared(client); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.html new file mode 100644 index 0000000000..b0ad54b359 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.html @@ -0,0 +1,33 @@ + + +
      +
      +
      + +
      + +
      + +
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.ts new file mode 100644 index 0000000000..8a25814edc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/components/guac-tiled-thumbnails/guac-tiled-thumbnails.component.ts @@ -0,0 +1,67 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { ManagedClient } from '../../types/ManagedClient'; +import { ManagedClientGroup } from '../../types/ManagedClientGroup'; + +/** + * A component for displaying a group of Guacamole clients as a non-interactive + * thumbnail of tiled client displays. + */ +@Component({ + selector: 'guac-tiled-thumbnails', + templateUrl: './guac-tiled-thumbnails.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacTiledThumbnailsComponent { + + /** + * The group of clients to display as a thumbnail of tiled client + * displays. + */ + @Input({ required: true }) clientGroup!: ManagedClientGroup; + + /** + * The overall height of the thumbnail view of the tiled grid of + * clients within the client group, in pixels. This value is + * intentionally based off a snapshot of the current browser size at + * the time the directive comes into existence to ensure the contents + * of the thumbnail are familiar in appearance and aspect ratio. + */ + height = Math.min(window.innerHeight, 128); + + /** + * The overall width of the thumbnail view of the tiled grid of + * clients within the client group, in pixels. This value is + * intentionally based off a snapshot of the current browser size at + * the time the directive comes into existence to ensure the contents + * of the thumbnail are familiar in appearance and aspect ratio. + */ + width = window.innerWidth / window.innerHeight * this.height; + + /** + * @borrows ManagedClientGroup.getClientGrid + */ + getClientGrid(group: ManagedClientGroup): ManagedClient[][] { + return ManagedClientGroup.getClientGrid(group); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/directives/guac-zoom-ctrl.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/directives/guac-zoom-ctrl.directive.ts new file mode 100644 index 0000000000..6981d08716 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/directives/guac-zoom-ctrl.directive.ts @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * A directive which modifies the parsing of a form control value when used + * on a number input field. The behavior of this directive for other input elements + * is undefined. Converts between human-readable zoom percentage and display scale. + */ +@Directive({ + selector: '[guacZoomCtrl]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GuacZoomCtrlDirective), + multi: true + } + ], + standalone: false +}) +export class GuacZoomCtrlDirective implements ControlValueAccessor { + + /** + * Callback function that has to be called when the control's value changes in the UI. + * + * @param value + * The new value of the control. + */ + private onChange!: (value: any) => void; + + /** + * Callback function that has to be called when the control's value changes in the UI. + */ + private onTouched!: () => void; + + constructor(private renderer: Renderer2, private el: ElementRef) { + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty(this.el.nativeElement, 'disabled', isDisabled); + } + + /** + * Called by the forms API to write to the view when programmatic changes from model to view are requested. + * Multiplies the value by 100. + * + * @param value + * The new value for the input element. + */ + writeValue(value: any): void { + const newValue = Math.round(value * 100); + this.renderer.setProperty(this.el.nativeElement, 'value', newValue); + } + + + /** + * Form control will be updated on input. + * The value is divided by 100. + */ + @HostListener('input', ['$event.target.value']) + onInput(value: string): void { + const parsedValue = Math.round(Number(value)) / 100; + this.onChange(parsedValue); + this.onTouched(); + } + +} diff --git a/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-client-manager.service.ts similarity index 68% rename from guacamole/src/main/frontend/src/app/client/services/guacClientManager.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-client-manager.service.ts index 990d10cdec..2dd8368432 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacClientManager.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-client-manager.service.ts @@ -17,68 +17,74 @@ * under the License. */ +import { Injectable } from '@angular/core'; +import _ from 'lodash'; +import { SessionStorageEntry, SessionStorageFactory } from '../../storage/session-storage-factory.service'; +import { ManagedClient } from '../types/ManagedClient'; +import { ManagedClientGroup } from '../types/ManagedClientGroup'; +import { ManagedClientService } from './managed-client.service'; + /** * A service for managing several active Guacamole clients. */ -angular.module('client').factory('guacClientManager', ['$injector', - function guacClientManager($injector) { - - // Required types - const ManagedClient = $injector.get('ManagedClient'); - const ManagedClientGroup = $injector.get('ManagedClientGroup'); - - // Required services - const $window = $injector.get('$window'); - const sessionStorageFactory = $injector.get('sessionStorageFactory'); - - var service = {}; +@Injectable({ + providedIn: 'root' +}) +export class GuacClientManagerService { /** * Getter/setter which retrieves or sets the map of all active managed * clients. Each key is the ID of the connection used by that client. - * - * @type Function */ - var storedManagedClients = sessionStorageFactory.create({}, function destroyClientStorage() { + private readonly storedManagedClients: SessionStorageEntry> = this.sessionStorageFactory.create({}, () => { // Disconnect all clients when storage is destroyed - service.clear(); + this.clear(); }); - /** - * Returns a map of all active managed clients. Each key is the ID of the - * connection used by that client. - * - * @returns {Object.} - * A map of all active managed clients. - */ - service.getManagedClients = function getManagedClients() { - return storedManagedClients(); - }; - /** * Getter/setter which retrieves or sets the array of all active managed * client groups. - * - * @type Function */ - const storedManagedClientGroups = sessionStorageFactory.create([], function destroyClientGroupStorage() { + private readonly storedManagedClientGroups: SessionStorageEntry = this.sessionStorageFactory.create([] as ManagedClientGroup[], () => { // Disconnect all clients when storage is destroyed - service.clear(); + this.clear(); }); + /** + * Inject required services. + */ + constructor(private sessionStorageFactory: SessionStorageFactory, + private managedClientService: ManagedClientService) { + + // Disconnect all clients when window is unloaded + window.addEventListener('unload', () => this.clear()); + + } + + /** + * Returns a map of all active managed clients. Each key is the ID of the + * connection used by that client. + * + * @returns + * A map of all active managed clients. + */ + getManagedClients(): Record { + return this.storedManagedClients(); + } + /** * Returns an array of all managed client groups. * - * @returns {ManagedClientGroup[]>} + * @returns * An array of all active managed client groups. */ - service.getManagedClientGroups = function getManagedClientGroups() { - return storedManagedClientGroups(); - }; + getManagedClientGroups(): ManagedClientGroup[] { + return this.storedManagedClientGroups(); + } /** * Removes the ManagedClient with the given ID from all @@ -86,12 +92,12 @@ angular.module('client').factory('guacClientManager', ['$injector', * clients that remain in each group. All client groups that are empty * after the client is removed will also be removed. * - * @param {string} id + * @param id * The ID of the ManagedClient to remove. */ - const ungroupManagedClient = function ungroupManagedClient(id) { + private ungroupManagedClient(id: string): void { - const managedClientGroups = storedManagedClientGroups(); + const managedClientGroups: ManagedClientGroup[] = this.storedManagedClientGroups(); // Remove client from all groups managedClientGroups.forEach(group => { @@ -103,7 +109,9 @@ angular.module('client').factory('guacClientManager', ['$injector', // retained and result in a newly added connection unexpectedly // sharing focus) if (!group.attached) - removed.forEach(client => { client.clientProperties.focused = false; }); + removed.forEach(client => { + client.clientProperties.focused = false; + }); // Recalculate group grid if number of clients is changing ManagedClientGroup.recalculateTiles(group); @@ -114,28 +122,28 @@ angular.module('client').factory('guacClientManager', ['$injector', // Remove any groups that are now empty _.remove(managedClientGroups, group => !group.clients.length); - }; + } /** * Removes the existing ManagedClient associated with the connection having * the given ID, if any. If no such a ManagedClient already exists, this * function has no effect. * - * @param {String} id + * @param id * The ID of the connection whose ManagedClient should be removed. - * - * @returns {Boolean} + * + * @returns * true if an existing client was removed, false otherwise. */ - service.removeManagedClient = function removeManagedClient(id) { - - var managedClients = storedManagedClients(); + removeManagedClient(id: string): boolean { + + const managedClients: Record = this.storedManagedClients(); // Remove client if it exists if (id in managedClients) { // Pull client out of any containing groups - ungroupManagedClient(id); + this.ungroupManagedClient(id); // Disconnect and remove managedClients[id].client.disconnect(); @@ -149,24 +157,24 @@ angular.module('client').factory('guacClientManager', ['$injector', // No client was removed return false; - }; + } /** * Creates a new ManagedClient associated with the connection having the * given ID. If such a ManagedClient already exists, it is disconnected and * replaced. * - * @param {String} id + * @param id * The ID of the connection whose ManagedClient should be retrieved. - * - * @returns {ManagedClient} + * + * @returns * The ManagedClient associated with the connection having the given * ID. */ - service.replaceManagedClient = function replaceManagedClient(id) { + replaceManagedClient(id: string): ManagedClient { - const managedClients = storedManagedClients(); - const managedClientGroups = storedManagedClientGroups(); + const managedClients: Record = this.storedManagedClients(); + const managedClientGroups: ManagedClientGroup[] = this.storedManagedClientGroups(); // Remove client if it exists if (id in managedClients) { @@ -174,7 +182,7 @@ angular.module('client').factory('guacClientManager', ['$injector', const hadFocus = managedClients[id].clientProperties.focused; managedClients[id].client.disconnect(); delete managedClients[id]; - + // Remove client from all groups managedClientGroups.forEach(group => { @@ -182,7 +190,7 @@ angular.module('client').factory('guacClientManager', ['$injector', if (index === -1) return; - group.clients[index] = managedClients[id] = ManagedClient.getInstance(id); + group.clients[index] = managedClients[id] = this.managedClientService.getInstance(id); managedClients[id].clientProperties.focused = hadFocus; }); @@ -191,51 +199,51 @@ angular.module('client').factory('guacClientManager', ['$injector', return managedClients[id]; - }; + } /** * Returns the ManagedClient associated with the connection having the * given ID. If no such ManagedClient exists, a new ManagedClient is * created. * - * @param {String} id + * @param id * The ID of the connection whose ManagedClient should be retrieved. - * - * @returns {ManagedClient} + * + * @returns * The ManagedClient associated with the connection having the given * ID. */ - service.getManagedClient = function getManagedClient(id) { + getManagedClient(id: string): ManagedClient { - var managedClients = storedManagedClients(); + const managedClients = this.storedManagedClients(); // Ensure any existing client is removed from its containing group // prior to being returned - ungroupManagedClient(id); + this.ungroupManagedClient(id); // Create new managed client if it doesn't already exist if (!(id in managedClients)) - managedClients[id] = ManagedClient.getInstance(id); + managedClients[id] = this.managedClientService.getInstance(id); // Return existing client return managedClients[id]; - }; + } /** * Returns the ManagedClientGroup having the given ID. If no such * ManagedClientGroup exists, a new ManagedClientGroup is created by * extracting the relevant connections from the ID. * - * @param {String} id + * @param id * The ID of the ManagedClientGroup to retrieve or create. * - * @returns {ManagedClientGroup} + * @returns * The ManagedClientGroup having the given ID. */ - service.getManagedClientGroup = function getManagedClientGroup(id) { + getManagedClientGroup(id: string): ManagedClientGroup { - const managedClientGroups = storedManagedClientGroups(); + const managedClientGroups: ManagedClientGroup[] = this.storedManagedClientGroups(); const existingGroup = _.find(managedClientGroups, (group) => { return id === ManagedClientGroup.getIdentifier(group); }); @@ -244,17 +252,17 @@ angular.module('client').factory('guacClientManager', ['$injector', if (existingGroup) return existingGroup; - const clients = []; + const clients: ManagedClient[] = []; const clientIds = ManagedClientGroup.getClientIdentifiers(id); // Separate active clients by whether they should be displayed within // the current view - clientIds.forEach(function groupClients(id) { - clients.push(service.getManagedClient(id)); + clientIds.forEach(id => { + clients.push(this.getManagedClient(id)); }); const group = new ManagedClientGroup({ - clients : clients + clients: clients }); // Focus the first client if there are no clients focused @@ -263,7 +271,7 @@ angular.module('client').factory('guacClientManager', ['$injector', managedClientGroups.push(group); return group; - }; + } /** * Removes the existing ManagedClientGroup having the given ID, if any, @@ -271,16 +279,16 @@ angular.module('client').factory('guacClientManager', ['$injector', * group. If no such a ManagedClientGroup currently exists, this function * has no effect. * - * @param {String} id + * @param id * The ID of the ManagedClientGroup to remove. - * - * @returns {Boolean} + * + * @returns * true if a ManagedClientGroup was removed, false otherwise. */ - service.removeManagedClientGroup = function removeManagedClientGroup(id) { + removeManagedClientGroup(id: string): boolean { - const managedClients = storedManagedClients(); - const managedClientGroups = storedManagedClientGroups(); + const managedClients: Record = this.storedManagedClients(); + const managedClientGroups: ManagedClientGroup[] = this.storedManagedClientGroups(); // Remove all matching groups (there SHOULD only be one) const removed = _.remove(managedClientGroups, (group) => ManagedClientGroup.getIdentifier(group) === id); @@ -300,29 +308,24 @@ angular.module('client').factory('guacClientManager', ['$injector', return !!removed.length; - }; + } /** * Disconnects and removes all currently-connected clients and client * groups. */ - service.clear = function clear() { + clear(): void { - var managedClients = storedManagedClients(); + const managedClients: Record = this.storedManagedClients(); // Disconnect each managed client - for (var id in managedClients) + for (const id in managedClients) managedClients[id].client.disconnect(); // Clear managed clients and client groups - storedManagedClients({}); - storedManagedClientGroups([]); - - }; - - // Disconnect all clients when window is unloaded - $window.addEventListener('unload', service.clear); + this.storedManagedClients({}); + this.storedManagedClientGroups([]); - return service; + } -}]); +} diff --git a/guacamole/src/main/frontend/src/app/client/services/guacFullscreen.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-fullscreen.service.ts similarity index 63% rename from guacamole/src/main/frontend/src/app/client/services/guacFullscreen.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-fullscreen.service.ts index 12720e936d..7b63c6353b 100644 --- a/guacamole/src/main/frontend/src/app/client/services/guacFullscreen.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-fullscreen.service.ts @@ -17,39 +17,45 @@ * under the License. */ +import { Injectable } from '@angular/core'; + /** * A service for providing true fullscreen and keyboard lock support. * Keyboard lock is currently only supported by Chromium based browsers * (Edge >= V79, Chrome >= V68 and Opera >= V55) */ -angular.module('client').factory('guacFullscreen', ['$injector', - function guacFullscreen($injector) { - - var service = {}; +@Injectable({ + providedIn: 'root' +}) +export class GuacFullscreenService { - // check is browser in true fullscreen mode - service.isInFullscreenMode = function isInFullscreenMode() { + /** + * Check is browser in true fullscreen mode + */ + isInFullscreenMode(): Element | null { return document.fullscreenElement; } - // set fullscreen mode - service.setFullscreenMode = function setFullscreenMode(state) { + /** + * Set fullscreen mode + */ + setFullscreenMode(state: boolean): void { if (document.fullscreenEnabled) { - if (state && !service.isInFullscreenMode()) - document.documentElement.requestFullscreen().then(navigator.keyboard.lock()); - else if (!state && service.isInFullscreenMode()) - document.exitFullscreen().then(navigator.keyboard.unlock()); + if (state && !this.isInFullscreenMode()) + // @ts-ignore navigator.keyboard limited availability + document.documentElement.requestFullscreen().then(navigator.keyboard.lock()); + else if (!state && this.isInFullscreenMode()) + // @ts-ignore navigator.keyboard limited availability + document.exitFullscreen().then(navigator.keyboard.unlock()); } } // toggles current fullscreen mode (off if on, on if off) - service.toggleFullscreenMode = function toggleFullscreenMode() { - if (!service.isInFullscreenMode()) - service.setFullscreenMode(true); + toggleFullscreenMode(): void { + if (!this.isInFullscreenMode()) + this.setFullscreenMode(true); else - service.setFullscreenMode(false); + this.setFullscreenMode(false); } - return service; - -}]); \ No newline at end of file +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.spec.ts new file mode 100644 index 0000000000..105405b953 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.spec.ts @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { TRANSLOCO_MISSING_HANDLER, TranslocoService, TranslocoTestingModule } from '@ngneat/transloco'; +import { TestScheduler } from 'rxjs/testing'; +import { GuacMissingHandler } from '../../locale/locale.module'; +import { getTestScheduler } from '../../util/test-helper'; +import { TranslationResult } from '../types/TranslationResult'; +import { GuacTranslateService } from './guac-translate.service'; + + +describe('GuacTranslateService', () => { + let service: GuacTranslateService; + let transloco: TranslocoService; + let testScheduler: TestScheduler; + + beforeEach(() => { + testScheduler = getTestScheduler(); + TestBed.configureTestingModule({ + imports : [ + TranslocoTestingModule.forRoot({ + langs : {}, + translocoConfig: { + availableLangs: ['en'], + defaultLang : 'en', + + }, + preloadLangs : true, + }) + ], + providers: [ + { + provide : TRANSLOCO_MISSING_HANDLER, + useClass: GuacMissingHandler + } + ] + }); + service = TestBed.inject(GuacTranslateService); + }); + + beforeEach(() => { + transloco = TestBed.inject(TranslocoService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should return requested translation if available', () => { + testScheduler.run(({ expectObservable }) => { + + const translationId = 'ID'; + const translationMessage = 'message'; + const defaultTranslationId = 'DEFAULT_ID'; + + transloco.setTranslation({ [translationId]: translationMessage }); + + const result = service.translateWithFallback(translationId, defaultTranslationId); + + const expected = new TranslationResult({ + id : translationId, + message: translationMessage + }); + + expectObservable(result).toBe('(a|)', { a: expected }); + }); + }); + + + it('should return translation for default id if available but requested id is not', () => { + testScheduler.run(({ expectObservable }) => { + + const translationId = 'ID'; + const defaultTranslationId = 'DEFAULT_ID'; + const defaultTranslationMessage = 'default message'; + + transloco.setTranslation({ [defaultTranslationId]: defaultTranslationMessage }); + + const result = service.translateWithFallback(translationId, defaultTranslationId); + + const expected = new TranslationResult({ + id : defaultTranslationId, + message: defaultTranslationMessage + }); + + expectObservable(result).toBe('(a|)', { a: expected }); + }); + }); + + it('should return the literal value of `defaultTranslationId` for both the ID and message if neither id could be translated', () => { + testScheduler.run(({ expectObservable }) => { + + const translationId = 'ID'; + const defaultTranslationId = 'DEFAULT_ID'; + + const result = service.translateWithFallback(translationId, defaultTranslationId); + + const expected = new TranslationResult({ + id : defaultTranslationId, + message: defaultTranslationId + }); + + expectObservable(result).toBe('(a|)', { a: expected }); + }); + }); + + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.ts new file mode 100644 index 0000000000..a3cb0a6768 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/guac-translate.service.ts @@ -0,0 +1,98 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { HashMap } from '@ngneat/transloco/lib/types'; +import { Observable, of, switchMap, take } from 'rxjs'; +import { translationValueIfMissing } from '../../locale/locale.module'; +import { TranslationResult } from '../types/TranslationResult'; + +/** + * A wrapper around the angular-translate $translate service that offers a + * convenient way to fall back to a default translation if the requested + * translation is not available. + */ +@Injectable({ + providedIn: 'root' +}) +export class GuacTranslateService { + + /** + * Inject required services. + */ + constructor(private translocoService: TranslocoService) { + } + + /** + * Returns an observable that will emit a TranslationResult containing either the + * requested ID and message (if translated), or the default ID and message if translated, + * or the literal value of `defaultTranslationId` for both the ID and message if neither + * is translated. + * + * @param translationId + * The requested translation ID, which may or may not be translated. + * + * @param defaultTranslationId + * The translation ID that will be used if no translation is found for `translationId`. + * + * @returns + * An observable which emits a TranslationResult containing the results from + * the translation attempt. + */ + translateWithFallback(translationId: string, defaultTranslationId: string): Observable { + + // Use null as a fallback value to detect missing translations more easily + const params: HashMap = translationValueIfMissing(null); + + // Attempt to translate the requested translation ID + return this.translocoService.selectTranslate(translationId, params) + .pipe( + take(1), + switchMap((translation) => { + + // If the requested translation is available, use that + if (translation !== null) + return of(new TranslationResult({ id: translationId, message: translation })); + + // Otherwise, try the default translation ID + return this.translocoService.selectTranslate(defaultTranslationId, params) + .pipe( + take(1), + switchMap((defaultTranslation) => { + + // Default translation worked, so use that + if (defaultTranslation !== null) + return of(new TranslationResult({ + id : defaultTranslationId, + message: defaultTranslation + })); + + // Neither translation is available; as a fallback, return default ID for both + return of(new TranslationResult({ + id : defaultTranslationId, + message: defaultTranslationId + })); + }) + ); + }) + ); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-client.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-client.service.ts new file mode 100644 index 0000000000..e9b7922ca5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-client.service.ts @@ -0,0 +1,905 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; + +import { GuacAudioService, GuacImageService, GuacVideoService, } from 'guacamole-frontend-lib'; +import _ from 'lodash'; +import { catchError } from 'rxjs'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { ClipboardService } from '../../clipboard/services/clipboard.service'; +import { ClipboardData } from '../../clipboard/types/ClipboardData'; +import { GuacHistoryService } from '../../history/guac-history.service'; +import { ClientIdentifierService } from '../../navigation/service/client-identifier.service'; +import { ClientIdentifier } from '../../navigation/types/ClientIdentifier'; +import { ActiveConnectionService } from '../../rest/service/active-connection.service'; +import { ConnectionGroupService } from '../../rest/service/connection-group.service'; +import { ConnectionService } from '../../rest/service/connection.service'; +import { RequestService } from '../../rest/service/request.service'; +import { TunnelService } from '../../rest/service/tunnel.service'; +import { SharingProfile } from '../../rest/types/SharingProfile'; +import { PreferenceService } from '../../settings/services/preference.service'; +import { NOOP } from '../../util/noop'; +import { ManagedArgument } from '../types/ManagedArgument'; +import { DeferredPipeStream, ManagedClient, PipeStreamHandler } from '../types/ManagedClient'; +import { ManagedClientState } from '../types/ManagedClientState'; +import { ManagedClientThumbnail } from '../types/ManagedClientThumbnail'; +import { ManagedDisplay } from '../types/ManagedDisplay'; +import { ManagedFilesystem } from '../types/ManagedFilesystem'; +import { ManagedShareLink } from '../types/ManagedShareLink'; +import { ManagedFileUploadService } from './managed-file-upload.service'; +import { ManagedFilesystemService } from './managed-filesystem.service'; + + +/** + * A service for working with ManagedClient objects. + */ +@Injectable({ + providedIn: 'root' +}) +export class ManagedClientService { + + /** + * The minimum amount of time to wait between updates to the client + * thumbnail, in milliseconds. + */ + readonly THUMBNAIL_UPDATE_FREQUENCY: number = 5000; + + constructor( + @Inject(DOCUMENT) private document: Document, + private authenticationService: AuthenticationService, + private preferenceService: PreferenceService, + private tunnelService: TunnelService, + private requestService: RequestService, + private clipboardService: ClipboardService, + private managedFilesystemService: ManagedFilesystemService, + private managedFileUploadService: ManagedFileUploadService, + private connectionService: ConnectionService, + private connectionGroupService: ConnectionGroupService, + private activeConnectionService: ActiveConnectionService, + private guacHistory: GuacHistoryService, + private guacAudio: GuacAudioService, + private guacVideo: GuacVideoService, + private guacImage: GuacImageService, + private clientIdentifierService: ClientIdentifierService + ) { + } + + /** + * Returns a promise which resolves with the string of connection + * parameters to be passed to the Guacamole client during connection. This + * string generally contains the desired connection ID, display resolution, + * and supported audio/video/image formats. The returned promise is + * guaranteed to resolve successfully. + * + * @param identifier + * The identifier representing the connection or group to connect to. + * + * @param width + * The optimal display width, in local CSS pixels. + * + * @param height + * The optimal display height, in local CSS pixels. + * + * @returns + * A promise which resolves with the string of connection parameters to + * be passed to the Guacamole client, once the string is ready. + */ + private getConnectString(identifier: ClientIdentifier, width: number, height: number): Promise { + + return new Promise((resolve) => { + + // Calculate optimal width/height for display + const pixel_density = window.devicePixelRatio || 1; + const optimal_dpi = pixel_density * 96; + const optimal_width = width * pixel_density; + const optimal_height = height * pixel_density; + + // Build base connect string + let connectString = + 'token=' + encodeURIComponent(this.authenticationService.getCurrentToken()!) + + '&GUAC_DATA_SOURCE=' + encodeURIComponent(identifier.dataSource) + + '&GUAC_ID=' + encodeURIComponent(identifier.id!) + + '&GUAC_TYPE=' + encodeURIComponent(identifier.type) + + '&GUAC_WIDTH=' + Math.floor(optimal_width) + + '&GUAC_HEIGHT=' + Math.floor(optimal_height) + + '&GUAC_DPI=' + Math.floor(optimal_dpi) + + '&GUAC_TIMEZONE=' + encodeURIComponent(this.preferenceService.preferences.timezone); + + // Add audio mimetypes to connect string + this.guacAudio.supported.forEach(function (mimetype) { + connectString += '&GUAC_AUDIO=' + encodeURIComponent(mimetype); + }); + + // Add video mimetypes to connect string + this.guacVideo.supported.forEach(function (mimetype) { + connectString += '&GUAC_VIDEO=' + encodeURIComponent(mimetype); + }); + + // Add image mimetypes to connect string + this.guacImage.getSupportedMimetypes().then(function supportedMimetypesKnown(mimetypes) { + + // Add each image mimetype + mimetypes.forEach(mimetype => { + connectString += '&GUAC_IMAGE=' + encodeURIComponent(mimetype); + }); + + // Connect string is now ready - nothing else is deferred + resolve(connectString); + + }); + }); + + } + + /** + * Requests the creation of a new audio stream, recorded from the user's + * local audio input device. If audio input is supported by the connection, + * an audio stream will be created which will remain open until the remote + * desktop requests that it be closed. If the audio stream is successfully + * created but is later closed, a new audio stream will automatically be + * established to take its place. The mimetype used for all audio streams + * produced by this function is defined by + * ManagedClient.AUDIO_INPUT_MIMETYPE. + * + * @param client + * The Guacamole.Client for which the audio stream is being requested. + */ + private requestAudioStream(client: Guacamole.Client): void { + + // Create new audio stream, associating it with an AudioRecorder + const stream = client.createAudioStream(ManagedClient.AUDIO_INPUT_MIMETYPE); + const recorder = Guacamole.AudioRecorder.getInstance(stream, ManagedClient.AUDIO_INPUT_MIMETYPE); + + // If creation of the AudioRecorder failed, simply end the stream + if (!recorder) + stream.sendEnd(); + + // Otherwise, ensure that another audio stream is created after this + // audio stream is closed + else + recorder.onclose = this.requestAudioStream.bind(this, client); + + } + + /** + * Creates a new ManagedClient representing the specified connection or + * connection group. The ManagedClient will not initially be connected, + * and must be explicitly connected by invoking ManagedClient.connect(). + * + * @param id + * The ID of the connection or group to connect to. This String must be + * a valid ClientIdentifier string, as would be generated by + * ClientIdentifierService.toString(). + * + * @returns + * A new ManagedClient instance which represents the connection or + * connection group having the given ID. + */ + getInstance(id: string): ManagedClient { + + let tunnel: Guacamole.Tunnel; + + // If WebSocket available, try to use it. + if ('WebSocket' in window) + tunnel = new Guacamole.ChainedTunnel( + new Guacamole.WebSocketTunnel('websocket-tunnel'), + new Guacamole.HTTPTunnel('tunnel') + ); + + // If no WebSocket, then use HTTP. + else + tunnel = new Guacamole.HTTPTunnel('tunnel'); + + // Get new client instance + const client: Guacamole.Client = new Guacamole.Client(tunnel); + + // Associate new managed client with new client and tunnel + const managedClient: ManagedClient = new ManagedClient({ + id : id, + client: client, + tunnel: tunnel + }); + + // Fire events for tunnel errors + tunnel.onerror = function tunnelError(status: Guacamole.Status) { + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.TUNNEL_ERROR, + status.code); + }; + + // Pull protocol-specific information from tunnel once tunnel UUID is + // known + tunnel.onuuid = (uuid: string) => { + this.tunnelService.getProtocol(uuid).subscribe({ + next : protocol => { + managedClient.protocol = protocol.name !== undefined ? protocol.name : null; + managedClient.forms = protocol.connectionForms; + }, error: this.requestService.WARN + }); + }; + + // Update connection state as tunnel state changes + tunnel.onstatechange = (state: number) => { + + switch (state) { + + // Connection is being established + case Guacamole.Tunnel.State.CONNECTING: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CONNECTING); + break; + + // Connection is established / no longer unstable + case Guacamole.Tunnel.State.OPEN: + ManagedClientState.setTunnelUnstable(managedClient.clientState, false); + break; + + // Connection is established but misbehaving + case Guacamole.Tunnel.State.UNSTABLE: + ManagedClientState.setTunnelUnstable(managedClient.clientState, true); + break; + + // Connection has closed + case Guacamole.Tunnel.State.CLOSED: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.DISCONNECTED); + break; + + } + + }; + + // Update connection state as client state changes + client.onstatechange = (clientState: number) => { + + switch (clientState) { + + // Idle + case Guacamole.Client.State.IDLE: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.IDLE); + break; + + // Connecting + case Guacamole.Client.State.CONNECTING: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CONNECTING); + break; + + // Connected + waiting + case Guacamole.Client.State.WAITING: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.WAITING); + break; + + // Connected + case Guacamole.Client.State.CONNECTED: + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CONNECTED); + + // Sync current clipboard data + this.clipboardService.getClipboard().then((data) => { + this.setClipboard(managedClient, data); + }); + + // Begin streaming audio input if possible + this.requestAudioStream(client); + + // Update thumbnail with initial display contents + this.updateThumbnail(managedClient); + break; + + // Update history during disconnect phases + case Guacamole.Client.State.DISCONNECTING: + case Guacamole.Client.State.DISCONNECTED: + this.updateThumbnail(managedClient); + break; + + } + + }; + + // Disconnect and update status when the client receives an error + client.onerror = (status: Guacamole.Status) => { + + // Disconnect, if connected + client.disconnect(); + + // Update state + ManagedClientState.setConnectionState(managedClient.clientState, + ManagedClientState.ConnectionState.CLIENT_ERROR, + status.code); + + }; + + // Update user count when a new user joins + client.onjoin = (id: string, username: string) => { + + const connections = managedClient.users[username] || {}; + managedClient.users[username] = connections; + + managedClient.userCount++; + connections[id] = true; + + }; + + // Update user count when a user leaves + client.onleave = function userLeft(id: string, username: string) { + + const connections = managedClient.users[username] || {}; + managedClient.users[username] = connections; + + managedClient.userCount--; + delete connections[id]; + + // Delete user entry after no connections remain + if (_.isEmpty(connections)) + delete managedClient.users[username]; + + }; + + // Automatically update the client thumbnail + client.onsync = () => { + + const thumbnail: ManagedClientThumbnail | null = managedClient.thumbnail; + const timestamp: number = new Date().getTime(); + + // Update thumbnail if it doesn't exist or is old + if (!thumbnail || timestamp - thumbnail.timestamp >= this.THUMBNAIL_UPDATE_FREQUENCY) { + this.updateThumbnail(managedClient); + } + + }; + + // A default onpipe implementation that will automatically defer any + // received pipe streams, automatically invoking any registered handlers + // that may already be set for the received name + client.onpipe = (stream, mimetype, name) => { + + // Defer the pipe stream + managedClient.deferredPipeStreams[name] = new DeferredPipeStream( + { stream, mimetype, name }); + + // Invoke the handler now, if set + const handler = managedClient.deferredPipeStreamHandlers[name]; + if (handler) { + + // Handle the stream, and clear from the deferred streams + handler(stream, mimetype, name); + delete managedClient.deferredPipeStreams[name]; + } + }; + + // Test for argument mutability whenever an argument value is + // received + client.onargv = (stream: Guacamole.InputStream, mimetype: string, name: string) => { + + // Ignore arguments which do not use a mimetype currently supported + // by the web application + if (mimetype !== 'text/plain') + return; + + const reader = new Guacamole.StringReader(stream); + + // Assemble received data into a single string + let value = ''; + reader.ontext = text => { + value += text; + }; + + // Test mutability once stream is finished, storing the current + // value for the argument only if it is mutable + reader.onend = () => { + ManagedArgument.getInstance(managedClient, name, value).then(argument => { + managedClient.arguments[name] = argument; + }, function ignoreImmutableArguments() { + }); + }; + + }; + + // Handle any received clipboard data + client.onclipboard = (stream: Guacamole.InputStream, mimetype: string) => { + + let reader: Guacamole.StringReader | Guacamole.BlobReader; + + // If the received data is text, read it as a simple string + if (/^text\//.exec(mimetype)) { + + reader = new Guacamole.StringReader(stream); + + // Assemble received data into a single string + let data = ''; + reader.ontext = text => { + data += text; + }; + + // Set clipboard contents once stream is finished + reader.onend = () => { + this.clipboardService.setClipboard(new ClipboardData({ + source: managedClient.id, + type : mimetype, + data : data + })).catch(NOOP); + }; + + } + + // Otherwise read the clipboard data as a Blob + else { + reader = new Guacamole.BlobReader(stream, mimetype); + reader.onend = () => { + this.clipboardService.setClipboard(new ClipboardData({ + source: managedClient.id, + type : mimetype, + data : (reader as Guacamole.BlobReader).getBlob() + })).catch(NOOP); + }; + } + + }; + + // Update level of multi-touch support when known + client.onmultitouch = (layer: Guacamole.Display.VisibleLayer, touches: number) => { + managedClient.multiTouchSupport = touches; + }; + + // Update title when a "name" instruction is received + client.onname = name => { + managedClient.title = name; + }; + + // Handle any received files + client.onfile = (stream: Guacamole.InputStream, mimetype: string, filename: string) => { + this.tunnelService.downloadStream(tunnel.uuid!, stream, mimetype, filename); + }; + + // Handle any received filesystem objects + client.onfilesystem = (object: Guacamole.Object, name: string) => { + managedClient.filesystems.push(this.managedFilesystemService.getInstance(managedClient, object, name)); + }; + + // Handle any received prompts + client.onrequired = (parameters: string[]) => { + managedClient.requiredParameters = {}; + parameters.forEach(name => { + managedClient.requiredParameters![name] = ''; + }); + }; + + // Manage the client display + managedClient.managedDisplay = ManagedDisplay.getInstance(client.getDisplay()); + + // Parse connection details from ID + const clientIdentifier = this.clientIdentifierService.fromString(id); + + // Defer actually connecting the Guacamole client until + // ManagedClient.connect() is explicitly invoked + + // If using a connection, pull connection name and protocol information + if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION) { + this.connectionService.getConnection(clientIdentifier.dataSource, clientIdentifier.id!) + .subscribe({ + next : connection => { + managedClient.name = managedClient.title = connection.name; + }, error: this.requestService.WARN + }); + } + + // If using a connection group, pull connection name + else if (clientIdentifier.type === ClientIdentifier.Types.CONNECTION_GROUP) { + this.connectionGroupService.getConnectionGroup(clientIdentifier.dataSource, clientIdentifier.id) + .subscribe({ + next : group => { + managedClient.name = managedClient.title = group.name; + }, error: this.requestService.WARN + }); + } + + // If using an active connection, pull corresponding connection, then + // pull connection name and protocol information from that + else if (clientIdentifier.type === ClientIdentifier.Types.ACTIVE_CONNECTION) { + this.activeConnectionService.getActiveConnection(clientIdentifier.dataSource, clientIdentifier.id!) + .subscribe({ + next : activeConnection => { + + // Attempt to retrieve connection details only if the + // underlying connection is known + if (activeConnection.connectionIdentifier) { + this.connectionService.getConnection(clientIdentifier.dataSource, activeConnection.connectionIdentifier) + .subscribe({ + next : connection => { + managedClient.name = managedClient.title = connection.name; + }, error: this.requestService.WARN + }); + } + + }, error: this.requestService.WARN + }); + } + + return managedClient; + + } + + /** + * Connects the given ManagedClient instance to its associated connection + * or connection group. If the ManagedClient has already been connected, + * including if connected but subsequently disconnected, this function has + * no effect. + * + * @param managedClient + * The ManagedClient to connect. + * + * @param width + * The optimal display width, in local CSS pixels. If omitted, the + * browser window width will be used. + * + * @param height + * The optimal display height, in local CSS pixels. If omitted, the + * browser window height will be used. + */ + connect(managedClient: ManagedClient, width: number, height: number): void { + + // Ignore if already connected + if (managedClient.clientState.connectionState !== ManagedClientState.ConnectionState.IDLE) + return; + + // Parse connection details from ID + const clientIdentifier = this.clientIdentifierService.fromString(managedClient.id); + + // Connect the Guacamole client + this.getConnectString(clientIdentifier, width, height) + .then(function connectClient(connectString) { + managedClient.client.connect(connectString); + }); + + } + + /** + * Uploads the given file to the server through the given Guacamole client. + * The file transfer can be monitored through the corresponding entry in + * the uploads array of the given managedClient. + * + * @param managedClient + * The ManagedClient through which the file is to be uploaded. + * + * @param file + * The file to upload. + * + * @param filesystem + * The filesystem to upload the file to, if any. If not specified, the + * file will be sent as a generic Guacamole file stream. + * + * @param [directory=filesystem.currentDirectory] + * The directory within the given filesystem to upload the file to. If + * not specified, but a filesystem is given, the current directory of + * that filesystem will be used. + */ + uploadFile(managedClient: ManagedClient, file: File, filesystem?: ManagedFilesystem, directory?: ManagedFilesystem.File): void { + + // Use generic Guacamole file streams by default + let object = null; + let streamName = null; + + // If a filesystem is given, determine the destination object and stream + if (filesystem) { + object = filesystem.object; + streamName = (directory || filesystem.currentDirectory()).streamName + '/' + file.name; + } + + // Start and manage file upload + managedClient.uploads.push(this.managedFileUploadService.getInstance(managedClient, file, object, streamName)); + + } + + /** + * Sends the given clipboard data over the given Guacamole client, setting + * the contents of the remote clipboard to the data provided. If the given + * clipboard data was originally received from that client, the data is + * ignored and this function has no effect. + * + * @param managedClient + * The ManagedClient over which the given clipboard data is to be sent. + * + * @param data + * The clipboard data to send. + */ + setClipboard(managedClient: ManagedClient, data: ClipboardData): void { + + // Ignore clipboard data that was received from this connection + if (data.source === managedClient.id) + return; + + let writer: Guacamole.StringWriter | Guacamole.BlobWriter; + + // Create stream with proper mimetype + const stream = managedClient.client.createClipboardStream(data.type); + + // Send data as a string if it is stored as a string + if (typeof data.data === 'string') { + writer = new Guacamole.StringWriter(stream); + writer.sendText(data.data); + writer.sendEnd(); + } + + // Otherwise, assume the data is a File/Blob + else { + + // Write File/Blob asynchronously + writer = new Guacamole.BlobWriter(stream); + writer.oncomplete = function clipboardSent() { + writer.sendEnd(); + }; + + // Begin sending data + writer.sendBlob(data.data); + + } + + } + + /** + * Assigns the given value to the connection parameter having the given + * name, updating the behavior of the connection in real-time. If the + * connection parameter is not editable, this function has no effect. + * + * @param managedClient + * The ManagedClient instance associated with the active connection + * being modified. + * + * @param name + * The name of the connection parameter to modify. + * + * @param value + * The value to attempt to assign to the given connection parameter. + */ + setArgument(managedClient: ManagedClient, name: string, value: string): void { + const managedArgument = managedClient.arguments[name]; + managedArgument && ManagedArgument.setValue(managedArgument, value); + } + + /** + * Sends the given connection parameter values using "argv" streams, + * updating the behavior of the connection in real-time if the server is + * expecting or requiring these parameters. + * + * @param managedClient + * The ManagedClient instance associated with the active connection + * being modified. + * + * @param values + * The set of values to attempt to assign to corresponding connection + * parameters, where each object key is the connection parameter being + * set. + */ + sendArguments(managedClient: ManagedClient, values: Record | null): void { + for (const name in values) { + const value = values[name]; + + const stream = managedClient.client.createArgumentValueStream('text/plain', name); + const writer = new Guacamole.StringWriter(stream); + writer.sendText(value); + writer.sendEnd(); + } + } + + /** + * Retrieves the current values of all editable connection parameters as a + * set of name/value pairs suitable for use as the model of a form which + * edits those parameters. + * + * @param client + * The ManagedClient instance associated with the active connection + * whose parameter values are being retrieved. + * + * @returns + * A new set of name/value pairs containing the current values of all + * editable parameters. + */ + getArgumentModel(client: ManagedClient): Record { + + const model: Record = {}; + + for (const argumentName in client.arguments) { + const managedArgument = client.arguments[argumentName]; + + model[managedArgument.name] = managedArgument.value; + } + + return model; + + } + + /** + * Produces a sharing link for the given ManagedClient using the given + * sharing profile. The resulting sharing link, and any required login + * information, can be retrieved from the shareLinks property + * of the given ManagedClient once the various underlying service calls + * succeed. + * + * @param client + * The ManagedClient which will be shared via the generated sharing + * link. + * + * @param sharingProfile + * The sharing profile to use to generate the sharing link. + */ + createShareLink(client: ManagedClient, sharingProfile: SharingProfile): void { + + // Retrieve sharing credentials for the sake of generating a share link + this.tunnelService.getSharingCredentials(client.tunnel.uuid!, sharingProfile.identifier!) + .pipe(catchError(this.requestService.WARN)) + // Add a new share link once the credentials are ready + .subscribe(sharingCredentials => { + client.shareLinks[sharingProfile.identifier!] = + ManagedShareLink.getInstance(sharingProfile, sharingCredentials); + }); + + } + + /** + * Returns whether the given ManagedClient is being shared. A ManagedClient + * is shared if it has any associated share links. + * + * @param client + * The ManagedClient to check. + * + * @returns + * true if the ManagedClient has at least one associated share link, + * false otherwise. + */ + isShared(client: ManagedClient): boolean { + + // The connection is shared if at least one share link exists + for (const dummy in client.shareLinks) + return true; + + // No share links currently exist + return false; + + } + + /** + * Returns whether the given client has any associated file transfers, + * regardless of those file transfers' state. + * + * @param client + * The client for which file transfers should be checked. + * + * @returns + * true if there are any file transfers associated with the + * given client, false otherwise. + */ + hasTransfers(client: ManagedClient): boolean { + return !!(client && client.uploads && client.uploads.length); + } + + /** + * Store the thumbnail of the given managed client within the connection + * history under its associated ID. If the client is not connected, this + * function has no effect. + * + * @param managedClient + * The client whose history entry should be updated. + */ + updateThumbnail(managedClient: ManagedClient): void { + + const display = managedClient.client.getDisplay(); + + // Update stored thumbnail of previous connection + if (display && display.getWidth() > 0 && display.getHeight() > 0) { + + // Get screenshot + const canvas = display.flatten(); + + // Calculate scale of thumbnail (max 320x240, max zoom 100%) + const scale = Math.min(320 / canvas.width, 240 / canvas.height, 1); + + // Create thumbnail canvas + const thumbnail = this.document.createElement('canvas'); + thumbnail.width = canvas.width * scale; + thumbnail.height = canvas.height * scale; + + // Scale screenshot to thumbnail + const context = thumbnail.getContext('2d') as CanvasRenderingContext2D; + context.drawImage(canvas, + 0, 0, canvas.width, canvas.height, + 0, 0, thumbnail.width, thumbnail.height + ); + + // Store updated thumbnail within client + managedClient.thumbnail = new ManagedClientThumbnail({ + timestamp: new Date().getTime(), + canvas : thumbnail + }); + + // Update historical thumbnail + this.guacHistory.updateThumbnail(managedClient.id, thumbnail.toDataURL('image/png')); + + } + + } + + /** + * Register a handler that will be automatically invoked for any deferred + * pipe stream with the provided name, either when a pipe stream with a + * name matching a registered handler is received, or immediately when this + * function is called, if such a pipe stream has already been received. + * + * NOTE: Pipe streams are automatically deferred by the default onpipe + * implementation. To preserve this behavior when using a custom onpipe + * callback, make sure to defer to the default implementation as needed. + * + * @param managedClient + * The client for which the deferred pipe stream handler should be set. + * + * @param name + * The name of the pipe stream that should be handeled by the provided + * handler. If another handler is already registered for this name, it + * will be replaced by the handler provided to this function. + * + * @param handler + * The handler that should handle any deferred pipe stream with the + * provided name. This function must take the same arguments as the + * standard onpipe handler - namely, the stream itself, the mimetype, + * and the name. + */ + registerDeferredPipeHandler(managedClient: ManagedClient, name: string, handler: PipeStreamHandler): void { + managedClient.deferredPipeStreamHandlers[name] = handler; + + // Invoke the handler now, if the pipestream has already been received + if (managedClient.deferredPipeStreams[name]) { + + // Invoke the handler with the deferred pipe stream + const deferredStream = managedClient.deferredPipeStreams[name]; + handler(deferredStream.stream, + deferredStream.mimetype, + deferredStream.name); + + // Clean up the now-consumed pipe stream + delete managedClient.deferredPipeStreams[name]; + } + } + + /** + * Detach the provided deferred pipe stream handler, if it is currently + * registered for the provided pipe stream name. + * + * @param managedClient + * The client for which the deferred pipe stream handler should be + * detached. + * + * @param name + * The name of the associated pipe stream for the handler that should + * be detached. + * + * @param handler + * The handler that should be detached. + */ + detachDeferredPipeHandler(managedClient: ManagedClient, name: string, handler: PipeStreamHandler): void { + + // Remove the handler if found + if (managedClient.deferredPipeStreamHandlers[name] === handler) + delete managedClient.deferredPipeStreamHandlers[name]; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-file-upload.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-file-upload.service.ts new file mode 100644 index 0000000000..6e4c293b92 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-file-upload.service.ts @@ -0,0 +1,159 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { RequestService } from '../../rest/service/request.service'; +import { TunnelService } from '../../rest/service/tunnel.service'; +import { Error } from '../../rest/types/Error'; +import { ManagedClient } from '../types/ManagedClient'; +import { ManagedFileTransferState } from '../types/ManagedFileTransferState'; +import { ManagedFileUpload } from '../types/ManagedFileUpload'; + +/** + * A service for creating new ManagedFileUpload instances. + */ +@Injectable({ + providedIn: 'root' +}) +export class ManagedFileUploadService { + + /** + * Inject required services. + */ + constructor(private requestService: RequestService, + private tunnelService: TunnelService, + private guacEventService: GuacEventService) { + } + + /** + * Creates a new ManagedFileUpload which uploads the given file to the + * server through the given Guacamole client. + * + * @param managedClient + * The ManagedClient through which the file is to be uploaded. + * + * @param file + * The file to upload. + * + * @param object + * The object to upload the file to, if any, such as a filesystem + * object. + * + * @param streamName + * The name of the stream to upload the file to. If an object is given, + * this must be specified. + * + * @return + * A new ManagedFileUpload object which can be used to track the + * progress of the upload. + */ + getInstance(managedClient: ManagedClient, file: File, object?: any, streamName: string | null = null): ManagedFileUpload { + + const managedFileUpload = new ManagedFileUpload(); + + // Pull Guacamole.Tunnel and Guacamole.Client from given ManagedClient + const client = managedClient.client; + const tunnel = managedClient.tunnel; + + // Open file for writing + let stream: Guacamole.OutputStream; + if (!object) + stream = client.createFileStream(file.type, file.name); + + // If object/streamName specified, upload to that instead of a file + // stream + else + stream = object.createOutputStream(file.type, streamName); + + // Notify that the file transfer is pending + + // Init managed upload + managedFileUpload.filename = file.name; + managedFileUpload.mimetype = file.type; + managedFileUpload.progress = 0; + managedFileUpload.length = file.size; + + // Notify that stream is open + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.OPEN); + + + // Upload file once stream is acknowledged + stream.onack = (status: Guacamole.Status) => { + + // Notify of any errors from the Guacamole server + if (status.isError()) { + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.ERROR, status.code); + return; + } + + // Begin upload + this.tunnelService.uploadToStream(tunnel.uuid!, stream, file, length => { + managedFileUpload.progress = length; + }) + + // Notify if upload succeeds + .then(() => { + + // Upload complete + managedFileUpload.progress = file.size; + + // Close the stream + stream.sendEnd(); + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.CLOSED); + + // Notify of upload completion + this.guacEventService.broadcast('guacUploadComplete', { filename: file.name }); + }, + + // Notify if upload fails + this.requestService.createPromiseErrorCallback((error: any) => { + + // Use provide status code if the error is coming from the stream + if (error.type === Error.Type.STREAM_ERROR) + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.ERROR, + error.statusCode); + + // Fail with internal error for all other causes + else + ManagedFileTransferState.setStreamState(managedFileUpload.transferState, + ManagedFileTransferState.StreamState.ERROR, + Guacamole.Status.Code.SERVER_ERROR); // TODO: Guacamole.Status.Code.INTERNAL_ERROR does + // not exist + + // Close the stream + stream.sendEnd(); + + })); + + // Ignore all further acks + stream.onack = null; + + + }; + + return managedFileUpload; + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-filesystem.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-filesystem.service.ts new file mode 100644 index 0000000000..1c6e7a1c99 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/services/managed-filesystem.service.ts @@ -0,0 +1,215 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { TunnelService } from '../../rest/service/tunnel.service'; +import { ManagedClient } from '../types/ManagedClient'; +import { ManagedFilesystem } from '../types/ManagedFilesystem'; + +/** + * A service for working with ManagedFilesystem objects. + */ +@Injectable({ + providedIn: 'root' +}) +export class ManagedFilesystemService { + + /** + * Inject required services. + */ + constructor(private tunnelService: TunnelService) { + } + + /** + * Refreshes the contents of the given file, if that file is a directory. + * Only the immediate children of the file are refreshed. Files further + * down the directory tree are not refreshed. + * + * @param filesystem + * The filesystem associated with the file being refreshed. + * + * @param file + * The file being refreshed. + */ + refresh(filesystem: ManagedFilesystem, file: ManagedFilesystem.File): void { + + // @ts-ignore + // Do not attempt to refresh the contents of directories + if (file.mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) + return; + + // Request contents of given file + filesystem.object.requestInputStream(file.streamName, (stream: Guacamole.InputStream, mimetype: string) => { + + // @ts-ignore + // Ignore stream if mimetype is wrong + if (mimetype !== Guacamole.Object.STREAM_INDEX_MIMETYPE) { + stream.sendAck('Unexpected mimetype', Guacamole.Status.Code.UNSUPPORTED); + return; + } + + // Signal server that data is ready to be received + stream.sendAck('Ready', Guacamole.Status.Code.SUCCESS); + + // Read stream as JSON + const reader = new Guacamole.JSONReader(stream); + + // Acknowledge received JSON blobs + reader.onprogress = function onprogress() { + stream.sendAck('Received', Guacamole.Status.Code.SUCCESS); + }; + + // Reset contents of directory + reader.onend = function jsonReady() { + + // Empty contents + file.files.set({}); + + // Determine the expected filename prefix of each stream + let expectedPrefix = file.streamName; + if (expectedPrefix.charAt(expectedPrefix.length - 1) !== '/') + expectedPrefix += '/'; + + // For each received stream name + const mimetypes = reader.getJSON(); + for (const name in mimetypes) { + + // Assert prefix is correct + if (name.substring(0, expectedPrefix.length) !== expectedPrefix) + continue; + + // Extract filename from stream name + const filename = name.substring(expectedPrefix.length); + + // Deduce type from mimetype + let type = ManagedFilesystem.File.Type.NORMAL; + // @ts-ignore + if (mimetypes[name] === Guacamole.Object.STREAM_INDEX_MIMETYPE) + type = ManagedFilesystem.File.Type.DIRECTORY; + + // Add file entry + file.files.update(files => { + files[filename] = new ManagedFilesystem.File({ + // @ts-ignore + mimetype : mimetypes[name], + streamName: name, + type : type, + parent : file, + name : filename + }); + return files; + }); + + } + + }; + + }); + + } + + /** + * Creates a new ManagedFilesystem instance from the given Guacamole.Object + * and human-readable name. Upon creation, a request to populate the + * contents of the root directory will be automatically dispatched. + * + * @param client + * The client that originally received the "filesystem" instruction + * that resulted in the creation of this ManagedFilesystem. + * + * @param object + * The Guacamole.Object defining the filesystem. + * + * @param name + * A human-readable name for the filesystem. + * + * @returns + * The newly-created ManagedFilesystem. + */ + getInstance(client: ManagedClient, object: Guacamole.Object, name: string): ManagedFilesystem { + + // Init new filesystem object + const managedFilesystem = new ManagedFilesystem({ + client: client, + object: object, + name : name, + root : new ManagedFilesystem.File({ + // @ts-ignore + mimetype: Guacamole.Object.STREAM_INDEX_MIMETYPE, + // @ts-ignore + streamName: Guacamole.Object.ROOT_STREAM, + type : ManagedFilesystem.File.Type.DIRECTORY + }) + }); + + // Retrieve contents of root + this.refresh(managedFilesystem, managedFilesystem.root); + + return managedFilesystem; + + } + + /** + * Downloads the given file from the server using the given Guacamole + * client and filesystem. The browser will automatically start the + * download upon completion of this function. + * + * @param managedFilesystem + * The ManagedFilesystem from which the file is to be downloaded. Any + * path information provided must be relative to this filesystem. + * + * @param path + * The full, absolute path of the file to download. + */ + downloadFile(managedFilesystem: ManagedFilesystem, path: string): void { + + // Request download + managedFilesystem.object.requestInputStream(path, (stream: Guacamole.InputStream, mimetype: string) => { + + // Parse filename from string + const filename = path.match(/(.*[\\/])?(.*)/)![2]; + + // Start download + this.tunnelService.downloadStream(managedFilesystem.client.tunnel.uuid!, stream, mimetype, filename); + + }); + + } + + /** + * Changes the current directory of the given filesystem, automatically + * refreshing the contents of that directory. + * + * @param filesystem + * The filesystem whose current directory should be changed. + * + * @param file + * The directory to change to. + */ + changeDirectory(filesystem: ManagedFilesystem, file: ManagedFilesystem.File): void { + + // Refresh contents + this.refresh(filesystem, file); + + // Set current directory + filesystem.currentDirectory.set(file); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/client/styles/client.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/client.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/client.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/client.css index 4d452980b7..6a3ac270e9 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/client.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/client.css @@ -60,7 +60,7 @@ body.client { -moz-box-align: stretch; -moz-box-orient: vertical; -moz-box-pack: end; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; @@ -134,4 +134,4 @@ body.client { .client .drop-pending .display > *{ opacity: 0.5; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/client/styles/connection-select-menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/connection-select-menu.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/connection-select-menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/connection-select-menu.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/connection-warning.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/connection-warning.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/connection-warning.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/connection-warning.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/display.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/display.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/display.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/display.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/file-browser.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/file-browser.css similarity index 97% rename from guacamole/src/main/frontend/src/app/client/styles/file-browser.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/file-browser.css index bafaa0328e..0476c1fc18 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/file-browser.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/file-browser.css @@ -29,7 +29,7 @@ border: 1px solid transparent; } -.file-browser .list-item.focused .caption { +.file-browser guac-file.focused .caption { border: 1px dotted rgba(0, 0, 0, 0.5); background: rgba(204, 221, 170, 0.5); } diff --git a/guacamole/src/main/frontend/src/app/client/styles/file-transfer-dialog.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/file-transfer-dialog.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/file-transfer-dialog.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/file-transfer-dialog.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/filesystem-menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/filesystem-menu.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/filesystem-menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/filesystem-menu.css index aed541aee8..42f94ba3a7 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/filesystem-menu.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/filesystem-menu.css @@ -69,4 +69,4 @@ height: 2em; padding: 0; vertical-align: middle; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/client/styles/guac-menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/guac-menu.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/guac-menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/guac-menu.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/keyboard.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/keyboard.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/keyboard.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/keyboard.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/menu.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/menu.css index b5742ab5d0..0417598ad1 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/menu.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/menu.css @@ -45,7 +45,7 @@ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: vertical; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; @@ -60,7 +60,7 @@ display: flex; align-items: stretch; flex-direction: column; - + width: 100%; height: 100%; @@ -75,7 +75,7 @@ flex: 0 0 auto; margin-bottom: 0; - + } .menu-body { @@ -98,7 +98,7 @@ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: vertical; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; diff --git a/guacamole/src/main/frontend/src/app/client/styles/notification.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/notification.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/notification.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/notification.css index 86b2db3e74..9e0a1c332e 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/notification.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/notification.css @@ -91,5 +91,5 @@ .client-status-modal .notification .parameters textarea { background: transparent; border: 2px solid white; - color: white; + color: white; } diff --git a/guacamole/src/main/frontend/src/app/client/styles/share-menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/share-menu.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/share-menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/share-menu.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/thumbnail-display.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/thumbnail-display.css similarity index 94% rename from guacamole/src/main/frontend/src/app/client/styles/thumbnail-display.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/thumbnail-display.css index 468c51acae..15c64c7089 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/thumbnail-display.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/thumbnail-display.css @@ -17,7 +17,8 @@ * under the License. */ -div.thumbnail-main { +div.thumbnail-main, +guac-thumbnail.thumbnail-main { overflow: hidden; width: 100%; height: 100%; @@ -27,4 +28,4 @@ div.thumbnail-main { .thumbnail-main .display { pointer-events: none; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/client/styles/tiled-client-grid.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/tiled-client-grid.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/tiled-client-grid.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/tiled-client-grid.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/transfer-manager.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/transfer-manager.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/transfer-manager.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/transfer-manager.css diff --git a/guacamole/src/main/frontend/src/app/client/styles/transfer.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/transfer.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/transfer.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/transfer.css index 2c9c624d71..5bf625f921 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/transfer.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/transfer.css @@ -57,13 +57,13 @@ width: 100%; padding: 0.25em; - + position: absolute; top: 0; left: 0; bottom: 0; opacity: 0.25; - + } .transfer.in-progress .progress { diff --git a/guacamole/src/main/frontend/src/app/client/styles/viewport.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/viewport.css similarity index 99% rename from guacamole/src/main/frontend/src/app/client/styles/viewport.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/viewport.css index f1bf157c5c..892b01ed56 100644 --- a/guacamole/src/main/frontend/src/app/client/styles/viewport.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/viewport.css @@ -24,4 +24,4 @@ width: 100%; height: 100%; overflow: hidden; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/client/styles/zoom.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/zoom.css similarity index 100% rename from guacamole/src/main/frontend/src/app/client/styles/zoom.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/styles/zoom.css diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientMenu.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientMenu.ts new file mode 100644 index 0000000000..5ef25a8028 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientMenu.ts @@ -0,0 +1,56 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { WritableSignal } from '@angular/core'; +import { ScrollState } from 'guacamole-frontend-lib'; + +/** + * Properties of the client menu. + */ +export interface ClientMenu { + + /** + * Whether the menu is currently shown. + */ + shown: WritableSignal; + + /** + * The currently selected input method. This may be any of the values + * defined within preferenceService.inputMethods. + */ + inputMethod: WritableSignal; + + /** + * Whether translation of touch to mouse events should emulate an + * absolute pointer device, or a relative pointer device. + */ + emulateAbsoluteMouse: WritableSignal; + + /** + * The current scroll state of the menu. + */ + scrollState: WritableSignal; + + /** + * The current desired values of all editable connection parameters as + * a set of name/value pairs, including any changes made by the user. + */ + connectionParameters: Record; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientProperties.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientProperties.ts new file mode 100644 index 0000000000..99eec52e9c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ClientProperties.ts @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * A service for generating new guacClient properties objects. + * Object used for interacting with a guacClient directive. + */ +export class ClientProperties { + + /** + * Whether the display should be scaled automatically to fit within the + * available space. + * + * @default true + */ + autoFit: boolean; + + /** + * The current scale. If autoFit is true, the effect of setting this + * value is undefined. + * + * @default 1 + */ + scale: number; + + /** + * The minimum scale value. + * + * @default 1 + */ + minScale: number; + + /** + * The maximum scale value. + * + * @default 3 + */ + maxScale: number; + + /** + * Whether this client should receive keyboard events. + * + * @default false + */ + focused: boolean; + + /** + * The relative Y coordinate of the scroll offset of the display within + * the client element. + * + * @default 0 + */ + scrollTop: number; + + /** + * The relative X coordinate of the scroll offset of the display within + * the client element. + * + * @default 0 + */ + scrollLeft: number; + + /** + * @param template + * The object whose properties should be copied within the new + * ClientProperties. + */ + constructor(template: Partial = {}) { + this.autoFit = template.autoFit || true; + this.scale = template.scale || 1; + this.minScale = template.minScale || 1; + this.maxScale = template.maxScale || 3; + this.focused = template.focused || false; + this.scrollTop = template.scrollTop || 0; + this.scrollLeft = template.scrollLeft || 0; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ConnectionListContext.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ConnectionListContext.ts new file mode 100644 index 0000000000..cd0e9945e8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ConnectionListContext.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Arbitrary context that should be exposed to the guacGroupList directive + * displaying the dropdown list of available connections within the + * Guacamole menu. + */ +export interface ConnectionListContext { + + /** + * The set of clients desired within the current view. For each client + * that should be present within the current view, that client's ID + * will map to "true" here. + */ + attachedClients: Record; + + /** + * Notifies that the client with the given ID has been added or + * removed from the set of clients desired within the current view, + * and the current view should be updated accordingly. + * + * @param id + * The ID of the client that was added or removed from the current + * view. + */ + updateAttachedClients(id: string): void; + +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedArgument.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedArgument.ts similarity index 53% rename from guacamole/src/main/frontend/src/app/client/types/ManagedArgument.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedArgument.ts index 5e676bfcb1..745ca257e4 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedArgument.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedArgument.ts @@ -17,104 +17,100 @@ * under the License. */ +import { ManagedClient } from './ManagedClient'; + /** * Provides the ManagedArgument class used by ManagedClient. + * Represents an argument (connection parameter) which may be + * changed by the user while the connection is open. */ -angular.module('client').factory('ManagedArgument', ['$q', function defineManagedArgument($q) { +export class ManagedArgument { /** - * Object which represents an argument (connection parameter) which may be - * changed by the user while the connection is open. - * - * @constructor - * @param {ManagedArgument|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedArgument. + * The name of the connection parameter. */ - var ManagedArgument = function ManagedArgument(template) { + name: string; - // Use empty object by default - template = template || {}; + /** + * The current value of the connection parameter. + */ + value: string; - /** - * The name of the connection parameter. - * - * @type {String} - */ - this.name = template.name; + /** + * A valid, open output stream which may be used to apply a new value + * to the connection parameter. + */ + stream: Guacamole.OutputStream; - /** - * The current value of the connection parameter. - * - * @type {String} - */ - this.value = template.value; + /** + * True if this argument has been modified in the webapp, but yet to + * be confirmed by guacd, or false in any other case. A pending + * argument cannot be modified again, and must be recreated before + * editing is enabled again. + */ + pending: boolean; - /** - * A valid, open output stream which may be used to apply a new value - * to the connection parameter. - * - * @type {Guacamole.OutputStream} - */ + /** + * Creates a new ManagedArgument. This constructor initializes the properties of the + * new ManagedArgument with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManagedArgument. + */ + constructor(template: Omit) { + this.name = template.name; + this.value = template.value; this.stream = template.stream; - - /** - * True if this argument has been modified in the webapp, but yet to - * be confirmed by guacd, or false in any other case. A pending - * argument cannot be modified again, and must be recreated before - * editing is enabled again. - * - * @type {boolean} - */ this.pending = false; - - }; + } /** * Requests editable access to a given connection parameter, returning a * promise which is resolved with a ManagedArgument instance that provides * such access if the parameter is indeed editable. * - * @param {ManagedClient} managedClient + * @param managedClient * The ManagedClient instance associated with the connection for which * an editable version of the connection parameter is being retrieved. * - * @param {String} name + * @param name * The name of the connection parameter. * - * @param {String} value + * @param value * The current value of the connection parameter, as received from a * prior, inbound "argv" stream. * - * @returns {Promise.} + * @returns * A promise which is resolved with the new ManagedArgument instance * once the requested parameter has been verified as editable. */ - ManagedArgument.getInstance = function getInstance(managedClient, name, value) { + static getInstance(managedClient: ManagedClient, name: string, value: string): Promise { - var deferred = $q.defer(); + return new Promise((resolve, reject) => { - // Create internal, fully-populated instance of ManagedArgument, to be - // returned only once mutability of the associated connection parameter - // has been verified - var managedArgument = new ManagedArgument({ - name : name, - value : value, - stream : managedClient.client.createArgumentValueStream('text/plain', name) - }); - // The connection parameter is editable only if a successful "ack" is - // received - managedArgument.stream.onack = function ackReceived(status) { - if (status.isError()) - deferred.reject(status); - else - deferred.resolve(managedArgument); - }; + // Create internal, fully-populated instance of ManagedArgument, to be + // returned only once mutability of the associated connection parameter + // has been verified + const managedArgument = new ManagedArgument({ + name : name, + value : value, + stream: managedClient.client.createArgumentValueStream('text/plain', name) + }); + + // The connection parameter is editable only if a successful "ack" is + // received + managedArgument.stream.onack = (status: Guacamole.Status) => { + if (status.isError()) + reject(status); + else + resolve(managedArgument); + }; - return deferred.promise; + }); - }; + } /** * Sets the given editable argument (connection parameter) to the given @@ -124,20 +120,21 @@ angular.module('client').factory('ManagedArgument', ['$q', function defineManage * instance. This function only has an effect if the new parameter value * is different from the current value. * - * @param {ManagedArgument} managedArgument + * @param managedArgument * The ManagedArgument instance associated with the connection * parameter being modified. * - * @param {String} value + * @param value * The new value to assign to the connection parameter. */ - ManagedArgument.setValue = function setValue(managedArgument, value) { + static setValue(managedArgument: ManagedArgument, value: string): void { + // Stream new value only if value has changed and a change is not // already pending if (!managedArgument.pending && value !== managedArgument.value) { - var writer = new Guacamole.StringWriter(managedArgument.stream); + const writer = new Guacamole.StringWriter(managedArgument.stream); writer.sendText(value); writer.sendEnd(); @@ -146,8 +143,5 @@ angular.module('client').factory('ManagedArgument', ['$q', function defineManage } - }; - - return ManagedArgument; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClient.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClient.ts new file mode 100644 index 0000000000..76424a44a1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClient.ts @@ -0,0 +1,284 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Form } from '../../rest/types/Form'; +import { Optional } from '../../util/utility-types'; +import { ClientProperties } from './ClientProperties'; +import { ManagedArgument } from './ManagedArgument'; +import { ManagedClientState } from './ManagedClientState'; +import { ManagedClientThumbnail } from './ManagedClientThumbnail'; +import { ManagedDisplay } from './ManagedDisplay'; +import { ManagedFilesystem } from './ManagedFilesystem'; +import { ManagedFileUpload } from './ManagedFileUpload'; +import { ManagedShareLink } from './ManagedShareLink'; + +/** + * Type definition for the onpipe handler accepted by the Guacamole.Client + */ +export type PipeStreamHandler = NonNullable; + +/** + * A deferred pipe stream, that has yet to be consumed, as well as all + * axuilary information needed to pull data from the stream. + */ +export class DeferredPipeStream { + + /** + * The stream that will receive data from the server. + */ + stream: Guacamole.InputStream; + + /** + * The mimetype of the data which will be received. + */ + mimetype: string; + + /** + * The name of the pipe. + */ + name: string; + + /** + * Creates a new DeferredPipeStream. + * + * @param template + * The object whose properties should be copied within the new + * DeferredPipeStream. + */ + constructor(template: DeferredPipeStream) { + + this.stream = template.stream; + this.mimetype = template.mimetype; + this.name = template.name; + + } + +} + +/** + * Type definition for the template parameter accepted by the ManagedClient constructor. + * Properties that have a default value are optional. + */ +export type ManagedClientTemplate = Optional; + +/** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * client while it is active, allowing it to be maintained in the + * background. One or more ManagedClients are grouped within + * ManagedClientGroups before being attached to the client view. + */ +export class ManagedClient { + + /** + * The ID of the connection associated with this client. + */ + id: string; + + /** + * The actual underlying Guacamole client. + */ + client: Guacamole.Client; + + /** + * The tunnel being used by the underlying Guacamole client. + */ + tunnel: Guacamole.Tunnel; + + /** + * The display associated with the underlying Guacamole client. + */ + managedDisplay?: ManagedDisplay; + + /** + * The name returned associated with the connection or connection + * group in use. + */ + name?: string; + + /** + * The title which should be displayed as the page title for this + * client. + */ + title?: string; + + /** + * The name which uniquely identifies the protocol of the connection in + * use. If the protocol cannot be determined, such as when a connection + * group is in use, this will be null. + */ + protocol: string | null; + + /** + * An array of forms describing all known parameters for the connection + * in use, including those which may not be editable. + */ + forms: Form[]; + + /** + * The most recently-generated thumbnail for this connection, as + * stored within the local connection history. If no thumbnail is + * stored, this will be null. + */ + thumbnail: ManagedClientThumbnail | null; + + /** + * The current state of all parameters requested by the server via + * "required" instructions, where each object key is the name of a + * requested parameter and each value is the current value entered by + * the user or null if no parameters are currently being requested. + */ + requiredParameters: Record | null; + + /** + * All uploaded files. As files are uploaded, their progress can be + * observed through the elements of this array. It is intended that + * this array be manipulated externally as needed. + */ + uploads: ManagedFileUpload[]; + + /** + * All currently-exposed filesystems. When the Guacamole server exposes + * a filesystem object, that object will be made available as a + * ManagedFilesystem within this array. + */ + filesystems: ManagedFilesystem[]; + + /** + * The current number of users sharing this connection, excluding the + * user that originally started the connection. Duplicate connections + * from the same user are included in this total. + */ + userCount: number; + + /** + * All users currently sharing this connection, excluding the user that + * originally started the connection. If the connection is not shared, + * this object will be empty. This map consists of key/value pairs + * where each key is the user's username and each value is an object + * tracking the unique connections currently used by that user (a map + * of Guacamole protocol user IDs to boolean values). + */ + users: Record>; + + /** + * All available share links generated for the this ManagedClient via + * ManagedClient.createShareLink(). Each resulting share link is stored + * under the identifier of its corresponding SharingProfile. + */ + shareLinks: Record; + + /** + * The number of simultaneous touch contacts supported by the remote + * desktop. Unless explicitly declared otherwise by the remote desktop + * after connecting, this will be 0 (multi-touch unsupported). + */ + multiTouchSupport: number; + + /** + * The current state of the Guacamole client (idle, connecting, + * connected, terminated with error, etc.). + */ + clientState: ManagedClientState; + + /** + * Properties associated with the display and behavior of the Guacamole + * client. + */ + clientProperties: ClientProperties; + + /** + * All editable arguments (connection parameters), stored by their + * names. Arguments will only be present within this set if their + * current values have been exposed by the server via an inbound "argv" + * stream and the server has confirmed that the value may be changed + * through a successful "ack" to an outbound "argv" stream. + */ + arguments: Record; + + /** + * Any received pipe streams that have not been consumed by an onpipe + * handler or registered pipe handler, indexed by pipe stream name. + */ + deferredPipeStreams: Record = {}; + + /** + * Handlers for deferred pipe streams, indexed by the name of the pipe + * stream that the handler should handle. + */ + deferredPipeStreamHandlers: Record = {}; + + /** + * The mimetype of audio data to be sent along the Guacamole connection if + * audio input is supported. + */ + static readonly AUDIO_INPUT_MIMETYPE: string = 'audio/L16;rate=44100,channels=2'; + + /** + * Creates a new ManagedClient. This constructor initializes the properties of the + * new ManagedClient with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManagedClient. + */ + constructor(template: ManagedClientTemplate) { + this.id = template.id; + this.client = template.client; + this.tunnel = template.tunnel; + this.managedDisplay = template.managedDisplay; + this.name = template.name; + this.title = template.title; + this.protocol = template.protocol || null; + this.forms = template.forms || []; + this.thumbnail = template.thumbnail = null; + this.requiredParameters = null; + this.uploads = template.uploads || []; + this.filesystems = template.filesystems || []; + this.userCount = template.userCount || 0; + this.users = template.users || {}; + this.shareLinks = template.shareLinks || {}; + this.multiTouchSupport = template.multiTouchSupport || 0; + this.clientState = template.clientState || new ManagedClientState(); + this.clientProperties = template.clientProperties || new ClientProperties(); + this.arguments = template.arguments || {}; + this.deferredPipeStreams = template.deferredPipeStreams || {}; + this.deferredPipeStreamHandlers = template.deferredPipeStreamHandlers || {}; + } + + +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientGroup.ts similarity index 67% rename from guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientGroup.ts index cd42bdc75b..91ac72d86d 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClientGroup.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientGroup.ts @@ -17,74 +17,96 @@ * under the License. */ +import _ from 'lodash'; +import { ManagedClient } from './ManagedClient'; + +export namespace ManagedClientGroup { + + /** + * A callback that is invoked for a ManagedClient within a ManagedClientGroup. + * + * @param client + * The relevant ManagedClient. + * + * @param row + * The row number of the client within the tiled grid, where 0 is the + * first row. + * + * @param column + * The column number of the client within the tiled grid, where 0 is + * the first column. + * + * @param index + * The index of the client within the relevant + * {@link ManagedClientGroup#clients} array. + */ + export type ClientCallback = (client: ManagedClient, row: number, column: number, index: number) => void; + +} + /** - * Provides the ManagedClientGroup class used by the guacClientManager service. + * Serves as a grouping of ManagedClients. Each + * ManagedClientGroup may be attached, detached, and reattached dynamically + * from different client views, with its contents automatically displayed + * in a tiled arrangement if needed. + * + * Used by the guacClientManager service. */ -angular.module('client').factory('ManagedClientGroup', ['$injector', function defineManagedClientGroup($injector) { +export class ManagedClientGroup { + + /** + * The time that this group was last brought to the foreground of + * the current tab, as the number of milliseconds elapsed since + * midnight of January 1, 1970 UTC. If the group has not yet been + * viewed, this will be 0. + */ + lastUsed: number; + + /** + * Whether this ManagedClientGroup is currently attached to the client + * interface (true) or is running in the background (false). + * + * @default false + */ + attached: boolean; + + /** + * The clients that should be displayed within the client interface + * when this group is attached. + * + * @default [] + */ + clients: ManagedClient[]; + + /** + * The number of rows that should be used when arranging the clients + * within this group in a grid. By default, this value is automatically + * calculated from the number of clients. + */ + rows: number; + + /** + * The number of columns that should be used when arranging the clients + * within this group in a grid. By default, this value is automatically + * calculated from the number of clients. + */ + columns: number; /** - * Object which serves as a grouping of ManagedClients. Each - * ManagedClientGroup may be attached, detached, and reattached dynamically - * from different client views, with its contents automatically displayed - * in a tiled arrangment if needed. - * - * @constructor - * @param {ManagedClientGroup|Object} [template={}] + * Creates a new ManagedClientGroup. This constructor initializes the properties of the + * new ManagedClientGroup with the corresponding properties of the given template. + * + * @param template * The object whose properties should be copied within the new * ManagedClientGroup. */ - const ManagedClientGroup = function ManagedClientGroup(template) { - - // Use empty object by default - template = template || {}; - - /** - * The time that this group was last brought to the foreground of - * the current tab, as the number of milliseconds elapsed since - * midnight of January 1, 1970 UTC. If the group has not yet been - * viewed, this will be 0. - * - * @type Number - */ + constructor(template: Partial = {}) { this.lastUsed = template.lastUsed || 0; - - /** - * Whether this ManagedClientGroup is currently attached to the client - * interface (true) or is running in the background (false). - * - * @type {boolean} - * @default false - */ this.attached = template.attached || false; - - /** - * The clients that should be displayed within the client interface - * when this group is attached. - * - * @type {ManagedClient[]} - * @default [] - */ this.clients = template.clients || []; - - /** - * The number of rows that should be used when arranging the clients - * within this group in a grid. By default, this value is automatically - * calculated from the number of clients. - * - * @type {number} - */ this.rows = template.rows || ManagedClientGroup.getRows(this); - - /** - * The number of columns that should be used when arranging the clients - * within this group in a grid. By default, this value is automatically - * calculated from the number of clients. - * - * @type {number} - */ this.columns = template.columns || ManagedClientGroup.getColumns(this); - - }; + } /** * Updates the number of rows and columns stored within the given @@ -92,109 +114,115 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de * distributed. This function should be called whenever the size of a * group changes. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup that should be updated. */ - ManagedClientGroup.recalculateTiles = function recalculateTiles(group) { + static recalculateTiles(group: ManagedClientGroup): void { const recalculated = new ManagedClientGroup({ - clients : group.clients + clients: group.clients }); group.rows = recalculated.rows; group.columns = recalculated.columns; - }; + } /** * Returns the unique ID representing the given ManagedClientGroup or set * of client IDs. The ID of a ManagedClientGroup consists simply of the * IDs of all its ManagedClients, separated by periods. * - * @param {ManagedClientGroup|string[]} group + * @param group * The ManagedClientGroup or array of client IDs to determine the * ManagedClientGroup ID of. * - * @returns {string} + * @returns * The unique ID representing the given ManagedClientGroup, or the * unique ID that would represent a ManagedClientGroup containing the * clients with the given IDs. */ - ManagedClientGroup.getIdentifier = function getIdentifier(group) { + static getIdentifier(group: ManagedClientGroup | string[]): string { if (!_.isArray(group)) group = _.map(group.clients, client => client.id); return group.join('.'); - }; + } /** * Returns an array of client identifiers for all clients contained within * the given ManagedClientGroup. Order of the identifiers is preserved * with respect to the order of the clients within the group. * - * @param {ManagedClientGroup|string} group + * @param group * The ManagedClientGroup to retrieve the client identifiers from, * or its ID. * - * @returns {string[]} + * @returns * The client identifiers of all clients contained within the given * ManagedClientGroup. */ - ManagedClientGroup.getClientIdentifiers = function getClientIdentifiers(group) { + static getClientIdentifiers(group: ManagedClientGroup | string): string[] { if (_.isString(group)) return group.split(/\./); return group.clients.map(client => client.id); - }; + } /** * Returns the number of columns that should be used to evenly arrange * all provided clients in a tiled grid. * - * @returns {Number} + * @param group + * The ManagedClientGroup to calculate the number of columns for. + * + * @returns * The number of columns that should be used for the grid of * clients. */ - ManagedClientGroup.getColumns = function getColumns(group) { + static getColumns(group: ManagedClientGroup): number { if (!group.clients.length) return 0; return Math.ceil(Math.sqrt(group.clients.length)); - }; + } /** * Returns the number of rows that should be used to evenly arrange all * provided clients in a tiled grid. * - * @returns {Number} + * @param group + * The ManagedClientGroup to calculate the number of rows for. + * + * @returns * The number of rows that should be used for the grid of clients. */ - ManagedClientGroup.getRows = function getRows(group) { + static getRows(group: ManagedClientGroup): number { if (!group.clients.length) return 0; return Math.ceil(group.clients.length / ManagedClientGroup.getColumns(group)); - }; + } /** * Returns the title which should be displayed as the page title if the * given client group is attached to the interface. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup to determine the title of. * - * @returns {string} + * @returns * The title of the given ManagedClientGroup. */ - ManagedClientGroup.getTitle = function getTitle(group) { + static getTitle(group: ManagedClientGroup): string | undefined { // Use client-specific title if only one client if (group.clients.length === 1) @@ -204,21 +232,21 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de // be confusing. Instead, use the combined names. return ManagedClientGroup.getName(group); - }; + } /** * Returns the combined names of all clients within the given * ManagedClientGroup, as determined by the names of the associated * connections or connection groups. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup to determine the name of. * - * @returns {string} + * @returns * The combined names of all clients within the given * ManagedClientGroup. */ - ManagedClientGroup.getName = function getName(group) { + static getName(group: ManagedClientGroup): string { // Generate a name from ONLY the focused clients, unless there are no // focused clients @@ -228,40 +256,20 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de return _.filter(relevantClients, (client => !!client.name)).map(client => client.name).join(', ') || '...'; - }; - - /** - * A callback that is invoked for a ManagedClient within a ManagedClientGroup. - * - * @callback ManagedClientGroup~clientCallback - * @param {ManagedClient} client - * The relevant ManagedClient. - * - * @param {number} row - * The row number of the client within the tiled grid, where 0 is the - * first row. - * - * @param {number} column - * The column number of the client within the tiled grid, where 0 is - * the first column. - * - * @param {number} index - * The index of the client within the relevant - * {@link ManagedClientGroup#clients} array. - */ + } /** * Loops through each of the clients associated with the given * ManagedClientGroup, invoking the given callback for each client. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup to loop through. * - * @param {ManagedClientGroup~clientCallback} callback + * @param callback * The callback to invoke for each of the clients within the given * ManagedClientGroup. */ - ManagedClientGroup.forEach = function forEach(group, callback) { + static forEach(group: ManagedClientGroup, callback: ManagedClientGroup.ClientCallback): void { let current = 0; for (let row = 0; row < group.rows; row++) { for (let column = 0; column < group.columns; column++) { @@ -274,22 +282,22 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de } } - }; + } /** * Returns whether the given ManagedClientGroup contains more than one * client. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup to test. * - * @returns {boolean} + * @returns * true if two or more clients are currently present in the given * group, false otherwise. */ - ManagedClientGroup.hasMultipleClients = function hasMultipleClients(group) { - return group && group.clients.length > 1; - }; + static hasMultipleClients(group: ManagedClientGroup | null): boolean { + return !!group && group.clients.length > 1; + } /** * Returns a two-dimensional array of all ManagedClients within the given @@ -297,25 +305,25 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de * and {@link ManagedClientGroup#columns}. If any grid cell lacks a * corresponding client (because the number of clients does not divide * evenly into a grid), that cell will be null. - * + * * For the sake of AngularJS scope watches, the results of calling this * function are cached and will always favor modifying an existing array * over creating a new array, even for nested arrays. * - * @param {ManagedClientGroup} group + * @param group * The ManagedClientGroup defining the tiled grid arrangement of * ManagedClients. * - * @returns {ManagedClient[][]} + * @returns * A two-dimensional array of all ManagedClients within the given * group. */ - ManagedClientGroup.getClientGrid = function getClientGrid(group) { + static getClientGrid(group: ManagedClientGroup): ManagedClient[][] { let index = 0; // Operate on cached copy of grid - const clientGrid = group._grid || (group._grid = []); + const clientGrid = (group as any)._grid || ((group as any)._grid = []); // Delete any rows in excess of the required size clientGrid.splice(group.rows); @@ -335,25 +343,22 @@ angular.module('client').factory('ManagedClientGroup', ['$injector', function de return clientGrid; - }; + } /** * Verifies that focus is assigned to at least one client in the given * group. If no client has focus, focus is assigned to the first client in * the group. * - * @param {ManagedClientGroup} group + * @param group * The group to verify. */ - ManagedClientGroup.verifyFocus = function verifyFocus(group) { + static verifyFocus(group: ManagedClientGroup | null): void { // Focus the first client if there are no clients focused - if (group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) { + if (group && group.clients.length >= 1 && _.findIndex(group.clients, client => client.clientProperties.focused) === -1) { group.clients[0].clientProperties.focused = true; } - }; - - return ManagedClientGroup; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClientState.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientState.ts similarity index 62% rename from guacamole/src/main/frontend/src/app/client/types/ManagedClientState.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientState.ts index 10f71b4d29..fbe4cdf467 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClientState.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientState.ts @@ -19,114 +19,93 @@ /** * Provides the ManagedClient class used by the guacClientManager service. + * Object which represents the state of a Guacamole client and its tunnel, + * including any error conditions. */ -angular.module('client').factory('ManagedClientState', [function defineManagedClientState() { +export class ManagedClientState { /** - * Object which represents the state of a Guacamole client and its tunnel, - * including any error conditions. - * - * @constructor - * @param {ManagedClientState|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedClientState. + * The current connection state. Valid values are described by + * ManagedClientState.ConnectionState. + * + * @default ManagedClientState.ConnectionState.IDLE */ - var ManagedClientState = function ManagedClientState(template) { + connectionState: string; - // Use empty object by default - template = template || {}; + /** + * Whether the network connection used by the tunnel seems unstable. If + * the network connection is unstable, the remote desktop connection + * may perform poorly or disconnect. + * + * @default false + */ + tunnelUnstable: boolean; - /** - * The current connection state. Valid values are described by - * ManagedClientState.ConnectionState. - * - * @type String - * @default ManagedClientState.ConnectionState.IDLE - */ - this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE; + /** + * The status code of the current error condition, if connectionState + * is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState + * values, this will be @link{Guacamole.Status.Code.SUCCESS}. + * + * @default Guacamole.Status.Code.SUCCESS + */ + statusCode: number; - /** - * Whether the network connection used by the tunnel seems unstable. If - * the network connection is unstable, the remote desktop connection - * may perform poorly or disconnect. - * - * @type Boolean - * @default false - */ + /** + * @param template + * The object whose properties should be copied within the new + * ManagedClientState. + */ + constructor(template: Partial = {}) { + this.connectionState = template.connectionState || ManagedClientState.ConnectionState.IDLE; this.tunnelUnstable = template.tunnelUnstable || false; - - /** - * The status code of the current error condition, if connectionState - * is CLIENT_ERROR or TUNNEL_ERROR. For all other connectionState - * values, this will be @link{Guacamole.Status.Code.SUCCESS}. - * - * @type Number - * @default Guacamole.Status.Code.SUCCESS - */ this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; - - }; + } /** * Valid connection state strings. Each state string is associated with a * specific state of a Guacamole connection. */ - ManagedClientState.ConnectionState = { + static ConnectionState = { /** * The Guacamole connection has not yet been attempted. - * - * @type String */ - IDLE : "IDLE", + IDLE: 'IDLE', /** * The Guacamole connection is being established. - * - * @type String */ - CONNECTING : "CONNECTING", + CONNECTING: 'CONNECTING', /** * The Guacamole connection has been successfully established, and the * client is now waiting for receipt of initial graphical data. - * - * @type String */ - WAITING : "WAITING", + WAITING: 'WAITING', /** * The Guacamole connection has been successfully established, and * initial graphical data has been received. - * - * @type String */ - CONNECTED : "CONNECTED", + CONNECTED: 'CONNECTED', /** * The Guacamole connection has terminated successfully. No errors are * indicated. - * - * @type String */ - DISCONNECTED : "DISCONNECTED", + DISCONNECTED: 'DISCONNECTED', /** * The Guacamole connection has terminated due to an error reported by * the client. The associated error code is stored in statusCode. - * - * @type String */ - CLIENT_ERROR : "CLIENT_ERROR", + CLIENT_ERROR: 'CLIENT_ERROR', /** * The Guacamole connection has terminated due to an error reported by * the tunnel. The associated error code is stored in statusCode. - * - * @type String */ - TUNNEL_ERROR : "TUNNEL_ERROR" - + TUNNEL_ERROR: 'TUNNEL_ERROR' }; /** @@ -135,23 +114,23 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl * client state was previously marked as unstable, that flag is implicitly * cleared. * - * @param {ManagedClientState} clientState + * @param clientState * The ManagedClientState to update. * - * @param {String} connectionState + * @param connectionState * The connection state to assign to the given ManagedClientState, as * listed within ManagedClientState.ConnectionState. - * - * @param {Number} [statusCode] + * + * @param [statusCode] * The status code to assign to the given ManagedClientState, if any, * as listed within Guacamole.Status.Code. If no status code is * specified, the status code of the ManagedClientState is not touched. */ - ManagedClientState.setConnectionState = function(clientState, connectionState, statusCode) { + static setConnectionState(clientState: ManagedClientState, connectionState: string, statusCode?: number): void { // Do not set state after an error is registered if (clientState.connectionState === ManagedClientState.ConnectionState.TUNNEL_ERROR - || clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) + || clientState.connectionState === ManagedClientState.ConnectionState.CLIENT_ERROR) return; // Update connection state @@ -162,24 +141,21 @@ angular.module('client').factory('ManagedClientState', [function defineManagedCl if (statusCode) clientState.statusCode = statusCode; - }; + } /** * Updates the given client state, setting whether the underlying tunnel * is currently unstable. An unstable tunnel is not necessarily * disconnected, but appears to be misbehaving and may be disconnected. * - * @param {ManagedClientState} clientState + * @param clientState * The ManagedClientState to update. * - * @param {Boolean} unstable + * @param unstable * Whether the underlying tunnel of the connection currently appears * unstable. */ - ManagedClientState.setTunnelUnstable = function setTunnelUnstable(clientState, unstable) { + static setTunnelUnstable(clientState: ManagedClientState, unstable: boolean): void { clientState.tunnelUnstable = unstable; - }; - - return ManagedClientState; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedClientThumbnail.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientThumbnail.ts similarity index 56% rename from guacamole/src/main/frontend/src/app/client/types/ManagedClientThumbnail.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientThumbnail.ts index fd9b3dea5e..e784605e7e 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedClientThumbnail.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedClientThumbnail.ts @@ -19,40 +19,32 @@ /** * Provides the ManagedClientThumbnail class used by ManagedClient. + * Object which represents a thumbnail of the Guacamole client display, + * along with the time that the thumbnail was generated. */ -angular.module('client').factory('ManagedClientThumbnail', [function defineManagedClientThumbnail() { +export class ManagedClientThumbnail { /** - * Object which represents a thumbnail of the Guacamole client display, - * along with the time that the thumbnail was generated. + * The time that this thumbnail was generated, as the number of + * milliseconds elapsed since midnight of January 1, 1970 UTC. + */ + timestamp: number; + + /** + * The thumbnail of the Guacamole client display. + */ + canvas: HTMLCanvasElement; + + /** + * Creates a new ManagedClientThumbnail. This constructor initializes the properties of the + * new ManagedClientThumbnail with the corresponding properties of the given template. * - * @constructor - * @param {ManagedClientThumbnail|Object} [template={}] + * @param template * The object whose properties should be copied within the new * ManagedClientThumbnail. */ - var ManagedClientThumbnail = function ManagedClientThumbnail(template) { - - // Use empty object by default - template = template || {}; - - /** - * The time that this thumbnail was generated, as the number of - * milliseconds elapsed since midnight of January 1, 1970 UTC. - * - * @type Number - */ + constructor(template: ManagedClientThumbnail) { this.timestamp = template.timestamp; - - /** - * The thumbnail of the Guacamole client display. - * - * @type HTMLCanvasElement - */ this.canvas = template.canvas; - - }; - - return ManagedClientThumbnail; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedDisplay.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedDisplay.ts new file mode 100644 index 0000000000..3b86ad21ac --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedDisplay.ts @@ -0,0 +1,170 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { signal, WritableSignal } from '@angular/core'; + + +export type ManagedDisplayTemplate = { + display?: Guacamole.Display; + size?: ManagedDisplayDimensions; + cursor?: ManagedDisplayCursor; +} + + +/** + * Provides the ManagedDisplay class used by the guacClientManager service. + * Object which serves as a surrogate interface, encapsulating a Guacamole + * display while it is active, allowing it to be detached and reattached + * from different client views. + */ +export class ManagedDisplay { + /** + * The underlying Guacamole display. + */ + display: WritableSignal; + + /** + * The current size of the Guacamole display. + */ + size: ManagedDisplayDimensions; + + /** + * The current mouse cursor, if any. + */ + cursor: ManagedDisplayCursor | undefined; + + /** + * @param [template={}] + * The object whose properties should be copied within the new + * ManagedDisplay. + */ + constructor(template: ManagedDisplayTemplate = {}) { + this.display = signal(template.display); + this.size = new ManagedDisplayDimensions(template.size); + this.cursor = template.cursor; + } + + /** + * Creates a new ManagedDisplay which represents the current state of the + * given Guacamole display. + * + * @param display + * The Guacamole display to represent. Changes to this display will + * affect this ManagedDisplay. + * + * @returns + * A new ManagedDisplay which represents the current state of the + * given Guacamole display. + */ + static getInstance(display: Guacamole.Display): ManagedDisplay { + + const managedDisplay = new ManagedDisplay({ + display: display + }); + + // Store changes to display size + display.onresize = function setClientSize() { + + managedDisplay.size = new ManagedDisplayDimensions({ + width : display.getWidth(), + height: display.getHeight() + }); + + }; + + // Store changes to display cursor + display.oncursor = function setClientCursor(canvas, x, y) { + + managedDisplay.cursor = new ManagedDisplayCursor({ + canvas: canvas, + x : x, + y : y + }); + + }; + + return managedDisplay; + } + +} + +/** + * Object which represents the size of the Guacamole display. + */ +export class ManagedDisplayDimensions { + + /** + * The current width of the Guacamole display, in pixels. + */ + width: number; + + /** + * The current width of the Guacamole display, in pixels. + */ + height: number; + + /** + * Creates a new ManagedDisplayDimensions object. This constructor initializes + * the properties of the new Dimensions with the corresponding properties + * of the given template. + * + * @param [template={}] + * The object whose properties should be copied within the new + * ManagedDisplayDimensions. + */ + constructor(template: Partial = {}) { + this.width = template.width || 0; + this.height = template.height || 0; + } +} + +/** + * Object which represents a mouse cursor used by the Guacamole display. + */ +export class ManagedDisplayCursor { + + /** + * The actual mouse cursor image. + */ + canvas?: HTMLCanvasElement; + + /** + * The X coordinate of the cursor hotspot. + */ + x?: number; + + /** + * The Y coordinate of the cursor hotspot. + */ + y?: number; + + /** + * Creates a new ManagedDisplayCursor. This constructor initializes the properties of the + * new Cursor with the corresponding properties of the given template. + * + * @param [template={}] + * The object whose properties should be copied within the new + * ManagedDisplayCursor. + */ + constructor(template: Partial = {}) { + this.canvas = template.canvas; + this.x = template.x; + this.y = template.y; + } +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedFileTransferState.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileTransferState.ts similarity index 61% rename from guacamole/src/main/frontend/src/app/client/types/ManagedFileTransferState.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileTransferState.ts index 1301cc7a91..9a660b0b6f 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedFileTransferState.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileTransferState.ts @@ -18,102 +18,61 @@ */ /** - * Provides the ManagedFileTransferState class used by the guacClientManager - * service. + * Represents the state of a Guacamole stream, including any + * error conditions. + * + * This class is used by the guacClientManager service. */ -angular.module('client').factory('ManagedFileTransferState', [function defineManagedFileTransferState() { +export class ManagedFileTransferState { /** - * Object which represents the state of a Guacamole stream, including any - * error conditions. - * - * @constructor - * @param {ManagedFileTransferState|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedFileTransferState. + * The current stream state. Valid values are described by + * ManagedFileTransferState.StreamState. + * + * @default ManagedFileTransferState.StreamState.IDLE */ - var ManagedFileTransferState = function ManagedFileTransferState(template) { - - // Use empty object by default - template = template || {}; - - /** - * The current stream state. Valid values are described by - * ManagedFileTransferState.StreamState. - * - * @type String - * @default ManagedFileTransferState.StreamState.IDLE - */ - this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE; - - /** - * The status code of the current error condition, if streamState - * is ERROR. For all other streamState values, this will be - * @link{Guacamole.Status.Code.SUCCESS}. - * - * @type Number - * @default Guacamole.Status.Code.SUCCESS - */ - this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; - - }; + streamState: ManagedFileTransferState.StreamState; /** - * Valid stream state strings. Each state string is associated with a - * specific state of a Guacamole stream. + * The status code of the current error condition, if streamState + * is ERROR. For all other streamState values, this will be + * {@link Guacamole.Status.Code.SUCCESS}. + * + * @default Guacamole.Status.Code.SUCCESS */ - ManagedFileTransferState.StreamState = { - - /** - * The stream has not yet been opened. - * - * @type String - */ - IDLE : "IDLE", - - /** - * The stream has been successfully established. Data can be sent or - * received. - * - * @type String - */ - OPEN : "OPEN", - - /** - * The stream has terminated successfully. No errors are indicated. - * - * @type String - */ - CLOSED : "CLOSED", - - /** - * The stream has terminated due to an error. The associated error code - * is stored in statusCode. - * - * @type String - */ - ERROR : "ERROR" + statusCode: number; - }; + /** + * Creates a new ManagedFileTransferState object. This constructor initializes the properties of the + * new ManagedFileTransferState with the corresponding properties of the given template. + * + * @param [template={}] + * The object whose properties should be copied within the new + * ManagedFileTransferState. + */ + constructor(template: Partial = {}) { + this.streamState = template.streamState || ManagedFileTransferState.StreamState.IDLE; + this.statusCode = template.statusCode || Guacamole.Status.Code.SUCCESS; + } /** * Sets the current transfer state and, if given, the associated status * code. If an error is already represented, this function has no effect. * - * @param {ManagedFileTransferState} transferState + * @param transferState * The ManagedFileTransferState to update. * - * @param {String} streamState + * @param streamState * The stream state to assign to the given ManagedFileTransferState, as * listed within ManagedFileTransferState.StreamState. - * - * @param {Number} [statusCode] + * + * @param statusCode * The status code to assign to the given ManagedFileTransferState, if * any, as listed within Guacamole.Status.Code. If no status code is * specified, the status code of the ManagedFileTransferState is not * touched. */ - ManagedFileTransferState.setStreamState = function setStreamState(transferState, streamState, statusCode) { + static setStreamState(transferState: ManagedFileTransferState, streamState: ManagedFileTransferState.StreamState, statusCode?: number): void { // Do not set state after an error is registered if (transferState.streamState === ManagedFileTransferState.StreamState.ERROR) @@ -126,8 +85,38 @@ angular.module('client').factory('ManagedFileTransferState', [function defineMan if (statusCode) transferState.statusCode = statusCode; - }; + } +} + +export namespace ManagedFileTransferState { + + /** + * Valid stream state strings. Each state string is associated with a + * specific state of a Guacamole stream. + */ + export enum StreamState { - return ManagedFileTransferState; + /** + * The stream has not yet been opened. + */ + IDLE = 'IDLE', + + /** + * The stream has been successfully established. Data can be sent or + * received. + */ + OPEN = 'OPEN', + + /** + * The stream has terminated successfully. No errors are indicated. + */ + CLOSED = 'CLOSED', + + /** + * The stream has terminated due to an error. The associated error code + * is stored in statusCode. + */ + ERROR = 'ERROR' -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileUpload.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileUpload.ts new file mode 100644 index 0000000000..cd61b60fa2 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFileUpload.ts @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Optional } from '../../util/utility-types'; +import { ManagedFileTransferState } from './ManagedFileTransferState'; + +/** + * Object which serves as a surrogate interface, encapsulating a Guacamole + * file upload while it is active, allowing it to be detached and + * reattached from different client views. + */ +export class ManagedFileUpload { + + /** + * The current state of the file transfer stream. + */ + transferState: ManagedFileTransferState; + + /** + * The mimetype of the file being transferred. + */ + mimetype?: string; + + /** + * The filename of the file being transferred. + */ + filename?: string; + + /** + * The number of bytes transferred so far. + */ + progress?: number; + + /** + * The total number of bytes in the file. + */ + length?: number; + + /** + * Creates a new ManagedFileUpload. This constructor initializes the properties of the + * new ManagedFileUpload with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManagedFileUpload. + */ + constructor(template: Optional = {}) { + this.transferState = template.transferState || new ManagedFileTransferState(); + this.mimetype = template.mimetype; + this.filename = template.filename; + this.progress = template.progress; + this.length = template.length; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFilesystem.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFilesystem.ts new file mode 100644 index 0000000000..846afa3100 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedFilesystem.ts @@ -0,0 +1,161 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { signal, WritableSignal } from '@angular/core'; +import { Optional } from '../../util/utility-types'; +import { ManagedClient } from './ManagedClient'; + +/** + * Provides the ManagedFilesystem class used by ManagedClient to represent + * available remote filesystems. + * + * Serves as a surrogate interface, encapsulating a Guacamole + * filesystem object while it is active, allowing it to be detached and + * reattached from different client views. + */ +export class ManagedFilesystem { + + /** + * The client that originally received the "filesystem" instruction + * that resulted in the creation of this ManagedFilesystem. + */ + client: ManagedClient; + + /** + * The Guacamole filesystem object, as received via a "filesystem" + * instruction. + */ + object: Guacamole.Object; + + /** + * The declared, human-readable name of the filesystem + */ + name: string; + + /** + * The root directory of the filesystem. + */ + root: ManagedFilesystem.File; + + /** + * The current directory being viewed or manipulated within the + * filesystem. + */ + currentDirectory: WritableSignal; + + /** + * Creates a new ManagedFilesystem. This constructor initializes the properties of the + * new ManagedFilesystem with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManagedFilesystem. + */ + constructor(template: Optional) { + this.client = template.client; + this.object = template.object; + this.name = template.name; + this.root = template.root; + this.currentDirectory = template.currentDirectory || signal(template.root); + } + +} + +export namespace ManagedFilesystem { + + /** + * A file within a ManagedFilesystem. Each ManagedFilesystem.File provides + * sufficient information for retrieval or replacement of the file's + * contents, as well as the file's name and type. + */ + export class File { + + /** + * The mimetype of the data contained within this file. + */ + mimetype: string; + + /** + * The name of the stream representing this files contents within its + * associated filesystem object. + */ + streamName: string; + + /** + * The type of this file. All legal file type strings are defined + * within ManagedFilesystem.File.Type. + */ + type: string; + + /** + * The name of this file. + */ + name?: string; + + /** + * The parent directory of this file. In the case of the root + * directory, this will be null. + */ + parent?: ManagedFilesystem.File; + + /** + * Map of all known files contained within this file by name. This is + * only applicable to directories. + */ + files: WritableSignal>; + + /** + * Creates a new ManagedFilesystem.File object. + * + * @param template + * The object whose properties should be copied within the new + * ManagedFilesystem.File. + */ + constructor(template: Optional) { + this.mimetype = template.mimetype; + this.streamName = template.streamName; + this.type = template.type; + this.name = template.name; + this.parent = template.parent; + this.files = template.files || signal({}); + } + } + +} + +export namespace ManagedFilesystem.File { + + /** + * All legal type strings for a ManagedFilesystem.File. + */ + export enum Type { + /** + * A normal file. As ManagedFilesystem does not currently represent any + * other non-directory types of files, like symbolic links, this type + * string may be used for any non-directory file. + */ + NORMAL = 'NORMAL', + + /** + * A directory. + */ + DIRECTORY = 'DIRECTORY' + } + +} diff --git a/guacamole/src/main/frontend/src/app/client/types/ManagedShareLink.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedShareLink.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/client/types/ManagedShareLink.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedShareLink.ts index 6afa93d0ff..c129a120c8 100644 --- a/guacamole/src/main/frontend/src/app/client/types/ManagedShareLink.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/ManagedShareLink.ts @@ -17,89 +17,79 @@ * under the License. */ +import { SharingProfile } from '../../rest/types/SharingProfile'; +import { UserCredentials } from '../../rest/types/UserCredentials'; + /** - * Provides the ManagedShareLink class used by ManagedClient to represent + * Represents a link which can be used to gain access to an + * active Guacamole connection. + * + * This class is used by ManagedClient to represent * generated connection sharing links. */ -angular.module('client').factory('ManagedShareLink', ['$injector', - function defineManagedShareLink($injector) { +export class ManagedShareLink { - // Required types - var UserCredentials = $injector.get('UserCredentials'); + /** + * The human-readable display name of this share link. + */ + name?: string; /** - * Object which represents a link which can be used to gain access to an - * active Guacamole connection. - * - * @constructor - * @param {ManagedShareLink|Object} [template={}] - * The object whose properties should be copied within the new - * ManagedShareLink. + * The actual URL of the link which can be used to access the shared + * connection. */ - var ManagedShareLink = function ManagedShareLink(template) { + href: string; - // Use empty object by default - template = template || {}; + /** + * The sharing profile which was used to generate the share link. + */ + sharingProfile: SharingProfile; - /** - * The human-readable display name of this share link. - * - * @type String - */ - this.name = template.name; + /** + * The credentials from which the share link was derived. + */ + sharingCredentials: UserCredentials; - /** - * The actual URL of the link which can be used to access the shared - * connection. - * - * @type String - */ + /** + * Creates a new ManagedShareLink. This constructor initializes the properties of the + * new ManagedShareLink with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManagedShareLink. + */ + constructor(template: ManagedShareLink) { + this.name = template.name; this.href = template.href; - - /** - * The sharing profile which was used to generate the share link. - * - * @type SharingProfile - */ this.sharingProfile = template.sharingProfile; - - /** - * The credentials from which the share link was derived. - * - * @type UserCredentials - */ this.sharingCredentials = template.sharingCredentials; - - }; + } /** * Creates a new ManagedShareLink from a set of UserCredentials and the * SharingProfile which was used to generate those UserCredentials. - * - * @param {SharingProfile} sharingProfile + * + * @param sharingProfile * The SharingProfile which was used, via the REST API, to generate the * given UserCredentials. - * - * @param {UserCredentials} sharingCredentials + * + * @param sharingCredentials * The UserCredentials object returned by the REST API in response to a * request to share a connection using the given SharingProfile. * - * @return {ManagedShareLink} + * @return * A new ManagedShareLink object can be used to access the connection * shared via the given SharingProfile and resulting UserCredentials. */ - ManagedShareLink.getInstance = function getInstance(sharingProfile, sharingCredentials) { + static getInstance(sharingProfile: SharingProfile, sharingCredentials: UserCredentials): ManagedShareLink { // Generate new share link using the given profile and credentials return new ManagedShareLink({ - 'name' : sharingProfile.name, - 'href' : UserCredentials.getLink(sharingCredentials), - 'sharingProfile' : sharingProfile, - 'sharingCredentials' : sharingCredentials + name : sharingProfile.name, + href : UserCredentials.getLink(sharingCredentials), + sharingProfile : sharingProfile, + sharingCredentials: sharingCredentials }); - }; - - return ManagedShareLink; - -}]); + } +} diff --git a/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/TranslationResult.ts similarity index 61% rename from guacamole/src/main/frontend/src/app/client/types/TranslationResult.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/TranslationResult.ts index 0a81511bb9..89548c2df4 100644 --- a/guacamole/src/main/frontend/src/app/client/types/TranslationResult.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/client/types/TranslationResult.ts @@ -18,42 +18,34 @@ */ /** - * Provides the TranslationResult class used by the guacTranslate service. This class contains + * Object which represents the result of a translation as returned from + * the guacTranslate service. + * + * Used by the guacTranslate service. This class contains * both the translated message and the translation ID that generated the message, in the case * where it's unknown whether a translation is defined or not. */ - angular.module('client').factory('TranslationResult', [function defineTranslationResult() { +export class TranslationResult { + + /** + * The translation ID. + */ + id?: string; + + /** + * The translated message. + */ + message?: string; /** - * Object which represents the result of a translation as returned from - * the guacTranslate service. + * Creates a new TranslationResult. * - * @constructor - * @param {TranslationResult|Object} [template={}] + * @param template * The object whose properties should be copied within the new * TranslationResult. */ - const TranslationResult = function TranslationResult(template) { - - // Use empty object by default - template = template || {}; - - /** - * The translation ID. - * - * @type {String} - */ + constructor(template: TranslationResult = {}) { this.id = template.id; - - /** - * The translated message. - * - * @type {String} - */ this.message = template.message; - - }; - - return TranslationResult; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/clipboard.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/clipboard.module.ts new file mode 100644 index 0000000000..9868219cf9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/clipboard.module.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ClipboardComponent } from './components/clipboard/clipboard.component'; + + +/** + * The module for code used to manipulate/observe the clipboard. + */ +@NgModule({ + declarations: [ + ClipboardComponent + ], + imports : [ + CommonModule, + TranslocoModule + ], + exports : [ + ClipboardComponent + ] +}) +export class ClipboardModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.html new file mode 100644 index 0000000000..78ed751ab6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.html @@ -0,0 +1,34 @@ + + +
      + +
      +

      {{ 'CLIENT.ACTION_SHOW_CLIPBOARD' | transloco }}

      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.ts new file mode 100644 index 0000000000..cbe71cc2f7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/components/clipboard/clipboard.component.ts @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { AfterViewInit, Component, DestroyRef, ElementRef, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { ClipboardService } from '../../services/clipboard.service'; +import { ClipboardData } from '../../types/ClipboardData'; + +@Component({ + selector: 'guac-clipboard', + templateUrl: './clipboard.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ClipboardComponent implements OnInit, AfterViewInit { + + /** + * The DOM element which will contain the clipboard contents within the + * user interface provided by this directive. We populate the clipboard + * editor via this DOM element rather than updating a model so that we + * are prepared for future support of rich text contents. + */ + @ViewChild('activeClipboardTextarea') element!: ElementRef; + + constructor(private readonly clipboardService: ClipboardService, + private guacEventService: GuacEventService, + private destroyRef: DestroyRef) { + } + + /** + * Whether clipboard contents should be displayed in the clipboard + * editor. If false, clipboard contents will not be displayed until + * the user manually reveals them. + */ + contentsShown = false; + + /** + * Updates clipboard editor to be active. + */ + showContents(): void { + this.contentsShown = true; + window.setTimeout(() => { + this.element.nativeElement.focus(); + }, 0); + } + + /** + * Rereads the contents of the clipboard field, updating the + * ClipboardData object on the scope as necessary. The type of data + * stored within the ClipboardData object will be heuristically + * determined from the HTML contents of the clipboard field. + */ + updateClipboardData() { + // Read contents of clipboard textarea + this.clipboardService.setClipboard(new ClipboardData({ + type: 'text/plain', + data: this.element.nativeElement.value + })); + } + + /** + * Updates the contents of the clipboard editor to the given data. + * + * @param data + * The ClipboardData to display within the clipboard editor for + * editing. + */ + updateClipboardEditor(data: ClipboardData) { + // If the clipboard data is a string, render it as text + if (typeof data.data === 'string') { + this.element.nativeElement.value = data.data; + } + + // Ignore other data types for now + } + + ngOnInit(): void { + // Update remote clipboard if local clipboard changes + this.guacEventService.on('guacClipboard') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ data }) => { + this.updateClipboardEditor(data); + }); + } + + ngAfterViewInit(): void { + + // Init clipboard editor with current clipboard contents + this.clipboardService.getClipboard().then((data) => { + this.updateClipboardEditor(data); + }); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/services/clipboard.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/services/clipboard.service.ts new file mode 100644 index 0000000000..53632e0fc8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/services/clipboard.service.ts @@ -0,0 +1,160 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { SessionStorageEntry, SessionStorageFactory } from '../../storage/session-storage-factory.service'; +import { ClipboardData } from '../types/ClipboardData'; + +/** + * A service for maintaining and accessing clipboard data. If possible, this + * service will leverage the local clipboard. If the local clipboard is not + * available, an internal in-memory clipboard will be used instead. + */ +@Injectable({ + providedIn: 'root' +}) +export class ClipboardService { + + /** + * Getter/setter which retrieves or sets the current stored clipboard + * contents. The stored clipboard contents are strictly internal to + * Guacamole, and may not reflect the local clipboard if local clipboard + * access is unavailable. + */ + private readonly storedClipboardData: SessionStorageEntry = this.sessionStorageFactory.create(new ClipboardData()); + + /** + * The promise associated with the current pending clipboard read attempt. + * If no clipboard read is active, this will be null. + */ + private pendingRead: Promise | null = null; + + + constructor(private readonly sessionStorageFactory: SessionStorageFactory, + private guacEventService: GuacEventService) { + } + + /** + * Sets the local clipboard, if possible, to the given text. + * + * @param data + * The data to assign to the local clipboard. + * + * @return + * A promise that resolves if setting the clipboard was successful, + * and rejects if it failed. + */ + private setLocalClipboard(data: ClipboardData): Promise { + + // Attempt to read the clipboard using the Asynchronous Clipboard + // API, if it's available + if (navigator.clipboard && navigator.clipboard.writeText) { + if (data.type === 'text/plain' && typeof data.data === 'string') { + return navigator.clipboard.writeText(data.data); + } + } + + return Promise.reject(); + } + + /** + * Get the current value of the local clipboard. + * + * @return + * A promise that resolves with the contents of the local clipboard + * if successful, and rejects if it fails. + */ + private getLocalClipboard(): Promise { + + // If the clipboard is already being read, do not overlap the read + // attempts; instead share the result across all requests + if (this.pendingRead) + return this.pendingRead; + + // Create and store the pending promise + this.pendingRead = navigator.clipboard.readText() + .then(text => new ClipboardData({ type: 'text/plain', data: text })) + .finally(() => { + // Clear pending promise after completion + this.pendingRead = null; + }); + + return this.pendingRead; + } + + /** + * Returns the current value of the internal clipboard shared across all + * active Guacamole connections running within the current browser tab. If + * access to the local clipboard is available, the internal clipboard is + * first synchronized with the current local clipboard contents. If access + * to the local clipboard is unavailable, only the internal clipboard will + * be used. + * + * @return + * A promise that will resolve with the contents of the internal + * clipboard, first retrieving those contents from the local clipboard + * if permission to do so has been granted. This promise is always + * resolved. + */ + getClipboard(): Promise { + return this.getLocalClipboard() + .then((data) => this.storedClipboardData(data), () => this.storedClipboardData()); + } + + /** + * Sets the content of the internal clipboard shared across all active + * Guacamole connections running within the current browser tab. If + * access to the local clipboard is available, the local clipboard is + * first set to the provided clipboard content. If access to the local + * clipboard is unavailable, only the internal clipboard will be used. A + * "guacClipboard" event will be broadcast with the assigned data once the + * operation has completed. + * + * @param data + * The data to assign to the clipboard. + * + * @return + * A promise that will resolve after the clipboard content has been + * set. This promise is always resolved. + */ + setClipboard(data: ClipboardData): Promise { + return this.setLocalClipboard(data).finally(() => { + + // Update internal clipboard and broadcast event notifying of + // updated contents + this.storedClipboardData(data); + this.guacEventService.broadcast('guacClipboard', { data }); + + }); + } + + /** + * Resynchronizes the local and internal clipboards, setting the contents + * of the internal clipboard to that of the local clipboard (if local + * clipboard access is granted) and broadcasting a "guacClipboard" event + * with the current internal clipboard contents for consumption by external + * components like the "guacClient" directive. + */ + resyncClipboard = () => { + this.getLocalClipboard().then(data => this.setClipboard(data)); + }; + +} diff --git a/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/styles/clipboard.css similarity index 93% rename from guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/styles/clipboard.css index cec0c26503..a22049c286 100644 --- a/guacamole/src/main/frontend/src/app/clipboard/styles/clipboard.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/styles/clipboard.css @@ -48,16 +48,6 @@ background: url('images/checker.svg'); } -.clipboard-service-target { - position: fixed; - left: -1em; - right: -1em; - width: 1em; - height: 1em; - white-space: pre; - overflow: hidden; -} - .clipboard-editor { position: relative; } diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/types/ClipboardData.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/types/ClipboardData.ts new file mode 100644 index 0000000000..a8e5244d7c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/clipboard/types/ClipboardData.ts @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Arbitrary data which can be contained by the clipboard. + * Used for interchange between the guacClipboard directive, + * clipboardService service, etc. + */ +export class ClipboardData { + /** + * The ID of the ManagedClient handling the remote desktop connection + * that originated this clipboard data, or null if the data originated + * from the clipboard editor or local clipboard. + */ + source: string | undefined; + + /** + * The mimetype of the data currently stored within the clipboard. + */ + type: string; + + /** + * The data currently stored within the clipboard. Depending on the + * nature of the stored data, this may be either a String, a Blob, or a + * File. + */ + data: string | Blob | File; + + /** + * Creates a new ClipboardData object. This constructor initializes the + * properties of the new ClipboardData with the corresponding properties + * of the given template. + * + * @param [template={}] + * The object whose properties should be copied within the new + * ClipboardData. + */ + constructor(template: Partial = {}) { + this.source = template.source || undefined; + this.type = template.type || 'text/plain'; + this.data = template.data || ''; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/events/types/GuacFrontendEventArguments.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/events/types/GuacFrontendEventArguments.ts new file mode 100644 index 0000000000..5b36a8fbea --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/events/types/GuacFrontendEventArguments.ts @@ -0,0 +1,80 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { GuacEventArguments } from 'guacamole-frontend-lib'; +import { ManagedClient } from '../../client/types/ManagedClient'; +import { ClipboardData } from '../../clipboard/types/ClipboardData'; +import { Error } from '../../rest/types/Error'; + +/** + * Defines all possible guacamole events and their payloads that + * can be emitted by the frontend. + */ +export interface GuacFrontendEventArguments extends GuacEventArguments { + + // Global events + guacFatalPageError: { error: any }; + + // Auth events + guacLogin: { authToken: string; }; + guacLoginPending: { parameters: Promise }; + guacLoginFailed: { parameters: Promise; error: Error; }; + guacInvalidCredentials: { parameters: Promise; error: any; }; + guacInsufficientCredentials: { parameters: Promise; error: any; }; + guacLogout: { token: string | null; }; + + // Keyboard events + guacBeforeKeydown: { keysym: number; keyboard: Guacamole.Keyboard; }; + guacBeforeKeyup: { keysym: number; keyboard: Guacamole.Keyboard; }; + + // Mouse events + guacClientMouseDown: { event: Guacamole.Event, client: ManagedClient }; + guacClientMouseUp: { event: Guacamole.Event, client: ManagedClient }; + guacClientMouseMove: { event: Guacamole.Event, client: ManagedClient }; + + // Touch events + guacClientTouchStart: { event: Guacamole.Event, client: ManagedClient }; + guacClientTouchEnd: { event: Guacamole.Event, client: ManagedClient }; + guacClientTouchMove: { event: Guacamole.Event, client: ManagedClient }; + + // File browser events + guacUploadComplete: { filename: string; }; + + // Client events + guacClientFocused: { newFocusedClient: ManagedClient | null; }; + guacMenuShown: { menuShown: boolean }; + guacClientArgumentsUpdated: { focusedClient: ManagedClient | null; }; + guacClientProtocolUpdated: { focusedClient: ManagedClient | null; }; + + // Clipboard events + guacClipboard: { data: ClipboardData }; + + // Player events + guacPlayerLoading: {}; + guacPlayerLoaded: {}; + guacPlayerError: { message: string; }; + guacPlayerProgress: { duration: number; current: number; }; + guacPlayerPlay: {}; + guacPlayerPause: {}; + guacPlayerSeek: { position: number; }; +} + +export type MouseEventName = 'guacClientMouseDown' | 'guacClientMouseUp' | 'guacClientMouseMove'; + +export type TouchEventName = 'guacClientTouchStart' | 'guacClientTouchEnd' | 'guacClientTouchMove'; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.html new file mode 100644 index 0000000000..4a1b82a5c0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.html @@ -0,0 +1,28 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.ts new file mode 100644 index 0000000000..3caffa04ab --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/checkbox-field/checkbox-field.component.ts @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Checkbox field component + */ +@Component({ + selector: 'guac-checkbox-field', + templateUrl: './checkbox-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class CheckboxFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Internal form control that holds the typed value + * of the field. + */ + typedControl: FormControl = new FormControl(null); + + constructor(private destroyRef: DestroyRef) { + super(); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `typedControl` + * form controls. + */ + ngOnInit(): void { + // Set initial value of typed control + const initialValue: boolean = this.control?.value === this.field.options![0]; + this.typedControl.setValue(initialValue, { emitEvent: false }); + + // Update typed value when model is changed + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.typedControl.setValue(value === this.field.options![0], { emitEvent: false }); + }); + + // Update string value in model when typed value is changed + this.typedControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + const newValue = value ? this.field.options![0] : ''; + this.control?.setValue(newValue); + }); + } + + + /** + * Apply disabled state to typed value form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.typedControl, disabled); + + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.html new file mode 100644 index 0000000000..d2501d598d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.html @@ -0,0 +1,30 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.ts new file mode 100644 index 0000000000..58367c482b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/date-field/date-field.component.ts @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component to display date fields. + */ +@Component({ + selector: 'guac-date-field', + templateUrl: './date-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class DateFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Internal form control that holds the typed value + * of the field. + */ + typedControl: FormControl = new FormControl(null); + + constructor(private destroyRef: DestroyRef) { + super(); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `typedControl` + * form controls. + */ + ngOnInit(): void { + // Set initial value of the typed control + const initialValue: Date | null = this.control?.value ? this.parseDate(this.control.value) : null; + this.typedControl.setValue(initialValue, { emitEvent: false }); + + // Update typed value when model is changed + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + const newTypedValue = value ? this.parseDate(value) : null; + this.typedControl.setValue(newTypedValue, { emitEvent: false }); + }); + + // Update string value in model when typed value is changed + this.typedControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(typedValue => { + const newValue = typedValue ? formatDate(typedValue, 'yyyy-MM-dd', 'en-US', 'UTC') : ''; + this.control?.setValue(newValue); + }); + } + + /** + * Apply disabled state to typed value form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.typedControl, disabled); + + } + } + + /** + * Parses the date components of the given string into a Date with only the + * date components set. The resulting Date will be in the UTC timezone, + * with the time left as midnight. The input string must be in the format + * YYYY-MM-DD (zero-padded). + * + * @param str + * The date string to parse. + * + * @returns + * A Date object, in the UTC timezone, with only the date components + * set. + */ + parseDate(str: string): Date | null { + + // Parse date, return blank if invalid + const parsedDate = new Date(str + 'T00:00Z'); + if (isNaN(parsedDate.getTime())) + return null; + + return parsedDate; + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.html new file mode 100644 index 0000000000..0829db24ed --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.html @@ -0,0 +1,31 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.spec.ts new file mode 100644 index 0000000000..55e2e92fae --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { EmailFieldComponent } from './email-field.component'; + +describe('EmailFieldComponent', () => { + let component: EmailFieldComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [EmailFieldComponent] + }); + fixture = TestBed.createComponent(EmailFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.ts new file mode 100644 index 0000000000..e44a40e27a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/email-field/email-field.component.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +@Component({ + selector: 'guac-email-field', + templateUrl: './email-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class EmailFieldComponent extends FormFieldBaseComponent implements OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * TODO: To which value is this a reference in the AngularJS code? + */ + readOnly = false; + + /** + * Apply disabled state to typed form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field-base/form-field-base.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field-base/form-field-base.component.ts new file mode 100644 index 0000000000..0d13729b1a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field-base/form-field-base.component.ts @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { FormFieldComponentData } from 'guacamole-frontend-ext-lib'; +import { ManagedClient } from '../../../client/types/ManagedClient'; +import { canonicalize } from '../../../locale/service/translation.service'; +import { Field } from '../../../rest/types/Field'; + + +/** + * Produces the translation string for the given field option + * value. The translation string will be of the form: + * + * NAMESPACE.FIELD_OPTION_NAME_VALUE + * + * where NAMESPACE is the provided namespace, + * NAME is the field name transformed + * via canonicalize(), and + * VALUE is the option value transformed via + * canonicalize() + * + * @param field + * The field to which the option belongs. + * + * @param namespace + * The namespace to use when generating the translation string. + * + * @param value + * The name of the option value. + * + * @returns + * The translation string which produces the translated name of the + * value specified. + */ +export const getFieldOption = (field: Field | undefined, namespace?: string, value?: string): string => { + + // If no field, or no value, then no corresponding translation string + if (!field || !field.name) + return ''; + + return canonicalize(namespace || 'MISSING_NAMESPACE') + + '.FIELD_OPTION_' + canonicalize(field.name) + + '_' + canonicalize(value || 'EMPTY'); + +}; + +/** + * Base class for form field components. + */ +@Component({ + 'template': '', + standalone: false +}) +export abstract class FormFieldBaseComponent implements FormFieldComponentData { + + /** + * The translation namespace of the translation strings that will + * be generated for this field. This namespace is absolutely + * required. If this namespace is omitted, all generated + * translation strings will be placed within the MISSING_NAMESPACE + * namespace, as a warning. + */ + @Input() namespace: string | undefined; + + /** + * The field to display. + */ + @Input({ required: true }) field!: Field; + + /** + * The form control which contains this fields current value. When this + * field changes, the property will be updated accordingly. + */ + @Input() control?: FormControl; + + /** + * Whether this field should be rendered as disabled. By default, + * form fields are enabled. + */ + @Input() disabled = false; + + /** + * Whether this field should be focused. + */ + @Input() focused = false; + + /** + * The client associated with this form field, if any. + */ + @Input() client?: ManagedClient; + + /** + * An ID value which is reasonably likely to be unique relative to + * other elements on the page. This ID should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + abstract fieldId?: string; + + /** + * Sets the disabled state of the given control. + * @param control + * The control whose disabled state should be set. + * @param disabled + * Whether the control should be disabled. + */ + protected setDisabledState(control: FormControl | undefined, disabled: boolean): void { + if (disabled) { + control?.disable({ emitEvent: false }); + } else { + control?.enable({ emitEvent: false }); + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.html new file mode 100644 index 0000000000..0ea5d6c45b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.html @@ -0,0 +1,31 @@ + + +
      + + +
      + +
      + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.ts new file mode 100644 index 0000000000..4d0ed1006a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form-field/form-field.component.ts @@ -0,0 +1,189 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + AfterViewInit, + ChangeDetectorRef, + Component, + ComponentRef, + ElementRef, + OnChanges, + SimpleChanges, + ViewChild, + ViewContainerRef, + ViewEncapsulation +} from '@angular/core'; +import { canonicalize } from '../../../locale/service/translation.service'; +import { LogService } from '../../../util/log.service'; +import { FormService } from '../../service/form.service'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * A component that allows editing of a field. + */ +@Component({ + selector: 'guac-form-field', + templateUrl: './form-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class FormFieldComponent extends FormFieldBaseComponent implements AfterViewInit, OnChanges { + + /** + * Reference to the field content element. + */ + @ViewChild('formField') fieldContentRef!: ElementRef; + + /** + * The element which should contain any compiled field content. The + * actual content of a field is dynamically determined by its type. + */ + fieldContent?: Element; + + /** + * View container reference which is used to dynamically create and insert + * field components. + */ + @ViewChild('formField', { read: ViewContainerRef }) viewContainer!: ViewContainerRef; + + /** + * Reference to the dynamically created field component. + */ + fieldComponent: ComponentRef | undefined; + + + /** + * An ID value which is reasonably likely to be unique relative to + * other elements on the page. This ID should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + fieldId: string = 'guac-field-XXXXXXXXXXXXXXXX'.replace(/X/g, function getRandomCharacter() { + return Math.floor(Math.random() * 36).toString(36); + }) + '-' + new Date().getTime().toString(36); + + + constructor(private formService: FormService, + private log: LogService, + private cdr: ChangeDetectorRef) { + super(); + } + + /** + * Updates the field content and passes changes to the field component. + */ + async ngOnChanges(changes: SimpleChanges): Promise { + + // Update field contents when field definition is changed + if (changes['field']) { + await this.insertFieldElement(); + } + + // Pass changes to the specific field component if it exists and implements + // the ngOnChanges() lifecycle method + this.fieldComponent?.instance.ngOnChanges?.(changes); + } + + async ngAfterViewInit(): Promise { + this.fieldContent = this.fieldContentRef.nativeElement; + + // Set up form service + this.formService.setChangeDetectorRef(this.cdr); + this.formService.setViewContainer(this.viewContainer); + + // Initially insert field element + await this.insertFieldElement(); + } + + /** + * Produces the translation string for the header of the current + * field. The translation string will be of the form: + * + * NAMESPACE.FIELD_HEADER_NAME + * + * where NAMESPACE is the namespace provided to the + * directive and NAME is the field name transformed + * via canonicalize(). + * + * @returns + * The translation string which produces the translated header + * of the field. + */ + getFieldHeader(): string { + + // If no field, or no name, then no header + if (!this.field || !this.field.name) + return ''; + + return canonicalize(this.namespace || 'MISSING_NAMESPACE') + + '.FIELD_HEADER_' + canonicalize(this.field.name); + + } + + /** + * Returns an object as would be provided to the ngClass directive + * that defines the CSS classes that should be applied to this + * field. + * + * @return + * The ngClass object defining the CSS classes for the current + * field. + */ + getFieldClasses(): Record { + return this.formService.getClasses('labeled-field-', this.field, { + empty: !this.control?.value + }); + } + + /** + * Returns whether the current field should be displayed. + * + * @returns + * true if the current field should be displayed, false + * otherwise. + */ + isFieldVisible(): boolean { + return this.fieldContent?.hasChildNodes() || false; + } + + /** + * Inserts the element corresponding to the current field into the DOM. + */ + async insertFieldElement(): Promise { + + if (!this.fieldContent) { + return; + } + + // Reset contents + this.fieldContent.innerHTML = ''; + + // Append field content + if (this.field) { + + await this.formService.insertFieldElement(this.fieldContent, this.field.type, this) + // Store reference to the created field component + .then((fieldElement) => this.fieldComponent = fieldElement) + .catch(() => { + // fieldCreationFailed + this.log.warn('Failed to retrieve field with type "' + this.field.type + '"'); + }); + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.html new file mode 100644 index 0000000000..5208041185 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.html @@ -0,0 +1,44 @@ + + +
      +
      + + +

      {{ getSectionHeader(form) | transloco }}

      + + +
      + + + + +
      + +
      +
      diff --git a/guacamole/src/main/frontend/src/app/form/controllers/numberFieldController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.spec.ts similarity index 59% rename from guacamole/src/main/frontend/src/app/form/controllers/numberFieldController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.spec.ts index 02da81bda2..6e46612760 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/numberFieldController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.spec.ts @@ -17,21 +17,24 @@ * under the License. */ +import { ComponentFixture, TestBed } from '@angular/core/testing'; -/** - * Controller for number fields. - */ -angular.module('form').controller('numberFieldController', ['$scope', - function numberFieldController($scope) { +import { FormComponent } from './form.component'; - // Update typed value when model is changed - $scope.$watch('model', function modelChanged(model) { - $scope.typedValue = (model ? Number(model) : null); - }); +describe('FormComponent', () => { + let component: FormComponent; + let fixture: ComponentFixture; - // Update string value in model when typed value is changed - $scope.$watch('typedValue', function typedValueChanged(typedValue) { - $scope.model = ((typedValue || typedValue === 0) ? typedValue.toString() : ''); + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [FormComponent] + }); + fixture = TestBed.createComponent(FormComponent); + component = fixture.componentInstance; + fixture.detectChanges(); }); -}]); + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.ts new file mode 100644 index 0000000000..95e5741e47 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/form/form.component.ts @@ -0,0 +1,252 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import _ from 'lodash'; +import { ManagedClient } from '../../../client/types/ManagedClient'; +import { canonicalize } from '../../../locale/service/translation.service'; +import { Field } from '../../../rest/types/Field'; +import { Form } from '../../../rest/types/Form'; +import { FormService } from '../../service/form.service'; + +/** + * A component that allows editing of a collection of fields. + */ +@Component({ + selector: 'guac-form', + templateUrl: './form.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class FormComponent implements OnChanges { + + /** + * The translation namespace of the translation strings that will + * be generated for all fields. This namespace is absolutely + * required. If this namespace is omitted, all generated + * translation strings will be placed within the MISSING_NAMESPACE + * namespace, as a warning. + */ + @Input() namespace?: string | undefined; + + /** + * The form content to display. This may be a form, an array of + * forms, or a simple array of fields. + */ + @Input() content?: Form[] | Form | Field[] | Field; + + /** + * A form group which will receive all field values. Each field value + * will be assigned to a form control of this group having the same + * name. + */ + @Input() modelFormGroup?: FormGroup; + + /** + * Whether the contents of the form should be restricted to those + * fields/forms which match properties defined within the given + * model object. By default, all fields will be shown. + */ + @Input() modelOnly = false; + + /** + * Whether the contents of the form should be rendered as disabled. + * By default, form fields are enabled. + */ + @Input() disabled = false; + + /** + * The name of the field to be focused, if any. + */ + @Input() focusedField: string | undefined; + + /** + * The client associated with this form, if any. + * + * NOTE: If the provided client has any managed arguments in the + * pending state, any fields with the same name rendered by this + * form will be disabled. The fields will be re-enabled when guacd + * sends an updated argument with a the same name. + */ + @Input() client: ManagedClient | undefined; + + /** + * The array of all forms to display. + */ + forms: Form[] = []; + + constructor(private formService: FormService) { + } + + /** + * Produces the translation string for the section header of the + * given form. The translation string will be of the form: + * + * NAMESPACE.SECTION_HEADER_NAME + * + * where NAMESPACE is the namespace provided to the + * directive and NAME is the form name transformed + * via canonicalize(). + * + * @param form + * The form for which to produce the translation string. + * + * @returns + * The translation string which produces the translated header + * of the form. + */ + getSectionHeader(form: Form): string { + + // If no form, or no name, then no header + if (!form || !form.name) + return ''; + + return canonicalize(this.namespace || 'MISSING_NAMESPACE') + + '.SECTION_HEADER_' + canonicalize(form.name); + + } + + /** + * Returns an object as would be provided to the ngClass directive + * that defines the CSS classes that should be applied to the given + * form. + * + * @param form + * The form to generate the CSS classes for. + * + * @return + * The ngClass object defining the CSS classes for the given + * form. + */ + getFormClasses(form: Form): Record { + return this.formService.getClasses('form-', form); + } + + ngOnChanges(changes: SimpleChanges): void { + + // Produce set of forms from any given content + if (changes['content']) { + + const content: Form[] | Form | Field[] | Field = changes['content'].currentValue; + + // Transform content to always be an array of forms + this.forms = this.formService.asFormArray(content); + + } + + } + + /** + * Returns whether the given field should be focused or not. + * + * @param field + * The field to check. + * + * @returns + * true if the given field should be focused, false otherwise. + */ + isFocused(field: Field): boolean { + return field && (field.name === this.focusedField); + } + + /** + * Returns whether the given field should be displayed to the + * current user. + * + * @param field + * The field to check. + * + * @returns + * true if the given field should be visible, false otherwise. + */ + isVisible(field: Field): boolean { + + // All fields are visible if contents are not restricted to + // model properties only + if (!this.modelOnly) + return true; + + // Otherwise, fields are only visible if they are present + // within the form group + return field && !!this.modelFormGroup && !!this.modelFormGroup.get(field.name); + + } + + /** + * Returns whether the given field should be disabled (read-only) + * when presented to the current user. + * + * @param field + * The field to check. + * + * @returns + * true if the given field should be disabled, false otherwise. + */ + isDisabled(field: Field): boolean { + + /* + * The field is disabled if either the form as a whole is disabled, + * or if a client is provided to the directive, and the field is + * marked as pending. + */ + return !!(this.disabled || + _.get(this.client, ['arguments', field.name, 'pending'])); + } + + /** + * Returns whether at least one of the given fields should be + * displayed to the current user. + * + * @param fields + * The array of fields to check. + * + * @returns + * true if at least one field within the given array should be + * visible, false otherwise. + */ + containsVisible(fields: Field[]): boolean { + + // If fields are defined, check whether at least one is visible + if (fields) { + for (let i = 0; i < fields.length; i++) { + if (this.isVisible(fields[i])) + return true; + } + } + + // Otherwise, there are no visible fields + return false; + + } + + /** + * Returns the form control that is associated to the given field. + * + * @param field + * The field to get the control for. + * + * @returns + * The form control associated to the given field. If no form + * group has been provided, a dummy control will be returned. + */ + getControl(field: Field): FormControl { + return this.modelFormGroup ? this.modelFormGroup.get(field.name) as FormControl : new FormControl('not found'); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.html new file mode 100644 index 0000000000..e50c19cce0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.html @@ -0,0 +1,31 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.ts new file mode 100644 index 0000000000..195695e0c7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/guac-input-color/guac-input-color.component.ts @@ -0,0 +1,120 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, ElementRef, Input, ViewChild, ViewEncapsulation } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { ColorPickerService } from '../../service/color-picker.service'; + +/** + * A component which implements a color input field. If the underlying color + * picker implementation cannot be used due to a lack of browser support, this + * directive will become read-only, functioning essentially as a color preview. + * + * @see colorPickerService + */ +@Component({ + selector: 'guac-input-color', + templateUrl: './guac-input-color.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacInputColorComponent { + + /** + * The current selected color value, in standard 6-digit hexadecimal + * RGB notation. When the user selects a different color using this + * directive, this value will be updated accordingly. + */ + @Input({ required: true }) control!: FormControl; + + /** + * An optional array of colors to include within the color picker as a + * convenient selection of pre-defined colors. The colors within the + * array must be in standard 6-digit hexadecimal RGB notation. + */ + @Input() palette?: string[]; + + /** + * Reference to div element that should be used as a button to open the color picker. + */ + @ViewChild('colorPicker', { static: true }) colorInput!: ElementRef; + + /** + * Inject required services. + */ + constructor(private colorPickerService: ColorPickerService) { + } + + /** + * Returns whether the underlying color picker can be used. + * + * @returns + * true if the underlying color picker can be used, false otherwise. + */ + isColorPickerAvailable(): boolean { + return this.colorPickerService.isAvailable(); + } + + /** + * Returns whether the color currently selected is "dark" in the sense + * that the color white will have higher contrast against it than the + * color black. + * + * @returns + * true if the currently selected color is relatively dark (white + * text would provide better contrast than black), false otherwise. + */ + isDark(): boolean { + + // Assume not dark if color is invalid or undefined + const rgb = this.control.value && /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(this.control.value); + if (!rgb) + return false; + + // Parse color component values as hexadecimal + const red = parseInt(rgb[1], 16); + const green = parseInt(rgb[2], 16); + const blue = parseInt(rgb[3], 16); + + // Convert RGB to luminance in HSL space (as defined by the + // relative luminance formula given by the W3C for accessibility) + const luminance = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + + // Consider the background to be dark if white text over that + // background would provide better contrast than black + return luminance <= 153; // 153 is the component value 0.6 converted from 0-1 to the 0-255 range + + } + + /** + * Prompts the user to choose a color by displaying a color selection + * dialog. If the user chooses a color, this directive's model is + * automatically updated. If the user cancels the dialog, the model is + * left untouched. + */ + selectColor(): void { + this.colorPickerService.selectColor(this.colorInput.nativeElement, this.control.value, this.palette) + .then((color: string) => { + this.control.setValue(color); + }).catch(() => { + // Do nothing if the user cancels the dialog + }); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.html new file mode 100644 index 0000000000..c39d6a32c9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.html @@ -0,0 +1,28 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.ts new file mode 100644 index 0000000000..f464805413 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/language-field/language-field.component.ts @@ -0,0 +1,81 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { LanguageService } from '../../../rest/service/language.service'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component to display language fields. The language field type allows the + * user to select a language from the set of languages supported by the + * Guacamole web application. + */ +@Component({ + selector: 'guac-language-field', + templateUrl: './language-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class LanguageFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * A map of all available language keys to their human-readable + * names. + */ + languages?: Record = undefined; + + constructor(private languageService: LanguageService) { + super(); + } + + ngOnInit(): void { + + // Retrieve defined languages + this.languageService.getLanguages().subscribe(languages => { + this.languages = languages; + }); + + // Interpret undefined/null as empty string + this.control?.valueChanges.subscribe(value => { + if (!value && value !== '') + this.control?.setValue('', { emitEvent: false }); + }); + } + + /** + * Apply disabled state to form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.html new file mode 100644 index 0000000000..3bcb779566 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.html @@ -0,0 +1,28 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.ts new file mode 100644 index 0000000000..27acbb07fe --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/number-field/number-field.component.ts @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component to display number fields. + */ +@Component({ + selector: 'guac-number-field', + templateUrl: './number-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class NumberFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Internal form control that holds the typed value + * of the field. + */ + typedControl: FormControl = new FormControl(null); + + + constructor(private destroyRef: DestroyRef) { + super(); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `typedControl` + * form controls. + */ + ngOnInit(): void { + // Set initial value of typed control + const initialValue: number | null = this.control?.value ? Number(this.control.value) : null; + this.typedControl.setValue(initialValue, { emitEvent: false }); + + // Update typed value when model is changed + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + this.typedControl.setValue((value ? Number(value) : null), { emitEvent: false }); + }); + + // Update string value in model when typed value is changed + this.typedControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(typedValue => { + this.control?.setValue(((typedValue || typedValue === 0) ? typedValue.toString() : '')); + }); + } + + /** + * Apply disabled state to typed form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.typedControl, disabled); + + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.html new file mode 100644 index 0000000000..edea48e13e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.html @@ -0,0 +1,31 @@ + + +
      + +
      +
      diff --git a/guacamole/src/main/frontend/src/app/form/controllers/passwordFieldController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.ts similarity index 56% rename from guacamole/src/main/frontend/src/app/form/controllers/passwordFieldController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.ts index 0725eb4fc1..5beea2114d 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/passwordFieldController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/password-field/password-field.component.ts @@ -17,56 +17,82 @@ * under the License. */ +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; /** - * Controller for password fields. + * Component to display password fields. */ -angular.module('form').controller('passwordFieldController', ['$scope', - function passwordFieldController($scope) { +@Component({ + selector: 'guac-password-field', + templateUrl: './password-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class PasswordFieldComponent extends FormFieldBaseComponent implements OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; /** * The type to use for the input field. By default, the input field will * have the type 'password', and thus will be masked. * - * @type String * @default 'password' */ - $scope.passwordInputType = 'password'; + passwordInputType = 'password'; + + /** + * Apply disabled state to form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + + } + } /** * Returns a string which describes the action the next call to * togglePassword() will have. * - * @return {String} + * @return * A string which describes the action the next call to * togglePassword() will have. */ - $scope.getTogglePasswordHelpText = function getTogglePasswordHelpText() { + getTogglePasswordHelpText(): string { // If password is hidden, togglePassword() will show the password - if ($scope.passwordInputType === 'password') + if (this.passwordInputType === 'password') return 'FORM.HELP_SHOW_PASSWORD'; // If password is shown, togglePassword() will hide the password return 'FORM.HELP_HIDE_PASSWORD'; - }; + } /** * Toggles visibility of the field contents, if this field is a * password field. Initially, password contents are masked * (invisible). */ - $scope.togglePassword = function togglePassword() { + togglePassword() { // If password is hidden, show the password - if ($scope.passwordInputType === 'password') - $scope.passwordInputType = 'text'; + if (this.passwordInputType === 'password') + this.passwordInputType = 'text'; // If password is shown, hide the password else - $scope.passwordInputType = 'password'; + this.passwordInputType = 'password'; - }; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.html new file mode 100644 index 0000000000..2bbd0a8d97 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.html @@ -0,0 +1,26 @@ + + +
      +
      +

      + {{ field.translatableMessage?.key | transloco: { value: field.translatableMessage?.variables } }} +

      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.ts new file mode 100644 index 0000000000..522a89f8b4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/redirect-field/redirect-field.component.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Component, Inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component for the redirect field, which redirects the user to the provided + * URL. + */ +@Component({ + selector: 'guac-redirect-field', + templateUrl: './redirect-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class RedirectFieldComponent extends FormFieldBaseComponent implements OnInit { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + constructor(@Inject(DOCUMENT) private document: Document) { + super(); + } + + /** + * Redirect the user to the provided URL. + */ + ngOnInit(): void { + this.document.location.href = (this.field as any).redirectUrl; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.html new file mode 100644 index 0000000000..531cae96fc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.html @@ -0,0 +1,30 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.spec.ts new file mode 100644 index 0000000000..856ad0d2fd --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { SelectFieldComponent } from './select-field.component'; + +describe('SelectFieldComponent', () => { + let component: SelectFieldComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [SelectFieldComponent] + }); + fixture = TestBed.createComponent(SelectFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.ts new file mode 100644 index 0000000000..c197f35cb7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/select-field/select-field.component.ts @@ -0,0 +1,58 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent, getFieldOption } from '../form-field-base/form-field-base.component'; + +/** + * Component for select fields. + */ +@Component({ + selector: 'guac-select-field', + templateUrl: './select-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class SelectFieldComponent extends FormFieldBaseComponent implements OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * @borrows FormFieldBaseComponent.getFieldOption + */ + protected readonly getFieldOption = getFieldOption; + + /** + * Apply disabled state to form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.html new file mode 100644 index 0000000000..6ac449f5e8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.html @@ -0,0 +1,102 @@ + + +
      + + + + + +
      + + +
      + + + {{ 'COLOR_SCHEME.FIELD_HEADER_FOREGROUND' | transloco }} + +
      + + +
      + + {{ 'COLOR_SCHEME.FIELD_HEADER_BACKGROUND' | transloco }} + +
      + + +
      +
      + + + {{ index }} + +
      +
      + + +
      +
      + + {{ index }} + +
      +
      + +
      + + +

      + {{ 'COLOR_SCHEME.SECTION_HEADER_DETAILS' | transloco }} + {{ 'COLOR_SCHEME.ACTION_SHOW_DETAILS' | transloco }} + {{ 'COLOR_SCHEME.ACTION_HIDE_DETAILS' | transloco }} +

      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.ts new file mode 100644 index 0000000000..24ee967026 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/terminal-color-scheme-field/terminal-color-scheme-field.component.ts @@ -0,0 +1,287 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormControl, FormGroup } from '@angular/forms'; +import _ from 'lodash'; +import { ColorScheme } from '../../types/ColorScheme'; +import { FormFieldBaseComponent, getFieldOption } from '../form-field-base/form-field-base.component'; + +/** + * The string value which is assigned to selectedColorScheme if a custom + * color scheme is selected. + */ +const CUSTOM_COLOR_SCHEME = 'custom'; + +/** + * The palette indices of all colors which are considered low-intensity. + */ +const lowIntensity = [0, 1, 2, 3, 4, 5, 6, 7] as const; + +/** + * The palette indices of all colors which are considered high-intensity. + */ +const highIntensity = [8, 9, 10, 11, 12, 13, 14, 15] as const; + +/** + * The palette indices of all colors. + */ +const terminalColorIndices = [...lowIntensity, ...highIntensity] as const; + +/** + * Type definition for all possible values of a terminal color palette index. + */ +type TerminalColorIndex = typeof terminalColorIndices[number]; + +/** + * Component for terminal color scheme fields. + */ +@Component({ + selector: 'guac-terminal-color-scheme-field', + templateUrl: './terminal-color-scheme-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TerminalColorSchemeFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * The currently selected color scheme. If a pre-defined color scheme is + * selected, this will be the connection parameter value associated with + * that color scheme. If a custom color scheme is selected, this will be + * the string "custom". + */ + selectedColorScheme: FormControl = new FormControl(''); + + /** + * The array of colors to include within the color picker as pre-defined + * options for convenience. + */ + defaultPalette: string[] = new ColorScheme().colors; + + /** + * Whether the raw details of the custom color scheme should be shown. By + * default, such details are hidden. + * + * @default false + */ + detailsShown = false; + + /** + * Form group which contains the form controls for the custom color scheme. + */ + customColorSchemeFormGroup!: FormGroup<{ + background: FormControl, + foreground: FormControl, + colors: FormGroup>> + }>; + + /** + * The form control for the background color of the custom color scheme. + */ + get customColorSchemeBackground(): FormControl { + return this.customColorSchemeFormGroup.get('background') as FormControl; + } + + /** + * The form control for the foreground color of the custom color scheme. + */ + get customColorSchemeForeground(): FormControl { + return this.customColorSchemeFormGroup.get('foreground') as FormControl; + } + + /** + * The form control for the color at the given palette index of the custom + * color scheme. + * @param index + * The palette index of the color to retrieve. + * + * @returns + * The form control for the color at the given palette index. + */ + customColorSchemeColorFormControl(index: TerminalColorIndex): FormControl { + return this.customColorSchemeFormGroup.get('colors')?.get(index.toString()) as FormControl; + } + + constructor(private destroyRef: DestroyRef, private fb: FormBuilder) { + super(); + + // Initialize empty form group for custom color scheme colors + const colorFormControls: Record> = {} as any; + + // Create form controls for each color + terminalColorIndices.forEach((index: TerminalColorIndex) => { + colorFormControls[index] = this.fb.control(''); + }); + + // Create form group for custom color scheme + this.customColorSchemeFormGroup = this.fb.group({ + background: this.fb.control(''), + foreground: this.fb.control(''), + colors : this.fb.group(colorFormControls) + }); + + } + + /** + * Sets the values of the form controls to the given color scheme. + * + * @param customColorScheme + * The custom color scheme to set. + */ + private setCustomColorScheme(customColorScheme: ColorScheme): void { + + this.customColorSchemeFormGroup.setValue({ + background: customColorScheme.background, + foreground: customColorScheme.foreground, + // Transform color array into object with indices as keys + colors: terminalColorIndices.reduce((acc, index: TerminalColorIndex) => { + acc[index] = customColorScheme.colors[index]; + return acc; + }, {} as Record) + }, { onlySelf: true, emitEvent: false }); + } + + /** + * Constructs a {@link ColorScheme} object from the values of the form controls. + * + * @returns + * Color scheme object representing the current color scheme. + */ + private getCustomColorScheme(): ColorScheme { + return new ColorScheme({ + background: this.customColorSchemeBackground.value, + foreground: this.customColorSchemeForeground.value, + colors : terminalColorIndices.map(index => this.customColorSchemeColorFormControl(index).value) + }); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `selectedColorScheme` + * form controls. + */ + ngOnInit(): void { + + /** + * Updates the component data based on the given string value from the form control input. + */ + const updateComponentData = (value: string | null): void => { + if (this.selectedColorScheme.value === CUSTOM_COLOR_SCHEME || (value && !_.includes(this.field.options, value))) { + this.setCustomColorScheme(ColorScheme.fromString(value as string)); + this.selectedColorScheme.setValue(CUSTOM_COLOR_SCHEME, { emitEvent: false }); + } else + this.selectedColorScheme.setValue(value || '', { emitEvent: false }); + }; + + // Set initial value of the selected color scheme control + const initialValue: string | null = this.control?.value === undefined ? null : this.control.value; + updateComponentData(initialValue); + + // Keep selected color scheme and custom color scheme in sync with changes + // to model + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + updateComponentData(value); + }); + + // Keep model in sync with changes to selected color scheme + this.selectedColorScheme.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(selectedColorScheme => { + if (!selectedColorScheme) + this.control?.setValue(''); + else if (selectedColorScheme === CUSTOM_COLOR_SCHEME) + this.control?.setValue(ColorScheme.toString(this.getCustomColorScheme())); + else + this.control?.setValue(selectedColorScheme); + }); + + // Keep model in sync with changes to custom color scheme + this.customColorSchemeFormGroup?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + if (this.selectedColorScheme.value === CUSTOM_COLOR_SCHEME) + this.control?.setValue(ColorScheme.toString(this.getCustomColorScheme())); + }); + } + + /** + * Apply disabled state to selected color scheme controls. + */ + ngOnChanges(changes: SimpleChanges): void { + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.selectedColorScheme, disabled); + this.setDisabledState(this.control, disabled); + + } + + } + + /** + * Returns whether a custom color scheme has been selected. + * + * @returns + * true if a custom color scheme has been selected, false otherwise. + */ + isCustom(): boolean { + return this.selectedColorScheme.value === CUSTOM_COLOR_SCHEME; + } + + /** + * Shows the raw details of the custom color scheme. If the details are + * already shown, this function has no effect. + */ + showDetails(): void { + this.detailsShown = true; + } + + /** + * Hides the raw details of the custom color scheme. If the details are + * already hidden, this function has no effect. + */ + hideDetails(): void { + this.detailsShown = false; + } + + /** + * Make getFieldOption available in template. + */ + readonly getFieldOption = getFieldOption; + + /** + * Make lowIntensity available in template. + */ + readonly lowIntensity = lowIntensity; + + /** + * Make highIntensity available in template. + */ + readonly highIntensity = highIntensity; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.html new file mode 100644 index 0000000000..c725049769 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.html @@ -0,0 +1,28 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.spec.ts new file mode 100644 index 0000000000..a25d672230 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { TextAreaFieldComponent } from './text-area-field.component'; + +describe('TestAreaFieldComponent', () => { + let component: TextAreaFieldComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [TextAreaFieldComponent] + }); + fixture = TestBed.createComponent(TextAreaFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.ts new file mode 100644 index 0000000000..2cc6d7ccb0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-area-field/text-area-field.component.ts @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component for textarea fields. + */ +@Component({ + selector: 'guac-text-area-field', + templateUrl: './text-area-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TextAreaFieldComponent extends FormFieldBaseComponent implements OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Apply disabled state to form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.html new file mode 100644 index 0000000000..4e74fd7d9a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.html @@ -0,0 +1,35 @@ + + +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.ts new file mode 100644 index 0000000000..55ab397cc5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/text-field/text-field.component.ts @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent, getFieldOption } from '../form-field-base/form-field-base.component'; + +/** + * Component for text fields. + */ +@Component({ + selector: 'guac-text-field', + templateUrl: './text-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TextFieldComponent extends FormFieldBaseComponent implements OnChanges, OnInit { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * The ID of the datalist element that should be associated with the text + * field, providing a set of known-good values. If no such values are + * defined, this will be null. + */ + dataListId: string | null = null; + + ngOnInit(): void { + // Generate unique ID for datalist, if applicable + if (this.field.options && this.field.options.length) + this.dataListId = this.fieldId + '-datalist'; + } + + /** + * Apply disabled state to form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + + } + } + + /** + * @borrows FormFieldBaseComponent.getFieldOption + */ + protected readonly getFieldOption = getFieldOption; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.html new file mode 100644 index 0000000000..fa13a7e6f8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.html @@ -0,0 +1,30 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.ts new file mode 100644 index 0000000000..2c881e3bde --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-field/time-field.component.ts @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component for time fields. + */ +@Component({ + selector: 'guac-time-field', + templateUrl: './time-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TimeFieldComponent extends FormFieldBaseComponent implements OnInit, OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Internal form control that holds the typed value + * of the field. + */ + typedControl: FormControl = new FormControl(null); + + constructor(private destroyRef: DestroyRef) { + super(); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `typedControl` + * form controls. + */ + ngOnInit(): void { + + // Set initial value of typed control + const initialValue: Date | null = this.control?.value ? this.parseTime(this.control.value) : null; + this.typedControl.setValue(initialValue, { emitEvent: false }); + + // Update typed value when model is changed + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + const newTypedValue = value ? this.parseTime(value) : null; + this.typedControl.setValue(newTypedValue, { emitEvent: false }); + }); + + // Update string value in model when typed value is changed + this.typedControl.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(typedValue => { + const newValue = typedValue ? formatDate(typedValue, 'HH:mm:ss', 'en-US', 'UTC') : ''; + this.control?.setValue(newValue); + }); + } + + /** + * Apply disabled state to typed value form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.typedControl, disabled); + + } + } + + /** + * Parses the time components of the given string into a Date with only the + * time components set. The resulting Date will be in the UTC timezone, + * with the date left as 1970-01-01. The input string must be in the format + * HH:MM:SS (zero-padded, 24-hour). + * + * @param str + * The time string to parse. + * + * @returns + * A Date object, in the UTC timezone, with only the time components + * set. + */ + parseTime(str: string): Date | null { + + // Parse time, return blank if invalid + const parsedDate = new Date('1970-01-01T' + str + 'Z'); + if (isNaN(parsedDate.getTime())) + return null; + + return parsedDate; + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.html new file mode 100644 index 0000000000..e5878040a8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.html @@ -0,0 +1,42 @@ + + +
      + + + + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.ts new file mode 100644 index 0000000000..7a79ba3611 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/time-zone-field/time-zone-field.component.ts @@ -0,0 +1,797 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl } from '@angular/forms'; +import { SortService } from '../../../list/services/sort.service'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +/** + * Component for time zone fields. Time zone fields use IANA time zone + * database identifiers as the standard representation for each supported time + * zone. These identifiers are also legal Java time zone IDs. + */ +@Component({ + selector: 'guac-time-zone-field', + templateUrl: './time-zone-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TimeZoneFieldComponent extends FormFieldBaseComponent implements OnChanges, OnInit { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * The name of the region currently selected. The selected region narrows + * which time zones are selectable. + */ + region: FormControl = new FormControl('', { nonNullable: true }); + + /** + * All selectable regions. + */ + regions: string[] = []; + + /** + * Direct mapping of all time zone IDs to the region containing that ID. + */ + timeZoneRegions!: Record; + + /** + * Map of regions to the currently selected time zone for that region. + * Initially, all regions will be set to default selections (the first + * time zone, sorted lexicographically). + */ + selectedTimeZone!: Record; + + constructor(private destroyRef: DestroyRef, + private sortService: SortService) { + super(); + } + + /** + * Initializes the component by setting up value change listeners for the `control` and `region` + * form controls, getting the regions and time zones, and setting the initial disabled state of the controls. + */ + ngOnInit(): void { + + // Initialize calculated fields + this.regions = this.getRegions(); + this.timeZoneRegions = this.getTimeZoneRegions(); + this.selectedTimeZone = this.getSelectedTimeZone(); + + + // Set initial value of typed control + const initialTimeZoneID: string | null | undefined = this.control?.value; + if (initialTimeZoneID) { + this.region.setValue(this.timeZoneRegions[initialTimeZoneID] || '', { emitEvent: false }); + this.selectedTimeZone[this.region.value] = initialTimeZoneID; + } + + // Ensure corresponding region is selected + this.control?.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((timeZoneID) => { + if (timeZoneID === null) + return; + + this.region.setValue(this.timeZoneRegions[timeZoneID] || '', { emitEvent: false }); + this.selectedTimeZone[this.region.value] = timeZoneID; + }); + + // Restore time zone selection when region changes + this.region.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((region) => { + this.control?.setValue(this.selectedTimeZone[region] || null, { emitEvent: false }); + this.updateDisabledState(this.disabled); + }); + + // Set initial disabled state + this.updateDisabledState(this.disabled); + } + + /** + * Apply disabled state to both control and region + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + const disabled: boolean = changes['disabled'].currentValue; + this.updateDisabledState(disabled); + } + + } + + /** + * Updates the disabled and enabled state of the region and control form controls + * based on the disabled parameter and the region value. + * + * If no region is selected, the control will be disabled regardless of the + * disabled parameter. + * + * @param disabled + * Whether the controls should be disabled. + */ + private updateDisabledState(disabled: boolean): void { + + const regionEnabled = !disabled; + const controlEnabled = !disabled && !!this.region.value; + + // First disable both controls + this.region.disable({ emitEvent: false }); + this.control?.disable({ emitEvent: false }); + + if (regionEnabled) + this.region.enable({ emitEvent: false }); + + if (controlEnabled) + this.control?.enable({ emitEvent: false }); + } + + private getRegions(): string[] { + + // Start with blank entry + const regions = ['']; + + // Add each available region + for (const region in this.timeZones) + regions.push(region); + + return this.sortService.orderByPredicate(regions, []); + + } + + + private getTimeZoneRegions(): Record { + + const regions: Record = {}; + + // For each available region + for (const region in this.timeZones) { + + // Get time zones within that region + const timeZonesInRegion = this.timeZones[region]; + + // For each of those time zones + for (const timeZoneName in timeZonesInRegion) { + + // Get corresponding ID + const timeZoneID = timeZonesInRegion[timeZoneName]; + + // Store region in map + regions[timeZoneID] = region; + + } + + } + + return regions; + + } + + private getSelectedTimeZone(): Record { + + const defaultTimeZone: Record = {}; + + // For each available region + for (const region in this.timeZones) { + + // Get time zones within that region + const timeZonesInRegion = this.timeZones[region]; + + // No default initially + let defaultZoneName: string | undefined = undefined; + let defaultZoneID: string | undefined = undefined; + + // For each of those time zones + for (const timeZoneName in timeZonesInRegion) { + + // Get corresponding ID + const timeZoneID = timeZonesInRegion[timeZoneName]; + + // Set as default if earlier than existing default + if (!defaultZoneName || timeZoneName < defaultZoneName) { + defaultZoneName = timeZoneName; + defaultZoneID = timeZoneID; + } + + } + + // Store default zone + defaultTimeZone[region] = defaultZoneID as string; + + } + + return defaultTimeZone; + + } + + + /** + * Map of time zone regions to the map of all time zone name/ID pairs + * within those regions. + */ + timeZones: Record> = { + + 'Africa': { + 'Abidjan' : 'Africa/Abidjan', + 'Accra' : 'Africa/Accra', + 'Addis Ababa' : 'Africa/Addis_Ababa', + 'Algiers' : 'Africa/Algiers', + 'Asmara' : 'Africa/Asmara', + 'Asmera' : 'Africa/Asmera', + 'Bamako' : 'Africa/Bamako', + 'Bangui' : 'Africa/Bangui', + 'Banjul' : 'Africa/Banjul', + 'Bissau' : 'Africa/Bissau', + 'Blantyre' : 'Africa/Blantyre', + 'Brazzaville' : 'Africa/Brazzaville', + 'Bujumbura' : 'Africa/Bujumbura', + 'Cairo' : 'Africa/Cairo', + 'Casablanca' : 'Africa/Casablanca', + 'Ceuta' : 'Africa/Ceuta', + 'Conakry' : 'Africa/Conakry', + 'Dakar' : 'Africa/Dakar', + 'Dar es Salaam': 'Africa/Dar_es_Salaam', + 'Djibouti' : 'Africa/Djibouti', + 'Douala' : 'Africa/Douala', + 'El Aaiun' : 'Africa/El_Aaiun', + 'Freetown' : 'Africa/Freetown', + 'Gaborone' : 'Africa/Gaborone', + 'Harare' : 'Africa/Harare', + 'Johannesburg' : 'Africa/Johannesburg', + 'Juba' : 'Africa/Juba', + 'Kampala' : 'Africa/Kampala', + 'Khartoum' : 'Africa/Khartoum', + 'Kigali' : 'Africa/Kigali', + 'Kinshasa' : 'Africa/Kinshasa', + 'Lagos' : 'Africa/Lagos', + 'Libreville' : 'Africa/Libreville', + 'Lome' : 'Africa/Lome', + 'Luanda' : 'Africa/Luanda', + 'Lubumbashi' : 'Africa/Lubumbashi', + 'Lusaka' : 'Africa/Lusaka', + 'Malabo' : 'Africa/Malabo', + 'Maputo' : 'Africa/Maputo', + 'Maseru' : 'Africa/Maseru', + 'Mbabane' : 'Africa/Mbabane', + 'Mogadishu' : 'Africa/Mogadishu', + 'Monrovia' : 'Africa/Monrovia', + 'Nairobi' : 'Africa/Nairobi', + 'Ndjamena' : 'Africa/Ndjamena', + 'Niamey' : 'Africa/Niamey', + 'Nouakchott' : 'Africa/Nouakchott', + 'Ouagadougou' : 'Africa/Ouagadougou', + 'Porto-Novo' : 'Africa/Porto-Novo', + 'Sao Tome' : 'Africa/Sao_Tome', + 'Timbuktu' : 'Africa/Timbuktu', + 'Tripoli' : 'Africa/Tripoli', + 'Tunis' : 'Africa/Tunis', + 'Windhoek' : 'Africa/Windhoek' + }, + + 'America': { + 'Adak' : 'America/Adak', + 'Anchorage' : 'America/Anchorage', + 'Anguilla' : 'America/Anguilla', + 'Antigua' : 'America/Antigua', + 'Araguaina' : 'America/Araguaina', + 'Argentina / Buenos Aires' : 'America/Argentina/Buenos_Aires', + 'Argentina / Catamarca' : 'America/Argentina/Catamarca', + 'Argentina / Comodoro Rivadavia': 'America/Argentina/ComodRivadavia', + 'Argentina / Cordoba' : 'America/Argentina/Cordoba', + 'Argentina / Jujuy' : 'America/Argentina/Jujuy', + 'Argentina / La Rioja' : 'America/Argentina/La_Rioja', + 'Argentina / Mendoza' : 'America/Argentina/Mendoza', + 'Argentina / Rio Gallegos' : 'America/Argentina/Rio_Gallegos', + 'Argentina / Salta' : 'America/Argentina/Salta', + 'Argentina / San Juan' : 'America/Argentina/San_Juan', + 'Argentina / San Luis' : 'America/Argentina/San_Luis', + 'Argentina / Tucuman' : 'America/Argentina/Tucuman', + 'Argentina / Ushuaia' : 'America/Argentina/Ushuaia', + 'Aruba' : 'America/Aruba', + 'Asuncion' : 'America/Asuncion', + 'Atikokan' : 'America/Atikokan', + 'Atka' : 'America/Atka', + 'Bahia' : 'America/Bahia', + 'Bahia Banderas' : 'America/Bahia_Banderas', + 'Barbados' : 'America/Barbados', + 'Belem' : 'America/Belem', + 'Belize' : 'America/Belize', + 'Blanc-Sablon' : 'America/Blanc-Sablon', + 'Boa Vista' : 'America/Boa_Vista', + 'Bogota' : 'America/Bogota', + 'Boise' : 'America/Boise', + 'Buenos Aires' : 'America/Buenos_Aires', + 'Cambridge Bay' : 'America/Cambridge_Bay', + 'Campo Grande' : 'America/Campo_Grande', + 'Cancun' : 'America/Cancun', + 'Caracas' : 'America/Caracas', + 'Catamarca' : 'America/Catamarca', + 'Cayenne' : 'America/Cayenne', + 'Cayman' : 'America/Cayman', + 'Chicago' : 'America/Chicago', + 'Chihuahua' : 'America/Chihuahua', + 'Coral Harbour' : 'America/Coral_Harbour', + 'Cordoba' : 'America/Cordoba', + 'Costa Rica' : 'America/Costa_Rica', + 'Creston' : 'America/Creston', + 'Cuiaba' : 'America/Cuiaba', + 'Curacao' : 'America/Curacao', + 'Danmarkshavn' : 'America/Danmarkshavn', + 'Dawson' : 'America/Dawson', + 'Dawson Creek' : 'America/Dawson_Creek', + 'Denver' : 'America/Denver', + 'Detroit' : 'America/Detroit', + 'Dominica' : 'America/Dominica', + 'Edmonton' : 'America/Edmonton', + 'Eirunepe' : 'America/Eirunepe', + 'El Salvador' : 'America/El_Salvador', + 'Ensenada' : 'America/Ensenada', + 'Fort Wayne' : 'America/Fort_Wayne', + 'Fortaleza' : 'America/Fortaleza', + 'Glace Bay' : 'America/Glace_Bay', + 'Godthab' : 'America/Godthab', + 'Goose Bay' : 'America/Goose_Bay', + 'Grand Turk' : 'America/Grand_Turk', + 'Grenada' : 'America/Grenada', + 'Guadeloupe' : 'America/Guadeloupe', + 'Guatemala' : 'America/Guatemala', + 'Guayaquil' : 'America/Guayaquil', + 'Guyana' : 'America/Guyana', + 'Halifax' : 'America/Halifax', + 'Havana' : 'America/Havana', + 'Hermosillo' : 'America/Hermosillo', + 'Indiana / Indianapolis' : 'America/Indiana/Indianapolis', + 'Indiana / Knox' : 'America/Indiana/Knox', + 'Indiana / Marengo' : 'America/Indiana/Marengo', + 'Indiana / Petersburg' : 'America/Indiana/Petersburg', + 'Indiana / Tell City' : 'America/Indiana/Tell_City', + 'Indiana / Vevay' : 'America/Indiana/Vevay', + 'Indiana / Vincennes' : 'America/Indiana/Vincennes', + 'Indiana / Winamac' : 'America/Indiana/Winamac', + 'Indianapolis' : 'America/Indianapolis', + 'Inuvik' : 'America/Inuvik', + 'Iqaluit' : 'America/Iqaluit', + 'Jamaica' : 'America/Jamaica', + 'Jujuy' : 'America/Jujuy', + 'Juneau' : 'America/Juneau', + 'Kentucky / Louisville' : 'America/Kentucky/Louisville', + 'Kentucky / Monticello' : 'America/Kentucky/Monticello', + 'Kralendijk' : 'America/Kralendijk', + 'La Paz' : 'America/La_Paz', + 'Lima' : 'America/Lima', + 'Los Angeles' : 'America/Los_Angeles', + 'Louisville' : 'America/Louisville', + 'Lower Princes' : 'America/Lower_Princes', + 'Maceio' : 'America/Maceio', + 'Managua' : 'America/Managua', + 'Manaus' : 'America/Manaus', + 'Marigot' : 'America/Marigot', + 'Martinique' : 'America/Martinique', + 'Matamoros' : 'America/Matamoros', + 'Mazatlan' : 'America/Mazatlan', + 'Mendoza' : 'America/Mendoza', + 'Menominee' : 'America/Menominee', + 'Merida' : 'America/Merida', + 'Metlakatla' : 'America/Metlakatla', + 'Mexico City' : 'America/Mexico_City', + 'Miquelon' : 'America/Miquelon', + 'Moncton' : 'America/Moncton', + 'Monterrey' : 'America/Monterrey', + 'Montevideo' : 'America/Montevideo', + 'Montreal' : 'America/Montreal', + 'Montserrat' : 'America/Montserrat', + 'Nassau' : 'America/Nassau', + 'New York' : 'America/New_York', + 'Nipigon' : 'America/Nipigon', + 'Nome' : 'America/Nome', + 'Noronha' : 'America/Noronha', + 'North Dakota / Beulah' : 'America/North_Dakota/Beulah', + 'North Dakota / Center' : 'America/North_Dakota/Center', + 'North Dakota / New Salem' : 'America/North_Dakota/New_Salem', + 'Ojinaga' : 'America/Ojinaga', + 'Panama' : 'America/Panama', + 'Pangnirtung' : 'America/Pangnirtung', + 'Paramaribo' : 'America/Paramaribo', + 'Phoenix' : 'America/Phoenix', + 'Port-au-Prince' : 'America/Port-au-Prince', + 'Port of Spain' : 'America/Port_of_Spain', + 'Porto Acre' : 'America/Porto_Acre', + 'Porto Velho' : 'America/Porto_Velho', + 'Puerto Rico' : 'America/Puerto_Rico', + 'Rainy River' : 'America/Rainy_River', + 'Rankin Inlet' : 'America/Rankin_Inlet', + 'Recife' : 'America/Recife', + 'Regina' : 'America/Regina', + 'Resolute' : 'America/Resolute', + 'Rio Branco' : 'America/Rio_Branco', + 'Rosario' : 'America/Rosario', + 'Santa Isabel' : 'America/Santa_Isabel', + 'Santarem' : 'America/Santarem', + 'Santiago' : 'America/Santiago', + 'Santo Domingo' : 'America/Santo_Domingo', + 'Sao Paulo' : 'America/Sao_Paulo', + 'Scoresbysund' : 'America/Scoresbysund', + 'Shiprock' : 'America/Shiprock', + 'Sitka' : 'America/Sitka', + 'St. Barthelemy' : 'America/St_Barthelemy', + 'St. Johns' : 'America/St_Johns', + 'St. Kitts' : 'America/St_Kitts', + 'St. Lucia' : 'America/St_Lucia', + 'St. Thomas' : 'America/St_Thomas', + 'St. Vincent' : 'America/St_Vincent', + 'Swift Current' : 'America/Swift_Current', + 'Tegucigalpa' : 'America/Tegucigalpa', + 'Thule' : 'America/Thule', + 'Thunder Bay' : 'America/Thunder_Bay', + 'Tijuana' : 'America/Tijuana', + 'Toronto' : 'America/Toronto', + 'Tortola' : 'America/Tortola', + 'Vancouver' : 'America/Vancouver', + 'Virgin' : 'America/Virgin', + 'Whitehorse' : 'America/Whitehorse', + 'Winnipeg' : 'America/Winnipeg', + 'Yakutat' : 'America/Yakutat', + 'Yellowknife' : 'America/Yellowknife' + }, + + 'Antarctica': { + 'Casey' : 'Antarctica/Casey', + 'Davis' : 'Antarctica/Davis', + 'Dumont d\'Urville': 'Antarctica/DumontDUrville', + 'Macquarie' : 'Antarctica/Macquarie', + 'Mawson' : 'Antarctica/Mawson', + 'McMurdo' : 'Antarctica/McMurdo', + 'Palmer' : 'Antarctica/Palmer', + 'Rothera' : 'Antarctica/Rothera', + 'South Pole' : 'Antarctica/South_Pole', + 'Syowa' : 'Antarctica/Syowa', + 'Troll' : 'Antarctica/Troll', + 'Vostok' : 'Antarctica/Vostok' + }, + + 'Arctic': { + 'Longyearbyen': 'Arctic/Longyearbyen' + }, + + 'Asia': { + 'Aden' : 'Asia/Aden', + 'Almaty' : 'Asia/Almaty', + 'Amman' : 'Asia/Amman', + 'Anadyr' : 'Asia/Anadyr', + 'Aqtau' : 'Asia/Aqtau', + 'Aqtobe' : 'Asia/Aqtobe', + 'Ashgabat' : 'Asia/Ashgabat', + 'Ashkhabad' : 'Asia/Ashkhabad', + 'Baghdad' : 'Asia/Baghdad', + 'Bahrain' : 'Asia/Bahrain', + 'Baku' : 'Asia/Baku', + 'Bangkok' : 'Asia/Bangkok', + 'Beirut' : 'Asia/Beirut', + 'Bishkek' : 'Asia/Bishkek', + 'Brunei' : 'Asia/Brunei', + 'Calcutta' : 'Asia/Calcutta', + 'Chita' : 'Asia/Chita', + 'Choibalsan' : 'Asia/Choibalsan', + 'Chongqing' : 'Asia/Chongqing', + 'Colombo' : 'Asia/Colombo', + 'Dacca' : 'Asia/Dacca', + 'Damascus' : 'Asia/Damascus', + 'Dhaka' : 'Asia/Dhaka', + 'Dili' : 'Asia/Dili', + 'Dubai' : 'Asia/Dubai', + 'Dushanbe' : 'Asia/Dushanbe', + 'Gaza' : 'Asia/Gaza', + 'Harbin' : 'Asia/Harbin', + 'Hebron' : 'Asia/Hebron', + 'Ho Chi Minh' : 'Asia/Ho_Chi_Minh', + 'Hong Kong' : 'Asia/Hong_Kong', + 'Hovd' : 'Asia/Hovd', + 'Irkutsk' : 'Asia/Irkutsk', + 'Istanbul' : 'Asia/Istanbul', + 'Jakarta' : 'Asia/Jakarta', + 'Jayapura' : 'Asia/Jayapura', + 'Jerusalem' : 'Asia/Jerusalem', + 'Kabul' : 'Asia/Kabul', + 'Kamchatka' : 'Asia/Kamchatka', + 'Karachi' : 'Asia/Karachi', + 'Kashgar' : 'Asia/Kashgar', + 'Kathmandu' : 'Asia/Kathmandu', + 'Katmandu' : 'Asia/Katmandu', + 'Khandyga' : 'Asia/Khandyga', + 'Kolkata' : 'Asia/Kolkata', + 'Krasnoyarsk' : 'Asia/Krasnoyarsk', + 'Kuala Lumpur' : 'Asia/Kuala_Lumpur', + 'Kuching' : 'Asia/Kuching', + 'Kuwait' : 'Asia/Kuwait', + 'Macao' : 'Asia/Macao', + 'Macau' : 'Asia/Macau', + 'Magadan' : 'Asia/Magadan', + 'Makassar' : 'Asia/Makassar', + 'Manila' : 'Asia/Manila', + 'Muscat' : 'Asia/Muscat', + 'Nicosia' : 'Asia/Nicosia', + 'Novokuznetsk' : 'Asia/Novokuznetsk', + 'Novosibirsk' : 'Asia/Novosibirsk', + 'Omsk' : 'Asia/Omsk', + 'Oral' : 'Asia/Oral', + 'Phnom Penh' : 'Asia/Phnom_Penh', + 'Pontianak' : 'Asia/Pontianak', + 'Pyongyang' : 'Asia/Pyongyang', + 'Qatar' : 'Asia/Qatar', + 'Qyzylorda' : 'Asia/Qyzylorda', + 'Rangoon' : 'Asia/Rangoon', + 'Riyadh' : 'Asia/Riyadh', + 'Saigon' : 'Asia/Saigon', + 'Sakhalin' : 'Asia/Sakhalin', + 'Samarkand' : 'Asia/Samarkand', + 'Seoul' : 'Asia/Seoul', + 'Shanghai' : 'Asia/Shanghai', + 'Singapore' : 'Asia/Singapore', + 'Srednekolymsk': 'Asia/Srednekolymsk', + 'Taipei' : 'Asia/Taipei', + 'Tashkent' : 'Asia/Tashkent', + 'Tbilisi' : 'Asia/Tbilisi', + 'Tehran' : 'Asia/Tehran', + 'Tel Aviv' : 'Asia/Tel_Aviv', + 'Thimbu' : 'Asia/Thimbu', + 'Thimphu' : 'Asia/Thimphu', + 'Tokyo' : 'Asia/Tokyo', + 'Ujung Pandang': 'Asia/Ujung_Pandang', + 'Ulaanbaatar' : 'Asia/Ulaanbaatar', + 'Ulan Bator' : 'Asia/Ulan_Bator', + 'Urumqi' : 'Asia/Urumqi', + 'Ust-Nera' : 'Asia/Ust-Nera', + 'Vientiane' : 'Asia/Vientiane', + 'Vladivostok' : 'Asia/Vladivostok', + 'Yakutsk' : 'Asia/Yakutsk', + 'Yekaterinburg': 'Asia/Yekaterinburg', + 'Yerevan' : 'Asia/Yerevan' + }, + + 'Atlantic': { + 'Azores' : 'Atlantic/Azores', + 'Bermuda' : 'Atlantic/Bermuda', + 'Canary' : 'Atlantic/Canary', + 'Cape Verde' : 'Atlantic/Cape_Verde', + 'Faeroe' : 'Atlantic/Faeroe', + 'Faroe' : 'Atlantic/Faroe', + 'Jan Mayen' : 'Atlantic/Jan_Mayen', + 'Madeira' : 'Atlantic/Madeira', + 'Reykjavik' : 'Atlantic/Reykjavik', + 'South Georgia': 'Atlantic/South_Georgia', + 'St. Helena' : 'Atlantic/St_Helena', + 'Stanley' : 'Atlantic/Stanley' + }, + + 'Australia': { + 'Adelaide' : 'Australia/Adelaide', + 'Brisbane' : 'Australia/Brisbane', + 'Broken Hill': 'Australia/Broken_Hill', + 'Canberra' : 'Australia/Canberra', + 'Currie' : 'Australia/Currie', + 'Darwin' : 'Australia/Darwin', + 'Eucla' : 'Australia/Eucla', + 'Hobart' : 'Australia/Hobart', + 'Lindeman' : 'Australia/Lindeman', + 'Lord Howe' : 'Australia/Lord_Howe', + 'Melbourne' : 'Australia/Melbourne', + 'North' : 'Australia/North', + 'Perth' : 'Australia/Perth', + 'Queensland' : 'Australia/Queensland', + 'South' : 'Australia/South', + 'Sydney' : 'Australia/Sydney', + 'Tasmania' : 'Australia/Tasmania', + 'Victoria' : 'Australia/Victoria', + 'West' : 'Australia/West', + 'Yancowinna' : 'Australia/Yancowinna' + }, + + 'Brazil': { + 'Acre' : 'Brazil/Acre', + 'Fernando de Noronha': 'Brazil/DeNoronha', + 'East' : 'Brazil/East', + 'West' : 'Brazil/West' + }, + + 'Canada': { + 'Atlantic' : 'Canada/Atlantic', + 'Central' : 'Canada/Central', + 'Eastern' : 'Canada/Eastern', + 'Mountain' : 'Canada/Mountain', + 'Newfoundland': 'Canada/Newfoundland', + 'Pacific' : 'Canada/Pacific', + 'Saskatchewan': 'Canada/Saskatchewan', + 'Yukon' : 'Canada/Yukon' + }, + + 'Chile': { + 'Continental' : 'Chile/Continental', + 'Easter Island': 'Chile/EasterIsland' + }, + + 'Europe': { + 'Amsterdam' : 'Europe/Amsterdam', + 'Andorra' : 'Europe/Andorra', + 'Athens' : 'Europe/Athens', + 'Belfast' : 'Europe/Belfast', + 'Belgrade' : 'Europe/Belgrade', + 'Berlin' : 'Europe/Berlin', + 'Bratislava' : 'Europe/Bratislava', + 'Brussels' : 'Europe/Brussels', + 'Bucharest' : 'Europe/Bucharest', + 'Budapest' : 'Europe/Budapest', + 'Busingen' : 'Europe/Busingen', + 'Chisinau' : 'Europe/Chisinau', + 'Copenhagen' : 'Europe/Copenhagen', + 'Dublin' : 'Europe/Dublin', + 'Gibraltar' : 'Europe/Gibraltar', + 'Guernsey' : 'Europe/Guernsey', + 'Helsinki' : 'Europe/Helsinki', + 'Isle of Man': 'Europe/Isle_of_Man', + 'Istanbul' : 'Europe/Istanbul', + 'Jersey' : 'Europe/Jersey', + 'Kaliningrad': 'Europe/Kaliningrad', + 'Kiev' : 'Europe/Kiev', + 'Lisbon' : 'Europe/Lisbon', + 'Ljubljana' : 'Europe/Ljubljana', + 'London' : 'Europe/London', + 'Luxembourg' : 'Europe/Luxembourg', + 'Madrid' : 'Europe/Madrid', + 'Malta' : 'Europe/Malta', + 'Mariehamn' : 'Europe/Mariehamn', + 'Minsk' : 'Europe/Minsk', + 'Monaco' : 'Europe/Monaco', + 'Moscow' : 'Europe/Moscow', + 'Nicosia' : 'Europe/Nicosia', + 'Oslo' : 'Europe/Oslo', + 'Paris' : 'Europe/Paris', + 'Podgorica' : 'Europe/Podgorica', + 'Prague' : 'Europe/Prague', + 'Riga' : 'Europe/Riga', + 'Rome' : 'Europe/Rome', + 'Samara' : 'Europe/Samara', + 'San Marino' : 'Europe/San_Marino', + 'Sarajevo' : 'Europe/Sarajevo', + 'Simferopol' : 'Europe/Simferopol', + 'Skopje' : 'Europe/Skopje', + 'Sofia' : 'Europe/Sofia', + 'Stockholm' : 'Europe/Stockholm', + 'Tallinn' : 'Europe/Tallinn', + 'Tirane' : 'Europe/Tirane', + 'Tiraspol' : 'Europe/Tiraspol', + 'Uzhgorod' : 'Europe/Uzhgorod', + 'Vaduz' : 'Europe/Vaduz', + 'Vatican' : 'Europe/Vatican', + 'Vienna' : 'Europe/Vienna', + 'Vilnius' : 'Europe/Vilnius', + 'Volgograd' : 'Europe/Volgograd', + 'Warsaw' : 'Europe/Warsaw', + 'Zagreb' : 'Europe/Zagreb', + 'Zaporozhye' : 'Europe/Zaporozhye', + 'Zurich' : 'Europe/Zurich' + }, + + 'GMT': { + 'GMT-14': 'Etc/GMT-14', + 'GMT-13': 'Etc/GMT-13', + 'GMT-12': 'Etc/GMT-12', + 'GMT-11': 'Etc/GMT-11', + 'GMT-10': 'Etc/GMT-10', + 'GMT-9' : 'Etc/GMT-9', + 'GMT-8' : 'Etc/GMT-8', + 'GMT-7' : 'Etc/GMT-7', + 'GMT-6' : 'Etc/GMT-6', + 'GMT-5' : 'Etc/GMT-5', + 'GMT-4' : 'Etc/GMT-4', + 'GMT-3' : 'Etc/GMT-3', + 'GMT-2' : 'Etc/GMT-2', + 'GMT-1' : 'Etc/GMT-1', + 'GMT+0' : 'Etc/GMT+0', + 'GMT+1' : 'Etc/GMT+1', + 'GMT+2' : 'Etc/GMT+2', + 'GMT+3' : 'Etc/GMT+3', + 'GMT+4' : 'Etc/GMT+4', + 'GMT+5' : 'Etc/GMT+5', + 'GMT+6' : 'Etc/GMT+6', + 'GMT+7' : 'Etc/GMT+7', + 'GMT+8' : 'Etc/GMT+8', + 'GMT+9' : 'Etc/GMT+9', + 'GMT+10': 'Etc/GMT+10', + 'GMT+11': 'Etc/GMT+11', + 'GMT+12': 'Etc/GMT+12' + }, + + 'Indian': { + 'Antananarivo': 'Indian/Antananarivo', + 'Chagos' : 'Indian/Chagos', + 'Christmas' : 'Indian/Christmas', + 'Cocos' : 'Indian/Cocos', + 'Comoro' : 'Indian/Comoro', + 'Kerguelen' : 'Indian/Kerguelen', + 'Mahe' : 'Indian/Mahe', + 'Maldives' : 'Indian/Maldives', + 'Mauritius' : 'Indian/Mauritius', + 'Mayotte' : 'Indian/Mayotte', + 'Reunion' : 'Indian/Reunion' + }, + + 'Mexico': { + 'Baja Norte': 'Mexico/BajaNorte', + 'Baja Sur' : 'Mexico/BajaSur', + 'General' : 'Mexico/General' + }, + + 'Pacific': { + 'Apia' : 'Pacific/Apia', + 'Auckland' : 'Pacific/Auckland', + 'Bougainville': 'Pacific/Bougainville', + 'Chatham' : 'Pacific/Chatham', + 'Chuuk' : 'Pacific/Chuuk', + 'Easter' : 'Pacific/Easter', + 'Efate' : 'Pacific/Efate', + 'Enderbury' : 'Pacific/Enderbury', + 'Fakaofo' : 'Pacific/Fakaofo', + 'Fiji' : 'Pacific/Fiji', + 'Funafuti' : 'Pacific/Funafuti', + 'Galapagos' : 'Pacific/Galapagos', + 'Gambier' : 'Pacific/Gambier', + 'Guadalcanal' : 'Pacific/Guadalcanal', + 'Guam' : 'Pacific/Guam', + 'Honolulu' : 'Pacific/Honolulu', + 'Johnston' : 'Pacific/Johnston', + 'Kiritimati' : 'Pacific/Kiritimati', + 'Kosrae' : 'Pacific/Kosrae', + 'Kwajalein' : 'Pacific/Kwajalein', + 'Majuro' : 'Pacific/Majuro', + 'Marquesas' : 'Pacific/Marquesas', + 'Midway' : 'Pacific/Midway', + 'Nauru' : 'Pacific/Nauru', + 'Niue' : 'Pacific/Niue', + 'Norfolk' : 'Pacific/Norfolk', + 'Noumea' : 'Pacific/Noumea', + 'Pago Pago' : 'Pacific/Pago_Pago', + 'Palau' : 'Pacific/Palau', + 'Pitcairn' : 'Pacific/Pitcairn', + 'Pohnpei' : 'Pacific/Pohnpei', + 'Ponape' : 'Pacific/Ponape', + 'Port Moresby': 'Pacific/Port_Moresby', + 'Rarotonga' : 'Pacific/Rarotonga', + 'Saipan' : 'Pacific/Saipan', + 'Samoa' : 'Pacific/Samoa', + 'Tahiti' : 'Pacific/Tahiti', + 'Tarawa' : 'Pacific/Tarawa', + 'Tongatapu' : 'Pacific/Tongatapu', + 'Truk' : 'Pacific/Truk', + 'Wake' : 'Pacific/Wake', + 'Wallis' : 'Pacific/Wallis', + 'Yap' : 'Pacific/Yap' + } + + }; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.html new file mode 100644 index 0000000000..27cc0c5907 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.html @@ -0,0 +1,29 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.spec.ts new file mode 100644 index 0000000000..d6395637a1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { UsernameFieldComponent } from './username-field.component'; + +describe('UsernameFieldComponent', () => { + let component: UsernameFieldComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [UsernameFieldComponent] + }); + fixture = TestBed.createComponent(UsernameFieldComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.ts new file mode 100644 index 0000000000..10b5387b82 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/components/username-field/username-field.component.ts @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { FormFieldBaseComponent } from '../form-field-base/form-field-base.component'; + +@Component({ + selector: 'guac-username-field', + templateUrl: './username-field.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class UsernameFieldComponent extends FormFieldBaseComponent implements OnChanges { + + /** + * The ID value that should be used to associate + * the relevant input element with the label provided by the + * guacFormField component, if there is such an input element. + */ + @Input() fieldId?: string; + + /** + * Apply disabled state to typed form control. + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['disabled']) { + + const disabled: boolean = changes['disabled'].currentValue; + this.setDisabledState(this.control, disabled); + } + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-date.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-date.directive.ts new file mode 100644 index 0000000000..a206eab789 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-date.directive.ts @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * A directive which modifies the parsing and formatting of a form control value when used + * on an HTML5 date input field, relaxing the otherwise strict parsing and + * validation behavior. The behavior of this directive for other input elements + * is undefined. + */ +@Directive({ + selector: '[guacLenientDate]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GuacLenientDateDirective), + multi: true + } + ], + standalone: false +}) +export class GuacLenientDateDirective implements ControlValueAccessor { + + /** + * Callback function that has to be called when the control's value changes in the UI. + * + * @param value + * The new value of the control. + */ + private onChange!: (value: any) => void; + + /** + * Callback function that has to be called when the control's value changes in the UI. + */ + private onTouched!: () => void; + + constructor(private renderer: Renderer2, private el: ElementRef) { + } + + /** + * Called by the forms API to write to the view when programmatic changes from model to view are requested. + * Formats the date value as "yyyy-MM-dd" and sets the value of the input element. + * + * @param value + * The new value for the input element. + */ + writeValue(value: any): void { + const formattedValue = value ? this.format(value) : ''; + this.renderer.setProperty(this.el.nativeElement, 'value', formattedValue); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty(this.el.nativeElement, 'disabled', isDisabled); + } + + /** + * Form control will be updated when the input loses focus. + */ + @HostListener('blur', ['$event.target.value']) + onInput(value: string): void { + const parsedValue = this.parse(value); + this.onChange(parsedValue); + this.onTouched(); + } + + /** + * Parse date strings leniently. + * + * @param viewValue + * The date string to parse. + */ + parse(viewValue: string): Date | null { + + // If blank, return null + if (!viewValue) + return null; + + // Match basic date pattern + const match = /([0-9]*)(?:-([0-9]*)(?:-([0-9]*))?)?/.exec(viewValue); + if (!match) + return null; + + // Determine year, month, and day based on pattern + const year = parseInt(match[1] || '0') || new Date().getFullYear(); + const month = parseInt(match[2] || '0') || 1; + const day = parseInt(match[3] || '0') || 1; + + // Convert to Date object + const parsedDate = new Date(Date.UTC(year, month - 1, day)); + if (isNaN(parsedDate.getTime())) + return null; + + return parsedDate; + + } + + /** + * Format date strings as "yyyy-MM-dd". + * + * @param modelValue + * The date to format. + */ + format(modelValue: Date | null): string { + return modelValue ? formatDate(modelValue, 'yyyy-MM-dd', 'en-US', 'UTC') : ''; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-time.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-time.directive.ts new file mode 100644 index 0000000000..c88ddfaa67 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/directives/guac-lenient-time.directive.ts @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Directive, ElementRef, forwardRef, HostListener, Renderer2 } from '@angular/core'; +import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; + +/** + * A directive which modifies the parsing and formatting of a form control value when used + * on an HTML5 date input field, relaxing the otherwise strict parsing and + * validation behavior. The behavior of this directive for other input elements + * is undefined. + */ +@Directive({ + selector: '[guacLenientTime]', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + useExisting: forwardRef(() => GuacLenientTimeDirective), + multi: true + } + ], + standalone: false +}) +export class GuacLenientTimeDirective implements ControlValueAccessor { + + /** + * Callback function that has to be called when the control's value changes in the UI. + * + * @param value + * The new value of the control. + */ + private onChange!: (value: any) => void; + + /** + * Callback function that has to be called when the control's value changes in the UI. + */ + private onTouched!: () => void; + + constructor(private renderer: Renderer2, private el: ElementRef) { + } + + /** + * Called by the forms API to write to the view when programmatic changes from model to view are requested. + * Formats the date value as "yyyy-MM-dd" and sets the value of the input element. + * + * @param value + * The new value for the input element. + */ + writeValue(value: any): void { + const formattedValue = value ? this.format(value) : ''; + this.renderer.setProperty(this.el.nativeElement, 'value', formattedValue); + } + + registerOnChange(fn: any): void { + this.onChange = fn; + } + + registerOnTouched(fn: any): void { + this.onTouched = fn; + } + + setDisabledState(isDisabled: boolean): void { + this.renderer.setProperty(this.el.nativeElement, 'disabled', isDisabled); + } + + /** + * Form control will be updated when the input loses focus. + */ + @HostListener('blur', ['$event.target.value']) + onInput(value: string): void { + const parsedValue = this.parse(value); + this.onChange(parsedValue); + this.onTouched(); + } + + /** + * Parse time strings leniently. + * + * @param viewValue + * The time string to parse. + */ + parse(viewValue: string): Date | null { + + // If blank, return null + if (!viewValue) + return null; + + // Match basic time pattern + const match = /([0-9]*)(?::([0-9]*)(?::([0-9]*))?)?(?:\s*(a|p))?/.exec(viewValue.toLowerCase()); + if (!match) + return null; + + // Determine hour, minute, and second based on pattern + let hour = parseInt(match[1] || '0'); + let minute = parseInt(match[2] || '0'); + let second = parseInt(match[3] || '0'); + + // Handle AM/PM + if (match[4]) { + + // Interpret 12 AM as 00:00 and 12 PM as 12:00 + if (hour === 12) + hour = 0; + + // Increment hour to evening if PM + if (match[4] === 'p') + hour += 12; + + } + + // Wrap seconds and minutes into minutes and hours + minute += second / 60; + second %= 60; + hour += minute / 60; + minute %= 60; + + // Constrain hours to 0 - 23 + hour %= 24; + + // Convert to Date object + const parsedDate = new Date(Date.UTC(1970, 0, 1, hour, minute, second)); + if (isNaN(parsedDate.getTime())) + return null; + + return parsedDate; + + } + + /** + * Format date strings as "yyyy-MM-dd". + * + * @param modelValue + * The date to format. + */ + format(modelValue: Date | null): string { + return modelValue ? formatDate(modelValue, 'HH:mm:ss', 'en-US', 'UTC') : ''; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/form.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/form.module.ts new file mode 100644 index 0000000000..7428dd8c38 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/form.module.ts @@ -0,0 +1,103 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ElementModule } from 'guacamole-frontend-lib'; +import { OrderByPipe } from '../util/order-by.pipe'; +import { CheckboxFieldComponent } from './components/checkbox-field/checkbox-field.component'; +import { DateFieldComponent } from './components/date-field/date-field.component'; +import { EmailFieldComponent } from './components/email-field/email-field.component'; +import { FormFieldComponent } from './components/form-field/form-field.component'; +import { FormComponent } from './components/form/form.component'; +import { GuacInputColorComponent } from './components/guac-input-color/guac-input-color.component'; +import { LanguageFieldComponent } from './components/language-field/language-field.component'; +import { NumberFieldComponent } from './components/number-field/number-field.component'; +import { PasswordFieldComponent } from './components/password-field/password-field.component'; +import { RedirectFieldComponent } from './components/redirect-field/redirect-field.component'; +import { SelectFieldComponent } from './components/select-field/select-field.component'; +import { + TerminalColorSchemeFieldComponent +} from './components/terminal-color-scheme-field/terminal-color-scheme-field.component'; +import { TextAreaFieldComponent } from './components/text-area-field/text-area-field.component'; +import { TextFieldComponent } from './components/text-field/text-field.component'; +import { TimeFieldComponent } from './components/time-field/time-field.component'; +import { TimeZoneFieldComponent } from './components/time-zone-field/time-zone-field.component'; +import { UsernameFieldComponent } from './components/username-field/username-field.component'; +import { GuacLenientDateDirective } from './directives/guac-lenient-date.directive'; +import { GuacLenientTimeDirective } from './directives/guac-lenient-time.directive'; + +/** + * Module for displaying dynamic forms. + */ +@NgModule({ + declarations: [ + GuacLenientDateDirective, + GuacLenientTimeDirective, + TimeZoneFieldComponent, + FormFieldComponent, + FormComponent, + CheckboxFieldComponent, + DateFieldComponent, + LanguageFieldComponent, + NumberFieldComponent, + PasswordFieldComponent, + RedirectFieldComponent, + SelectFieldComponent, + TextFieldComponent, + TextAreaFieldComponent, + TimeFieldComponent, + EmailFieldComponent, + UsernameFieldComponent, + TerminalColorSchemeFieldComponent, + GuacInputColorComponent + ], + exports: [ + TimeZoneFieldComponent, + FormFieldComponent, + FormComponent, + CheckboxFieldComponent, + DateFieldComponent, + GuacLenientDateDirective, + LanguageFieldComponent, + NumberFieldComponent, + PasswordFieldComponent, + RedirectFieldComponent, + SelectFieldComponent, + TextFieldComponent, + TextAreaFieldComponent, + TimeFieldComponent, + EmailFieldComponent, + UsernameFieldComponent, + TerminalColorSchemeFieldComponent, + GuacInputColorComponent + ], + imports: [ + CommonModule, + ElementModule, + ReactiveFormsModule, + TranslocoModule, + OrderByPipe + ] +}) +export class FormModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/color-picker.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/color-picker.service.ts new file mode 100644 index 0000000000..3bfbe5a098 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/color-picker.service.ts @@ -0,0 +1,298 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import Pickr from '@simonwep/pickr'; +import { forkJoin, take } from 'rxjs'; +import HSVaColor = Pickr.HSVaColor; + +/** + * A service for prompting the user to choose a color using the "Pickr" color + * picker. As the Pickr color picker might not be available if the JavaScript + * features it requires are not supported by the browser (Internet Explorer), + * the isAvailable() function should be used to test for usability. + */ +@Injectable({ + providedIn: 'root' +}) +export class ColorPickerService { + + /** + * A singleton instance of the "Pickr" color picker, shared by all users of + * this service. Pickr does not initialize synchronously, nor is it + * supported by all browsers. If Pickr is not yet initialized, or is + * unsupported, this will be null. + */ + pickr?: Pickr = undefined; + + /** + * Whether Pickr has completed initialization. + */ + public pickrInitComplete = false; + + /** + * The HTML element to provide to Pickr as the root element. + */ + pickerContainer: HTMLDivElement; + + /** + * A Promise which represents an active request for the + * user to choose a color. The promise will + * be resolved with the chosen color once a color is chosen, and rejected + * if the request is cancelled or Pickr is not available. If no request is + * active, this will be undefined. + */ + activeRequest?: Promise = undefined; + + /** + * Resolve function of the {@link activeRequest} promise. + */ + private activeRequestResolve?: (value: string) => void = undefined; + + /** + * Reject function of the {@link activeRequest} promise. + */ + private activeRequestReject?: (reason?: any) => void = undefined; + + private saveString?: string; + private cancelString?: string; + + constructor(@Inject(DOCUMENT) private document: Document, private translocoService: TranslocoService) { + + // Create container element for Pickr + this.pickerContainer = this.document.createElement('div'); + this.pickerContainer.className = 'shared-color-picker'; + + // Wait for translation strings + const saveString = this.translocoService.selectTranslate('APP.ACTION_SAVE').pipe(take(1)); + const cancelString = this.translocoService.selectTranslate('APP.ACTION_CANCEL').pipe(take(1)); + + forkJoin({ + saveString, + cancelString + }).subscribe(({ saveString, cancelString }) => { + this.saveString = saveString; + this.cancelString = cancelString; + + this.createPickr(); + this.createPickrPromise(); + }); + + } + + private createPickrPromise(): void { + this.pickrPromise = new Promise((resolve, reject) => { + + // Resolve promise when Pickr has completed initialization + if (this.pickrInitComplete) + resolve(); + else if (this.pickr) + this.pickr.on('init', resolve); + + // Reject promise if Pickr cannot be used at all + else + reject(); + }); + } + + /** + * Resolves the current active request with the given color value. If no + * color value is provided, the active request is rejected. If no request + * is active, this function has no effect. + * + * @param color + * The color value to resolve the active request with. + */ + completeActiveRequest(color?: string): void { + if (this.activeRequest) { + + // Hide color picker, if shown + this.pickr?.hide(); + + // Resolve/reject active request depending on value provided + if (color) + this.activeRequestResolve?.(color); + else + this.activeRequestReject?.(); + + // No active request + this.activeRequest = undefined; + + } + } + + createPickr(): void { + try { + + this.pickr = Pickr.create({ + + // Bind color picker to the container element + el: this.pickerContainer, + + // Wrap color picker dialog in Guacamole-specific class for + // sake of additional styling + appClass: 'guac-input-color-picker', + + default: '#000000', + + // Display color details as hex + defaultRepresentation: 'HEXA', + + // Use "monolith" theme, as a nice balance between "nano" (does + // not work in Internet Explorer) and "classic" (too big) + theme: 'monolith', + + // Leverage the container element as the button which shows the + // picker, relying on our own styling for that button + useAsButton: true, + + // Do not include opacity controls + lockOpacity: true, + + // Include a selection of palette entries for convenience and + // reference + swatches: [], + + components: { + + // Include hue and color preview controls + preview: true, + hue : true, + + // Display only a text color input field and the save and + // cancel buttons (no clear button) + interaction: { + input : true, + save : true, + cancel: true + } + + }, + + // Assign translated strings to button text + i18n: { + 'btn:save' : this.saveString, + 'btn:cancel': this.cancelString + } + + }); + + // Hide color picker after user clicks "cancel" + this.pickr.on('cancel', () => { + this.completeActiveRequest(); + }); + + // Keep model in sync with changes to the color picker + this.pickr.on('save', (color: HSVaColor | null) => { + const colorString = color ? color.toHEXA().toString() : undefined; + this.completeActiveRequest(colorString); + this.activeRequest = undefined; + }); + + // Keep color picker in sync with changes to the model + this.pickr.on('init', () => { + this.pickrInitComplete = true; + }); + } catch (e) { + // If the "Pickr" color picker cannot be loaded (Internet Explorer), + // the available flag will remain set to false + } + } + + /** + * Promise which is resolved when Pickr initialization has completed + * and rejected if Pickr cannot be used. + */ + pickrPromise!: Promise; + + /** + * Returns whether the underlying color picker (Pickr) can be used by + * calling selectColor(). If the browser cannot support the color + * picker, false is returned. + * + * @returns + * true if the underlying color picker can be used by calling + * selectColor(), false otherwise. + */ + isAvailable(): boolean { + return this.pickrInitComplete; + } + + /** + * Prompts the user to choose a color, returning the color chosen via a + * Promise. + * + * @param element + * The element that the user interacted with to indicate their + * desire to choose a color. + * + * @param current + * The color that should be selected by default, in standard + * 6-digit hexadecimal RGB format, including "#" prefix. + * + * @param palette + * An array of color choices which should be exposed to the user + * within the color chooser for convenience. Each color must be in + * standard 6-digit hexadecimal RGB format, including "#" prefix. + * + * @returns + * A Promise which is resolved with the color chosen by the user, + * in standard 6-digit hexadecimal RGB format with "#" prefix, and + * rejected if the selection operation was cancelled or the color + * picker cannot be used. + */ + selectColor(element: Element, current: string | null, palette: string[] = []): Promise { + + const newRequest = new Promise((resolve, reject) => { + this.activeRequestResolve = resolve; + this.activeRequestReject = reject; + }); + + // Show picker once Pickr is ready for use + return this.pickrPromise.then(() => { + if (!this.pickr) { + return Promise.reject(); + } + + // Cancel any active request + this.completeActiveRequest(); + + // Reset state of color picker to provided parameters. Use silent=true + // to avoid triggering the "save" event prematurely. + this.pickr.setColor(current, true); + this.pickr.applyColor(true); + element.appendChild(this.pickerContainer); + + // Replace all color swatches with the palette of colors given + while (this.pickr.removeSwatch(0)) { + } + for (const color of palette) { + this.pickr.addSwatch(color); + } + + // Show color picker and wait for user to complete selection + this.activeRequest = newRequest; + this.pickr.show(); + + return newRequest; + }); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.spec.ts new file mode 100644 index 0000000000..e2902b8d27 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.spec.ts @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; + +import { FormService } from './form.service'; + +describe('FormService', () => { + let service: FormService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(FormService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.ts new file mode 100644 index 0000000000..fa8bc1c6d3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/service/form.service.ts @@ -0,0 +1,402 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + ChangeDetectorRef, + ComponentRef, + Injectable, + Renderer2, + RendererFactory2, + Type, + ViewContainerRef +} from '@angular/core'; +import { FormControl, FormGroup } from '@angular/forms'; +import { FieldTypeService, FormFieldComponentData } from 'guacamole-frontend-ext-lib'; +import { Field } from '../../rest/types/Field'; +import { Form } from '../../rest/types/Form'; +import { isArray } from '../../util/is-array'; +import { CheckboxFieldComponent } from '../components/checkbox-field/checkbox-field.component'; +import { DateFieldComponent } from '../components/date-field/date-field.component'; +import { EmailFieldComponent } from '../components/email-field/email-field.component'; +import { FormFieldComponent } from '../components/form-field/form-field.component'; +import { LanguageFieldComponent } from '../components/language-field/language-field.component'; +import { NumberFieldComponent } from '../components/number-field/number-field.component'; +import { PasswordFieldComponent } from '../components/password-field/password-field.component'; +import { RedirectFieldComponent } from '../components/redirect-field/redirect-field.component'; +import { SelectFieldComponent } from '../components/select-field/select-field.component'; +import { + TerminalColorSchemeFieldComponent +} from '../components/terminal-color-scheme-field/terminal-color-scheme-field.component'; +import { TextAreaFieldComponent } from '../components/text-area-field/text-area-field.component'; +import { TextFieldComponent } from '../components/text-field/text-field.component'; +import { TimeFieldComponent } from '../components/time-field/time-field.component'; +import { TimeZoneFieldComponent } from '../components/time-zone-field/time-zone-field.component'; +import { UsernameFieldComponent } from '../components/username-field/username-field.component'; + +/** + * A service for maintaining form-related metadata and linking that data to + * corresponding controllers and templates. + */ +@Injectable({ + providedIn: 'root' +}) +export class FormService { + + /** + * Reference to the view container that should be used to create the field component. + */ + private viewContainer: ViewContainerRef | undefined; + + private cdr: ChangeDetectorRef | undefined; + + private renderer: Renderer2; + + constructor(private fieldTypeService: FieldTypeService, rendererFactory: RendererFactory2) { + this.registerFieldTypes(); + this.renderer = rendererFactory.createRenderer(null, null); + } + + /** + * Registers the default field types with this field type service. + */ + private registerFieldTypes() { + + /** + * Text field type. + */ + this.fieldTypeService.registerFieldType(Field.Type.TEXT, TextFieldComponent); + + /** + * Email address field type. + */ + this.fieldTypeService.registerFieldType(Field.Type.EMAIL, EmailFieldComponent); + + /** + * Numeric field type. + */ + this.fieldTypeService.registerFieldType(Field.Type.NUMERIC, NumberFieldComponent); + + /** + * Boolean field type. + */ + this.fieldTypeService.registerFieldType(Field.Type.BOOLEAN, CheckboxFieldComponent); + + /** + * Username field type. Identical in principle to a text field, but may + * have different semantics. + */ + this.fieldTypeService.registerFieldType(Field.Type.USERNAME, UsernameFieldComponent); + + /** + * Password field type. Similar to a text field, but the contents of + * the field are masked. + */ + this.fieldTypeService.registerFieldType(Field.Type.PASSWORD, PasswordFieldComponent); + + /** + * Enumerated field type. The user is presented a finite list of values + * to choose from. + */ + this.fieldTypeService.registerFieldType(Field.Type.ENUM, SelectFieldComponent); + + /** + * Multiline field type. The user may enter multiple lines of text. + */ + this.fieldTypeService.registerFieldType(Field.Type.MULTILINE, TextAreaFieldComponent); + + /** + * Field type which allows selection of languages. The languages + * displayed are the set of languages supported by the Guacamole web + * application. Legal values are valid language IDs, as dictated by + * the filenames of Guacamole's available translations. + */ + this.fieldTypeService.registerFieldType(Field.Type.LANGUAGE, LanguageFieldComponent); + + /** + * Field type which allows selection of time zones. + */ + this.fieldTypeService.registerFieldType(Field.Type.TIMEZONE, TimeZoneFieldComponent); + + /** + * Field type which allows selection of individual dates. + */ + this.fieldTypeService.registerFieldType(Field.Type.DATE, DateFieldComponent); + + /** + * Field type which allows selection of times of day. + */ + this.fieldTypeService.registerFieldType(Field.Type.TIME, TimeFieldComponent); + + /** + * Field type which allows selection of color schemes accepted by the + * Guacamole server terminal emulator and protocols which leverage it. + */ + this.fieldTypeService.registerFieldType(Field.Type.TERMINAL_COLOR_SCHEME, TerminalColorSchemeFieldComponent); + + /** + * Field type that supports redirecting the client browser to another + * URL. + */ + this.fieldTypeService.registerFieldType(Field.Type.REDIRECT, RedirectFieldComponent); + } + + /** + * Given form content and an arbitrary prefix, returns a corresponding + * CSS class object as would be provided to the ngClass directive that + * assigns a content-specific CSS class based on the prefix and + * form/field name. Generated class names follow the lowercase with + * dashes naming convention. For example, if the prefix is "field-" and + * the provided content is a field with the name "Swap red/blue", the + * object { 'field-swap-red-blue' : true } would be returned. + * + * @param prefix + * The arbitrary prefix to prepend to the name of the generated CSS + * class. + * + * @param content + * The form or field whose name should be used to produce the CSS + * class name. + * + * @param object + * The optional base ngClass object that should be used to provide + * additional name/value pairs within the returned object. + * + * @return + * The ngClass object based on the provided object and defining a + * CSS class name for the given content. + */ + getClasses(prefix: string, content?: Form | Field, object: Record = {}): Record { + + // Perform no transformation if there is no content or + // corresponding name + if (!content || !content.name) + return object; + + // Transform content name and prefix into lowercase-with-dashes + // CSS class name + const className = prefix + content.name.replace(/[^a-zA-Z0-9]+/g, '-').toLowerCase(); + + // Add CSS class name to provided base object (without touching + // base object) + const classes = { ...object }; + classes[className] = true; + return classes; + + } + + /** + * Creates a component for the field associated with the given name to the given + * scope, producing a distinct and independent DOM Element which functions + * as an instance of that field. The scope object provided must include at + * least the following properties since they are copied to the created + * component: + * + * namespace: + * A String which defines the unique namespace associated the + * translation strings used by the form using a field of this type. + * + * fieldId: + * A String value which is reasonably likely to be unique and may + * be used to associate the main element of the field with its + * label. + * + * field: + * The Field object that is being rendered, representing a field of + * this type. + * + * model: + * The current String value of the field, if any. + * + * disabled: + * A boolean value which is true if the field should be disabled. + * If false or undefined, the field should be enabled. + * + * focused: + * A boolean value which is true if the field should be focused. + * + * + * @param fieldContainer + * The DOM Element to which the field component should be added as a child. + * + * @param fieldType + * The name of the field type defining the nature of the element to be + * created. + * + * @param scope + * The scope from which the properties of the field will be copied. + * + * @return + * A Promise which resolves to the compiled Element. If an error occurs + * while retrieving the field type, this Promise will be rejected. + */ + insertFieldElement(fieldContainer: Element, fieldType: string, scope: FormFieldComponent): Promise> { + + // TODO: Add some comments + + return new Promise((resolve, reject) => { + + // Ensure field type is defined + const componentToInject: Type | null = this.fieldTypeService.getComponent(fieldType); + if (!(componentToInject && this.viewContainer)) { + reject(); + return; + } + + const componentRef = this.insertFieldElementInternal(componentToInject, fieldContainer, scope); + + if (!componentRef) { + reject(); + return; + } + + this.cdr?.detectChanges(); + resolve(componentRef); + + }); + } + + /** + * TODO: Document/rename + * @param componentType + * @param fieldContainer + * @param scope + * @private + */ + private insertFieldElementInternal(componentType: Type, fieldContainer: Element, scope: FormFieldComponent): ComponentRef | null { + + if (!this.viewContainer) { + return null; + } + + // Clear the container first + this.viewContainer.clear(); + + // Create a component using the factory and add it to the container + const componentRef = this.viewContainer.createComponent(componentType); + + // Copy properties from scope to the component instance + componentRef.instance.namespace = scope.namespace; + componentRef.instance.field = scope.field; + componentRef.instance.control = scope.control; + componentRef.instance.disabled = scope.disabled; + componentRef.instance.focused = scope.focused; + componentRef.instance.fieldId = scope.fieldId; + + // Get the native element of the created component + const nativeElement = componentRef.location.nativeElement; + + // Insert the created component after the target element + this.renderer.appendChild(fieldContainer, nativeElement); + + return componentRef; + } + + setViewContainer(viewContainer: ViewContainerRef) { + this.viewContainer = viewContainer; + } + + setChangeDetectorRef(cdr: ChangeDetectorRef) { + this.cdr = cdr; + } + + /** + * Creates a FormGroup object based on the given forms. + * The FormGroup object will contain a FormControl for each field in each form. + * The name of each FormControl will be the name of the corresponding field. + * + * @param forms + * The forms to be included in the form group. + */ + getFormGroup(forms: Form[]): FormGroup { + + const formGroup = new FormGroup({}); + + for (let i = 0; i < forms.length; i++) { + const form = forms[i]; + + for (const field of form.fields) { + formGroup.addControl(field.name, new FormControl('')); + } + + } + + return formGroup; + + } + + /** + * Produce set of forms from any given content. + * + * @param content + * The content to be converted to an array of forms. + */ + asFormArray(content?: Form[] | Form | Field[] | Field | null): Form[] { + + // If no content provided, there are no forms + if (!content) { + return []; + } + + // Ensure content is an array + if (!isArray(content)) + content = [content] as Form[] | Field[]; + + // If content is an array of fields, convert to an array of forms + if (this.isFieldArray(content)) { + content = [{ + fields: content + }]; + } + + // Content is now an array of forms + return content; + + } + + /** + * Determines whether the given object is an array of fields. + * + * @param obj + * The object to test. + * + * @returns + * true if the given object appears to be an array of + * fields, false otherwise. + */ + isFieldArray(obj: Form[] | Field[]): obj is Field[] { + return !!obj.length && !this.isForm(obj[0]); + } + + /** + * Determines whether the given object is a form, under the + * assumption that the object is either a form or a field. + * + * @param obj + * The object to test. + * + * @returns + * true if the given object appears to be a form, false + * otherwise. + */ + isForm(obj: Form | Field): obj is Form { + return 'name' in obj && 'fields' in obj; + } + + +} diff --git a/guacamole/src/main/frontend/src/app/form/styles/form-field.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/form-field.css similarity index 100% rename from guacamole/src/main/frontend/src/app/form/styles/form-field.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/form-field.css diff --git a/guacamole/src/main/frontend/src/app/form/styles/form.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/form.css similarity index 100% rename from guacamole/src/main/frontend/src/app/form/styles/form.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/form.css diff --git a/guacamole/src/main/frontend/src/app/form/styles/redirect-field.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/redirect-field.css similarity index 100% rename from guacamole/src/main/frontend/src/app/form/styles/redirect-field.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/redirect-field.css diff --git a/guacamole/src/main/frontend/src/app/form/styles/terminal-color-scheme-field.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/terminal-color-scheme-field.css similarity index 92% rename from guacamole/src/main/frontend/src/app/form/styles/terminal-color-scheme-field.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/terminal-color-scheme-field.css index 01eac1ac7f..f8de926427 100644 --- a/guacamole/src/main/frontend/src/app/form/styles/terminal-color-scheme-field.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/styles/terminal-color-scheme-field.css @@ -64,20 +64,20 @@ } -.terminal-color-scheme-field .guac-input-color.read-only { +.terminal-color-scheme-field .guac-input-color .color-picker.read-only { cursor: not-allowed; } -.terminal-color-scheme-field .guac-input-color.dark { +.terminal-color-scheme-field .guac-input-color .color-picker.dark { color: white; } -.terminal-color-scheme-field .palette .guac-input-color { +.terminal-color-scheme-field .palette .guac-input-color .color-picker { font-weight: bold; } /* Hide palette numbers unless color scheme details are visible */ -.terminal-color-scheme-field.custom-color-scheme-details-hidden .custom-color-scheme .palette .guac-input-color { +.terminal-color-scheme-field.custom-color-scheme-details-hidden .custom-color-scheme .palette .guac-input-color .color-picker { color: transparent; } diff --git a/guacamole/src/main/frontend/src/app/form/controllers/checkboxFieldController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.spec.ts similarity index 60% rename from guacamole/src/main/frontend/src/app/form/controllers/checkboxFieldController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.spec.ts index bb2676c5bb..3f71bd637b 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/checkboxFieldController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.spec.ts @@ -17,21 +17,28 @@ * under the License. */ +import { ColorScheme } from './ColorScheme'; -/** - * Controller for checkbox fields. - */ -angular.module('form').controller('checkboxFieldController', ['$scope', - function checkboxFieldController($scope) { +describe('ColorScheme', () => { + + let colorScheme: ColorScheme; + + beforeEach(() => { + colorScheme = new ColorScheme(); + }); - // Update typed value when model is changed - $scope.$watch('model', function modelChanged(model) { - $scope.typedValue = (model === $scope.field.options[0]); + it('should create an instance', () => { + expect(colorScheme).toBeTruthy(); }); - // Update string value in model when typed value is changed - $scope.$watch('typedValue', function typedValueChanged(typedValue) { - $scope.model = (typedValue ? $scope.field.options[0] : ''); + it('convert hex to x11 and back', () => { + const hex = '#A01212'; + const x11 = colorScheme.fromHexColor(hex); + + expect(x11).toBe('rgb:A0/12/12'); + + const hex2 = colorScheme.toHexColor(x11 as string); + expect(hex2).toEqual(hex); }); -}]); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.ts new file mode 100644 index 0000000000..5113f85ec7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/form/types/ColorScheme.ts @@ -0,0 +1,938 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import _ from 'lodash'; + +/** + * Intermediate representation of a custom color scheme which can be + * converted to the color scheme format used by Guacamole's terminal + * emulator. All colors must be represented in the six-digit hexadecimal + * RGB notation used by HTML ("#000000" for black, etc.). + */ +export class ColorScheme { + + /** + * The terminal background color. This will be the default foreground + * color of the Guacamole terminal emulator ("#000000") by default. + */ + background: string; + + /** + * The terminal foreground color. This will be the default foreground + * color of the Guacamole terminal emulator ("#999999") by default. + */ + foreground: string; + + /** + * The terminal color palette. Default values are provided for the + * normal 16 terminal colors using the default values of the Guacamole + * terminal emulator, however the terminal emulator and this + * representation support up to 256 colors. + */ + colors: string[]; + + /** + * The string which was parsed to produce this ColorScheme instance, if + * ColorScheme.fromString() was used to produce this ColorScheme. + * + * @private + */ + _originalString?: string; + + /** + * Creates a new ColorScheme. This constructor initializes the properties of the + * new ColorScheme with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ColorScheme. + */ + constructor(template: Partial = {}) { + this.background = template.background || '#000000'; + this.foreground = template.foreground || '#999999'; + this.colors = template.colors || [ + + // Normal colors + '#000000', // Black + '#993E3E', // Red + '#3E993E', // Green + '#99993E', // Brown + '#3E3E99', // Blue + '#993E99', // Magenta + '#3E9999', // Cyan + '#999999', // White + + // Intense colors + '#3E3E3E', // Black + '#FF6767', // Red + '#67FF67', // Green + '#FFFF67', // Brown + '#6767FF', // Blue + '#FF67FF', // Magenta + '#67FFFF', // Cyan + '#FFFFFF' // White + + ]; + + this._originalString = template._originalString; + + } + + /** + * Given a color string in the standard 6-digit hexadecimal RGB format, + * returns a X11 color spec which represents the same color. + * + * @param color + * The hexadecimal color string to convert. + * + * @returns + * The X11 color spec representing the same color as the given + * hexadecimal string, or undefined if the given string is not a valid + * 6-digit hexadecimal RGB color. + */ + fromHexColor(color: string): string | undefined { + + const groups = /^#([0-9a-fA-F]{2})([0-9a-fA-F]{2})([0-9a-fA-F]{2})$/.exec(color); + if (!groups) + return undefined; + + return 'rgb:' + groups[1] + '/' + groups[2] + '/' + groups[3]; + + } + + /** + * Parses the same subset of the X11 color spec supported by the Guacamole + * terminal emulator (the "rgb:*" format), returning the equivalent 6-digit + * hexadecimal color string supported by the ColorScheme representation. + * The X11 color spec defined by Xlib's XParseColor(). The human-readable + * color names supported by the Guacamole terminal emulator (the same color + * names as supported by xterm) may also be used. + * + * @param color + * The X11 color spec to parse, or the name of a known named color. + * + * @returns + * The 6-digit hexadecimal color string which represents the same color + * as the given X11 color spec/name, or undefined if the given spec/name is + * invalid. + */ + toHexColor(color: string): string | undefined { + + /** + * Shifts or truncates the given hexadecimal string such that it + * contains exactly two hexadecimal digits, as required by any + * individual color component of the 6-digit hexadecimal RGB format. + * + * @param component + * The hexadecimal string to shift or truncate to two digits. + * + * @returns + * A new 2-digit hexadecimal string containing the same digits as + * the provided string, shifted or truncated as necessary to fit + * within the 2-digit length limit. + */ + const toHexComponent = (component: string): string => (component + '0').substring(0, 2).toUpperCase(); + + // Attempt to parse any non-RGB color as a named color + const groups = /^rgb:([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})\/([0-9A-Fa-f]{1,4})$/.exec(color); + if (!groups) + return ColorScheme.NAMED_COLORS[color.toLowerCase()] || undefined; + + // Convert to standard 6-digit hexadecimal RGB format + return '#' + toHexComponent(groups[1]) + toHexComponent(groups[2]) + toHexComponent(groups[3]); + + } + + /** + * Converts the given string representation of a color scheme which is + * supported by the Guacamole terminal emulator to a corresponding, + * intermediate ColorScheme object. + * + * @param str + * An arbitrary color scheme, in the string format supported by the + * Guacamole terminal emulator. + * + * @returns + * A new ColorScheme instance which represents the same color scheme as + * the given string. + */ + static fromString(str: string): ColorScheme { + + const scheme = new ColorScheme({ _originalString: str }); + + // For each semicolon-separated statement in the provided color scheme + const statements = str.split(/;/); + for (let i = 0; i < statements.length; i++) { + + // Skip any statements which cannot be parsed + const statement = statements[i]; + const groups = /^\s*(background|foreground|color([0-9]+))\s*:\s*(\S*)\s*$/.exec(statement); + if (!groups) + continue; + + // If the statement is valid and contains a valid color, map that + // color to the appropriate property of the ColorScheme object + const color = scheme.toHexColor(groups[3]); + if (color) { + if (groups[1] === 'background') + scheme.background = color; + else if (groups[1] === 'foreground') + scheme.foreground = color; + else + scheme.colors[parseInt(groups[2])] = color; + } + + } + return scheme; + + } + + /** + * Returns whether the two given color schemes define the exact same + * colors. + * + * @param a + * The first ColorScheme to compare. + * + * @param b + * The second ColorScheme to compare. + * + * @returns + * true if both color schemes contain the same colors, false otherwise. + */ + static equals(a: ColorScheme, b: ColorScheme): boolean { + return a.foreground === b.foreground + && a.background === b.background + && _.isEqual(a.colors, b.colors); + } + + /** + * Converts the given ColorScheme to a string representation which is + * supported by the Guacamole terminal emulator. + * + * @param scheme + * The ColorScheme to convert to a string. + * + * @returns + * The given color scheme, converted to the string format supported by + * the Guacamole terminal emulator. + */ + static toString(scheme: ColorScheme): string { + + // Use originally-provided string if it equates to the exact same color scheme + if (!_.isUndefined(scheme._originalString) && ColorScheme.equals(scheme, ColorScheme.fromString(scheme._originalString))) + return scheme._originalString; + + // Add background and foreground + let str = 'background: ' + scheme.fromHexColor(scheme.background) + ';\n' + + 'foreground: ' + scheme.fromHexColor(scheme.foreground) + ';'; + + // Add color definitions for each palette entry + for (const index in scheme.colors) + str += '\ncolor' + index + ': ' + scheme.fromHexColor(scheme.colors[index]) + ';'; + + return str; + + } + + /** + * The set of all named colors supported by the Guacamole terminal + * emulator and their corresponding 6-digit hexadecimal RGB + * representations. This set should contain all colors supported by xterm. + */ + static readonly NAMED_COLORS: Record = { + 'aliceblue' : '#F0F8FF', + 'antiquewhite' : '#FAEBD7', + 'antiquewhite1' : '#FFEFDB', + 'antiquewhite2' : '#EEDFCC', + 'antiquewhite3' : '#CDC0B0', + 'antiquewhite4' : '#8B8378', + 'aqua' : '#00FFFF', + 'aquamarine' : '#7FFFD4', + 'aquamarine1' : '#7FFFD4', + 'aquamarine2' : '#76EEC6', + 'aquamarine3' : '#66CDAA', + 'aquamarine4' : '#458B74', + 'azure' : '#F0FFFF', + 'azure1' : '#F0FFFF', + 'azure2' : '#E0EEEE', + 'azure3' : '#C1CDCD', + 'azure4' : '#838B8B', + 'beige' : '#F5F5DC', + 'bisque' : '#FFE4C4', + 'bisque1' : '#FFE4C4', + 'bisque2' : '#EED5B7', + 'bisque3' : '#CDB79E', + 'bisque4' : '#8B7D6B', + 'black' : '#000000', + 'blanchedalmond' : '#FFEBCD', + 'blue' : '#0000FF', + 'blue1' : '#0000FF', + 'blue2' : '#0000EE', + 'blue3' : '#0000CD', + 'blue4' : '#00008B', + 'blueviolet' : '#8A2BE2', + 'brown' : '#A52A2A', + 'brown1' : '#FF4040', + 'brown2' : '#EE3B3B', + 'brown3' : '#CD3333', + 'brown4' : '#8B2323', + 'burlywood' : '#DEB887', + 'burlywood1' : '#FFD39B', + 'burlywood2' : '#EEC591', + 'burlywood3' : '#CDAA7D', + 'burlywood4' : '#8B7355', + 'cadetblue' : '#5F9EA0', + 'cadetblue1' : '#98F5FF', + 'cadetblue2' : '#8EE5EE', + 'cadetblue3' : '#7AC5CD', + 'cadetblue4' : '#53868B', + 'chartreuse' : '#7FFF00', + 'chartreuse1' : '#7FFF00', + 'chartreuse2' : '#76EE00', + 'chartreuse3' : '#66CD00', + 'chartreuse4' : '#458B00', + 'chocolate' : '#D2691E', + 'chocolate1' : '#FF7F24', + 'chocolate2' : '#EE7621', + 'chocolate3' : '#CD661D', + 'chocolate4' : '#8B4513', + 'coral' : '#FF7F50', + 'coral1' : '#FF7256', + 'coral2' : '#EE6A50', + 'coral3' : '#CD5B45', + 'coral4' : '#8B3E2F', + 'cornflowerblue' : '#6495ED', + 'cornsilk' : '#FFF8DC', + 'cornsilk1' : '#FFF8DC', + 'cornsilk2' : '#EEE8CD', + 'cornsilk3' : '#CDC8B1', + 'cornsilk4' : '#8B8878', + 'crimson' : '#DC143C', + 'cyan' : '#00FFFF', + 'cyan1' : '#00FFFF', + 'cyan2' : '#00EEEE', + 'cyan3' : '#00CDCD', + 'cyan4' : '#008B8B', + 'darkblue' : '#00008B', + 'darkcyan' : '#008B8B', + 'darkgoldenrod' : '#B8860B', + 'darkgoldenrod1' : '#FFB90F', + 'darkgoldenrod2' : '#EEAD0E', + 'darkgoldenrod3' : '#CD950C', + 'darkgoldenrod4' : '#8B6508', + 'darkgray' : '#A9A9A9', + 'darkgreen' : '#006400', + 'darkgrey' : '#A9A9A9', + 'darkkhaki' : '#BDB76B', + 'darkmagenta' : '#8B008B', + 'darkolivegreen' : '#556B2F', + 'darkolivegreen1' : '#CAFF70', + 'darkolivegreen2' : '#BCEE68', + 'darkolivegreen3' : '#A2CD5A', + 'darkolivegreen4' : '#6E8B3D', + 'darkorange' : '#FF8C00', + 'darkorange1' : '#FF7F00', + 'darkorange2' : '#EE7600', + 'darkorange3' : '#CD6600', + 'darkorange4' : '#8B4500', + 'darkorchid' : '#9932CC', + 'darkorchid1' : '#BF3EFF', + 'darkorchid2' : '#B23AEE', + 'darkorchid3' : '#9A32CD', + 'darkorchid4' : '#68228B', + 'darkred' : '#8B0000', + 'darksalmon' : '#E9967A', + 'darkseagreen' : '#8FBC8F', + 'darkseagreen1' : '#C1FFC1', + 'darkseagreen2' : '#B4EEB4', + 'darkseagreen3' : '#9BCD9B', + 'darkseagreen4' : '#698B69', + 'darkslateblue' : '#483D8B', + 'darkslategray' : '#2F4F4F', + 'darkslategray1' : '#97FFFF', + 'darkslategray2' : '#8DEEEE', + 'darkslategray3' : '#79CDCD', + 'darkslategray4' : '#528B8B', + 'darkslategrey' : '#2F4F4F', + 'darkturquoise' : '#00CED1', + 'darkviolet' : '#9400D3', + 'deeppink' : '#FF1493', + 'deeppink1' : '#FF1493', + 'deeppink2' : '#EE1289', + 'deeppink3' : '#CD1076', + 'deeppink4' : '#8B0A50', + 'deepskyblue' : '#00BFFF', + 'deepskyblue1' : '#00BFFF', + 'deepskyblue2' : '#00B2EE', + 'deepskyblue3' : '#009ACD', + 'deepskyblue4' : '#00688B', + 'dimgray' : '#696969', + 'dimgrey' : '#696969', + 'dodgerblue' : '#1E90FF', + 'dodgerblue1' : '#1E90FF', + 'dodgerblue2' : '#1C86EE', + 'dodgerblue3' : '#1874CD', + 'dodgerblue4' : '#104E8B', + 'firebrick' : '#B22222', + 'firebrick1' : '#FF3030', + 'firebrick2' : '#EE2C2C', + 'firebrick3' : '#CD2626', + 'firebrick4' : '#8B1A1A', + 'floralwhite' : '#FFFAF0', + 'forestgreen' : '#228B22', + 'fuchsia' : '#FF00FF', + 'gainsboro' : '#DCDCDC', + 'ghostwhite' : '#F8F8FF', + 'gold' : '#FFD700', + 'gold1' : '#FFD700', + 'gold2' : '#EEC900', + 'gold3' : '#CDAD00', + 'gold4' : '#8B7500', + 'goldenrod' : '#DAA520', + 'goldenrod1' : '#FFC125', + 'goldenrod2' : '#EEB422', + 'goldenrod3' : '#CD9B1D', + 'goldenrod4' : '#8B6914', + 'gray' : '#BEBEBE', + 'gray0' : '#000000', + 'gray1' : '#030303', + 'gray10' : '#1A1A1A', + 'gray100' : '#FFFFFF', + 'gray11' : '#1C1C1C', + 'gray12' : '#1F1F1F', + 'gray13' : '#212121', + 'gray14' : '#242424', + 'gray15' : '#262626', + 'gray16' : '#292929', + 'gray17' : '#2B2B2B', + 'gray18' : '#2E2E2E', + 'gray19' : '#303030', + 'gray2' : '#050505', + 'gray20' : '#333333', + 'gray21' : '#363636', + 'gray22' : '#383838', + 'gray23' : '#3B3B3B', + 'gray24' : '#3D3D3D', + 'gray25' : '#404040', + 'gray26' : '#424242', + 'gray27' : '#454545', + 'gray28' : '#474747', + 'gray29' : '#4A4A4A', + 'gray3' : '#080808', + 'gray30' : '#4D4D4D', + 'gray31' : '#4F4F4F', + 'gray32' : '#525252', + 'gray33' : '#545454', + 'gray34' : '#575757', + 'gray35' : '#595959', + 'gray36' : '#5C5C5C', + 'gray37' : '#5E5E5E', + 'gray38' : '#616161', + 'gray39' : '#636363', + 'gray4' : '#0A0A0A', + 'gray40' : '#666666', + 'gray41' : '#696969', + 'gray42' : '#6B6B6B', + 'gray43' : '#6E6E6E', + 'gray44' : '#707070', + 'gray45' : '#737373', + 'gray46' : '#757575', + 'gray47' : '#787878', + 'gray48' : '#7A7A7A', + 'gray49' : '#7D7D7D', + 'gray5' : '#0D0D0D', + 'gray50' : '#7F7F7F', + 'gray51' : '#828282', + 'gray52' : '#858585', + 'gray53' : '#878787', + 'gray54' : '#8A8A8A', + 'gray55' : '#8C8C8C', + 'gray56' : '#8F8F8F', + 'gray57' : '#919191', + 'gray58' : '#949494', + 'gray59' : '#969696', + 'gray6' : '#0F0F0F', + 'gray60' : '#999999', + 'gray61' : '#9C9C9C', + 'gray62' : '#9E9E9E', + 'gray63' : '#A1A1A1', + 'gray64' : '#A3A3A3', + 'gray65' : '#A6A6A6', + 'gray66' : '#A8A8A8', + 'gray67' : '#ABABAB', + 'gray68' : '#ADADAD', + 'gray69' : '#B0B0B0', + 'gray7' : '#121212', + 'gray70' : '#B3B3B3', + 'gray71' : '#B5B5B5', + 'gray72' : '#B8B8B8', + 'gray73' : '#BABABA', + 'gray74' : '#BDBDBD', + 'gray75' : '#BFBFBF', + 'gray76' : '#C2C2C2', + 'gray77' : '#C4C4C4', + 'gray78' : '#C7C7C7', + 'gray79' : '#C9C9C9', + 'gray8' : '#141414', + 'gray80' : '#CCCCCC', + 'gray81' : '#CFCFCF', + 'gray82' : '#D1D1D1', + 'gray83' : '#D4D4D4', + 'gray84' : '#D6D6D6', + 'gray85' : '#D9D9D9', + 'gray86' : '#DBDBDB', + 'gray87' : '#DEDEDE', + 'gray88' : '#E0E0E0', + 'gray89' : '#E3E3E3', + 'gray9' : '#171717', + 'gray90' : '#E5E5E5', + 'gray91' : '#E8E8E8', + 'gray92' : '#EBEBEB', + 'gray93' : '#EDEDED', + 'gray94' : '#F0F0F0', + 'gray95' : '#F2F2F2', + 'gray96' : '#F5F5F5', + 'gray97' : '#F7F7F7', + 'gray98' : '#FAFAFA', + 'gray99' : '#FCFCFC', + 'green' : '#00FF00', + 'green1' : '#00FF00', + 'green2' : '#00EE00', + 'green3' : '#00CD00', + 'green4' : '#008B00', + 'greenyellow' : '#ADFF2F', + 'grey' : '#BEBEBE', + 'grey0' : '#000000', + 'grey1' : '#030303', + 'grey10' : '#1A1A1A', + 'grey100' : '#FFFFFF', + 'grey11' : '#1C1C1C', + 'grey12' : '#1F1F1F', + 'grey13' : '#212121', + 'grey14' : '#242424', + 'grey15' : '#262626', + 'grey16' : '#292929', + 'grey17' : '#2B2B2B', + 'grey18' : '#2E2E2E', + 'grey19' : '#303030', + 'grey2' : '#050505', + 'grey20' : '#333333', + 'grey21' : '#363636', + 'grey22' : '#383838', + 'grey23' : '#3B3B3B', + 'grey24' : '#3D3D3D', + 'grey25' : '#404040', + 'grey26' : '#424242', + 'grey27' : '#454545', + 'grey28' : '#474747', + 'grey29' : '#4A4A4A', + 'grey3' : '#080808', + 'grey30' : '#4D4D4D', + 'grey31' : '#4F4F4F', + 'grey32' : '#525252', + 'grey33' : '#545454', + 'grey34' : '#575757', + 'grey35' : '#595959', + 'grey36' : '#5C5C5C', + 'grey37' : '#5E5E5E', + 'grey38' : '#616161', + 'grey39' : '#636363', + 'grey4' : '#0A0A0A', + 'grey40' : '#666666', + 'grey41' : '#696969', + 'grey42' : '#6B6B6B', + 'grey43' : '#6E6E6E', + 'grey44' : '#707070', + 'grey45' : '#737373', + 'grey46' : '#757575', + 'grey47' : '#787878', + 'grey48' : '#7A7A7A', + 'grey49' : '#7D7D7D', + 'grey5' : '#0D0D0D', + 'grey50' : '#7F7F7F', + 'grey51' : '#828282', + 'grey52' : '#858585', + 'grey53' : '#878787', + 'grey54' : '#8A8A8A', + 'grey55' : '#8C8C8C', + 'grey56' : '#8F8F8F', + 'grey57' : '#919191', + 'grey58' : '#949494', + 'grey59' : '#969696', + 'grey6' : '#0F0F0F', + 'grey60' : '#999999', + 'grey61' : '#9C9C9C', + 'grey62' : '#9E9E9E', + 'grey63' : '#A1A1A1', + 'grey64' : '#A3A3A3', + 'grey65' : '#A6A6A6', + 'grey66' : '#A8A8A8', + 'grey67' : '#ABABAB', + 'grey68' : '#ADADAD', + 'grey69' : '#B0B0B0', + 'grey7' : '#121212', + 'grey70' : '#B3B3B3', + 'grey71' : '#B5B5B5', + 'grey72' : '#B8B8B8', + 'grey73' : '#BABABA', + 'grey74' : '#BDBDBD', + 'grey75' : '#BFBFBF', + 'grey76' : '#C2C2C2', + 'grey77' : '#C4C4C4', + 'grey78' : '#C7C7C7', + 'grey79' : '#C9C9C9', + 'grey8' : '#141414', + 'grey80' : '#CCCCCC', + 'grey81' : '#CFCFCF', + 'grey82' : '#D1D1D1', + 'grey83' : '#D4D4D4', + 'grey84' : '#D6D6D6', + 'grey85' : '#D9D9D9', + 'grey86' : '#DBDBDB', + 'grey87' : '#DEDEDE', + 'grey88' : '#E0E0E0', + 'grey89' : '#E3E3E3', + 'grey9' : '#171717', + 'grey90' : '#E5E5E5', + 'grey91' : '#E8E8E8', + 'grey92' : '#EBEBEB', + 'grey93' : '#EDEDED', + 'grey94' : '#F0F0F0', + 'grey95' : '#F2F2F2', + 'grey96' : '#F5F5F5', + 'grey97' : '#F7F7F7', + 'grey98' : '#FAFAFA', + 'grey99' : '#FCFCFC', + 'honeydew' : '#F0FFF0', + 'honeydew1' : '#F0FFF0', + 'honeydew2' : '#E0EEE0', + 'honeydew3' : '#C1CDC1', + 'honeydew4' : '#838B83', + 'hotpink' : '#FF69B4', + 'hotpink1' : '#FF6EB4', + 'hotpink2' : '#EE6AA7', + 'hotpink3' : '#CD6090', + 'hotpink4' : '#8B3A62', + 'indianred' : '#CD5C5C', + 'indianred1' : '#FF6A6A', + 'indianred2' : '#EE6363', + 'indianred3' : '#CD5555', + 'indianred4' : '#8B3A3A', + 'indigo' : '#4B0082', + 'ivory' : '#FFFFF0', + 'ivory1' : '#FFFFF0', + 'ivory2' : '#EEEEE0', + 'ivory3' : '#CDCDC1', + 'ivory4' : '#8B8B83', + 'khaki' : '#F0E68C', + 'khaki1' : '#FFF68F', + 'khaki2' : '#EEE685', + 'khaki3' : '#CDC673', + 'khaki4' : '#8B864E', + 'lavender' : '#E6E6FA', + 'lavenderblush' : '#FFF0F5', + 'lavenderblush1' : '#FFF0F5', + 'lavenderblush2' : '#EEE0E5', + 'lavenderblush3' : '#CDC1C5', + 'lavenderblush4' : '#8B8386', + 'lawngreen' : '#7CFC00', + 'lemonchiffon' : '#FFFACD', + 'lemonchiffon1' : '#FFFACD', + 'lemonchiffon2' : '#EEE9BF', + 'lemonchiffon3' : '#CDC9A5', + 'lemonchiffon4' : '#8B8970', + 'lightblue' : '#ADD8E6', + 'lightblue1' : '#BFEFFF', + 'lightblue2' : '#B2DFEE', + 'lightblue3' : '#9AC0CD', + 'lightblue4' : '#68838B', + 'lightcoral' : '#F08080', + 'lightcyan' : '#E0FFFF', + 'lightcyan1' : '#E0FFFF', + 'lightcyan2' : '#D1EEEE', + 'lightcyan3' : '#B4CDCD', + 'lightcyan4' : '#7A8B8B', + 'lightgoldenrod' : '#EEDD82', + 'lightgoldenrod1' : '#FFEC8B', + 'lightgoldenrod2' : '#EEDC82', + 'lightgoldenrod3' : '#CDBE70', + 'lightgoldenrod4' : '#8B814C', + 'lightgoldenrodyellow': '#FAFAD2', + 'lightgray' : '#D3D3D3', + 'lightgreen' : '#90EE90', + 'lightgrey' : '#D3D3D3', + 'lightpink' : '#FFB6C1', + 'lightpink1' : '#FFAEB9', + 'lightpink2' : '#EEA2AD', + 'lightpink3' : '#CD8C95', + 'lightpink4' : '#8B5F65', + 'lightsalmon' : '#FFA07A', + 'lightsalmon1' : '#FFA07A', + 'lightsalmon2' : '#EE9572', + 'lightsalmon3' : '#CD8162', + 'lightsalmon4' : '#8B5742', + 'lightseagreen' : '#20B2AA', + 'lightskyblue' : '#87CEFA', + 'lightskyblue1' : '#B0E2FF', + 'lightskyblue2' : '#A4D3EE', + 'lightskyblue3' : '#8DB6CD', + 'lightskyblue4' : '#607B8B', + 'lightslateblue' : '#8470FF', + 'lightslategray' : '#778899', + 'lightslategrey' : '#778899', + 'lightsteelblue' : '#B0C4DE', + 'lightsteelblue1' : '#CAE1FF', + 'lightsteelblue2' : '#BCD2EE', + 'lightsteelblue3' : '#A2B5CD', + 'lightsteelblue4' : '#6E7B8B', + 'lightyellow' : '#FFFFE0', + 'lightyellow1' : '#FFFFE0', + 'lightyellow2' : '#EEEED1', + 'lightyellow3' : '#CDCDB4', + 'lightyellow4' : '#8B8B7A', + 'lime' : '#00FF00', + 'limegreen' : '#32CD32', + 'linen' : '#FAF0E6', + 'magenta' : '#FF00FF', + 'magenta1' : '#FF00FF', + 'magenta2' : '#EE00EE', + 'magenta3' : '#CD00CD', + 'magenta4' : '#8B008B', + 'maroon' : '#B03060', + 'maroon1' : '#FF34B3', + 'maroon2' : '#EE30A7', + 'maroon3' : '#CD2990', + 'maroon4' : '#8B1C62', + 'mediumaquamarine' : '#66CDAA', + 'mediumblue' : '#0000CD', + 'mediumorchid' : '#BA55D3', + 'mediumorchid1' : '#E066FF', + 'mediumorchid2' : '#D15FEE', + 'mediumorchid3' : '#B452CD', + 'mediumorchid4' : '#7A378B', + 'mediumpurple' : '#9370DB', + 'mediumpurple1' : '#AB82FF', + 'mediumpurple2' : '#9F79EE', + 'mediumpurple3' : '#8968CD', + 'mediumpurple4' : '#5D478B', + 'mediumseagreen' : '#3CB371', + 'mediumslateblue' : '#7B68EE', + 'mediumspringgreen' : '#00FA9A', + 'mediumturquoise' : '#48D1CC', + 'mediumvioletred' : '#C71585', + 'midnightblue' : '#191970', + 'mintcream' : '#F5FFFA', + 'mistyrose' : '#FFE4E1', + 'mistyrose1' : '#FFE4E1', + 'mistyrose2' : '#EED5D2', + 'mistyrose3' : '#CDB7B5', + 'mistyrose4' : '#8B7D7B', + 'moccasin' : '#FFE4B5', + 'navajowhite' : '#FFDEAD', + 'navajowhite1' : '#FFDEAD', + 'navajowhite2' : '#EECFA1', + 'navajowhite3' : '#CDB38B', + 'navajowhite4' : '#8B795E', + 'navy' : '#000080', + 'navyblue' : '#000080', + 'oldlace' : '#FDF5E6', + 'olive' : '#808000', + 'olivedrab' : '#6B8E23', + 'olivedrab1' : '#C0FF3E', + 'olivedrab2' : '#B3EE3A', + 'olivedrab3' : '#9ACD32', + 'olivedrab4' : '#698B22', + 'orange' : '#FFA500', + 'orange1' : '#FFA500', + 'orange2' : '#EE9A00', + 'orange3' : '#CD8500', + 'orange4' : '#8B5A00', + 'orangered' : '#FF4500', + 'orangered1' : '#FF4500', + 'orangered2' : '#EE4000', + 'orangered3' : '#CD3700', + 'orangered4' : '#8B2500', + 'orchid' : '#DA70D6', + 'orchid1' : '#FF83FA', + 'orchid2' : '#EE7AE9', + 'orchid3' : '#CD69C9', + 'orchid4' : '#8B4789', + 'palegoldenrod' : '#EEE8AA', + 'palegreen' : '#98FB98', + 'palegreen1' : '#9AFF9A', + 'palegreen2' : '#90EE90', + 'palegreen3' : '#7CCD7C', + 'palegreen4' : '#548B54', + 'paleturquoise' : '#AFEEEE', + 'paleturquoise1' : '#BBFFFF', + 'paleturquoise2' : '#AEEEEE', + 'paleturquoise3' : '#96CDCD', + 'paleturquoise4' : '#668B8B', + 'palevioletred' : '#DB7093', + 'palevioletred1' : '#FF82AB', + 'palevioletred2' : '#EE799F', + 'palevioletred3' : '#CD6889', + 'palevioletred4' : '#8B475D', + 'papayawhip' : '#FFEFD5', + 'peachpuff' : '#FFDAB9', + 'peachpuff1' : '#FFDAB9', + 'peachpuff2' : '#EECBAD', + 'peachpuff3' : '#CDAF95', + 'peachpuff4' : '#8B7765', + 'peru' : '#CD853F', + 'pink' : '#FFC0CB', + 'pink1' : '#FFB5C5', + 'pink2' : '#EEA9B8', + 'pink3' : '#CD919E', + 'pink4' : '#8B636C', + 'plum' : '#DDA0DD', + 'plum1' : '#FFBBFF', + 'plum2' : '#EEAEEE', + 'plum3' : '#CD96CD', + 'plum4' : '#8B668B', + 'powderblue' : '#B0E0E6', + 'purple' : '#A020F0', + 'purple1' : '#9B30FF', + 'purple2' : '#912CEE', + 'purple3' : '#7D26CD', + 'purple4' : '#551A8B', + 'rebeccapurple' : '#663399', + 'red' : '#FF0000', + 'red1' : '#FF0000', + 'red2' : '#EE0000', + 'red3' : '#CD0000', + 'red4' : '#8B0000', + 'rosybrown' : '#BC8F8F', + 'rosybrown1' : '#FFC1C1', + 'rosybrown2' : '#EEB4B4', + 'rosybrown3' : '#CD9B9B', + 'rosybrown4' : '#8B6969', + 'royalblue' : '#4169E1', + 'royalblue1' : '#4876FF', + 'royalblue2' : '#436EEE', + 'royalblue3' : '#3A5FCD', + 'royalblue4' : '#27408B', + 'saddlebrown' : '#8B4513', + 'salmon' : '#FA8072', + 'salmon1' : '#FF8C69', + 'salmon2' : '#EE8262', + 'salmon3' : '#CD7054', + 'salmon4' : '#8B4C39', + 'sandybrown' : '#F4A460', + 'seagreen' : '#2E8B57', + 'seagreen1' : '#54FF9F', + 'seagreen2' : '#4EEE94', + 'seagreen3' : '#43CD80', + 'seagreen4' : '#2E8B57', + 'seashell' : '#FFF5EE', + 'seashell1' : '#FFF5EE', + 'seashell2' : '#EEE5DE', + 'seashell3' : '#CDC5BF', + 'seashell4' : '#8B8682', + 'sienna' : '#A0522D', + 'sienna1' : '#FF8247', + 'sienna2' : '#EE7942', + 'sienna3' : '#CD6839', + 'sienna4' : '#8B4726', + 'silver' : '#C0C0C0', + 'skyblue' : '#87CEEB', + 'skyblue1' : '#87CEFF', + 'skyblue2' : '#7EC0EE', + 'skyblue3' : '#6CA6CD', + 'skyblue4' : '#4A708B', + 'slateblue' : '#6A5ACD', + 'slateblue1' : '#836FFF', + 'slateblue2' : '#7A67EE', + 'slateblue3' : '#6959CD', + 'slateblue4' : '#473C8B', + 'slategray' : '#708090', + 'slategray1' : '#C6E2FF', + 'slategray2' : '#B9D3EE', + 'slategray3' : '#9FB6CD', + 'slategray4' : '#6C7B8B', + 'slategrey' : '#708090', + 'snow' : '#FFFAFA', + 'snow1' : '#FFFAFA', + 'snow2' : '#EEE9E9', + 'snow3' : '#CDC9C9', + 'snow4' : '#8B8989', + 'springgreen' : '#00FF7F', + 'springgreen1' : '#00FF7F', + 'springgreen2' : '#00EE76', + 'springgreen3' : '#00CD66', + 'springgreen4' : '#008B45', + 'steelblue' : '#4682B4', + 'steelblue1' : '#63B8FF', + 'steelblue2' : '#5CACEE', + 'steelblue3' : '#4F94CD', + 'steelblue4' : '#36648B', + 'tan' : '#D2B48C', + 'tan1' : '#FFA54F', + 'tan2' : '#EE9A49', + 'tan3' : '#CD853F', + 'tan4' : '#8B5A2B', + 'teal' : '#008080', + 'thistle' : '#D8BFD8', + 'thistle1' : '#FFE1FF', + 'thistle2' : '#EED2EE', + 'thistle3' : '#CDB5CD', + 'thistle4' : '#8B7B8B', + 'tomato' : '#FF6347', + 'tomato1' : '#FF6347', + 'tomato2' : '#EE5C42', + 'tomato3' : '#CD4F39', + 'tomato4' : '#8B3626', + 'turquoise' : '#40E0D0', + 'turquoise1' : '#00F5FF', + 'turquoise2' : '#00E5EE', + 'turquoise3' : '#00C5CD', + 'turquoise4' : '#00868B', + 'violet' : '#EE82EE', + 'violetred' : '#D02090', + 'violetred1' : '#FF3E96', + 'violetred2' : '#EE3A8C', + 'violetred3' : '#CD3278', + 'violetred4' : '#8B2252', + 'webgray' : '#808080', + 'webgreen' : '#008000', + 'webgrey' : '#808080', + 'webmaroon' : '#800000', + 'webpurple' : '#800080', + 'wheat' : '#F5DEB3', + 'wheat1' : '#FFE7BA', + 'wheat2' : '#EED8AE', + 'wheat3' : '#CDBA96', + 'wheat4' : '#8B7E66', + 'white' : '#FFFFFF', + 'whitesmoke' : '#F5F5F5', + 'x11gray' : '#BEBEBE', + 'x11green' : '#00FF00', + 'x11grey' : '#BEBEBE', + 'x11maroon' : '#B03060', + 'x11purple' : '#A020F0', + 'yellow' : '#FFFF00', + 'yellow1' : '#FFFF00', + 'yellow2' : '#EEEE00', + 'yellow3' : '#CDCD00', + 'yellow4' : '#8B8B00', + 'yellowgreen' : '#9ACD32' + }; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.html new file mode 100644 index 0000000000..ee65a7546c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.html @@ -0,0 +1,26 @@ + + +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.ts new file mode 100644 index 0000000000..32fd43f863 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list-filter/guac-group-list-filter.component.ts @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +/** + * A component which provides a filtering text input field + * to filter connection groups. + */ +@Component({ + selector: 'guac-group-list-filter', + templateUrl: './guac-group-list-filter.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacGroupListFilterComponent { + + /** + * The filter search string to use to restrict the displayed + * connection groups. This subject will emit a new value whenever + * the search string changes. + */ + searchStringChange: BehaviorSubject = new BehaviorSubject(''); + + /** + * The placeholder text to display within the filter input field + * when no filter has been provided. + */ + @Input({ required: true }) placeholder!: string; + + /** + * The filter search string to use to restrict the displayed items. + */ + protected searchString: string | null = null; + + /** + * TODO: Document + */ + protected searchStringChanged(searchString: string) { + this.searchStringChange.next(searchString); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.html new file mode 100644 index 0000000000..42ee8190d8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.html @@ -0,0 +1,63 @@ + + +
      + +
      + + +
      + + + +
      + + +
      + + +
      + + + +
      + + +
      +
      + +
      +
      + +
      +
      +
      +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.ts new file mode 100644 index 0000000000..2292178e8b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/components/guac-group-list/guac-group-list.component.ts @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Component, + Input, + OnChanges, + OnInit, + SimpleChanges, + TemplateRef, + ViewChild, + ViewEncapsulation +} from '@angular/core'; +import { of } from 'rxjs'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { SortService } from '../../../list/services/sort.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { ActiveConnectionService } from '../../../rest/service/active-connection.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { Connection } from '../../../rest/types/Connection'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { GroupListItem } from '../../types/GroupListItem'; + +/** + * A component which displays the contents of a connection group within an + * automatically-paginated view. + */ +@Component({ + selector: 'guac-group-list', + templateUrl: './guac-group-list.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacGroupListComponent implements OnInit, OnChanges { + + /** + * The connection groups to display as a map of data source + * identifier to corresponding root group. + */ + @Input() connectionGroups: Record | null = null; + + /** + * Arbitrary object which shall be made available to the connection + * and connection group templates within the scope as + * context. + */ + @Input() context: any = {}; + + /** + * The map of @link{GroupListItem} type to the URL or ID of the + * Angular template to use when rendering a @link{GroupListItem} of + * that type. The @link{GroupListItem} itself will be within the + * scope of the template as item, and the arbitrary + * context object, if any, will be exposed as context. + * If the template for a type is omitted, items of that type will + * not be rendered. All standard types are defined by + * @link{GroupListItem.Type}, but use of custom types is legal. + */ + @Input({ required: true }) templates!: Record>; + + /** + * Whether the root of the connection group hierarchy given should + * be shown. If false (the default), only the descendants of the + * given connection group will be listed. + */ + @Input() showRootGroup = false; + + /** + * The maximum number of connections or groups to show per page. + */ + @Input() pageSize?: number; + + /** + * A callback which accepts an array of GroupListItems as its sole + * parameter. If provided, the callback will be invoked whenever an + * array of root-level GroupListItems is about to be rendered. + * Changes may be made by this function to that array or to the + * GroupListItems themselves. + */ + @Input() decorator?: (items: GroupListItem[]) => void; + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * Map of data source identifier to the number of active + * connections associated with a given connection identifier. + * If this information is unknown, or there are no active + * connections for a given identifier, no number will be stored. + */ + private connectionCount: Record> = {}; + + /** + * A list of all items which should appear at the root level. As + * connections and connection groups from multiple data sources may + * be included in a guacGroupList, there may be multiple root + * items, even if the root connection group is shown. + */ + rootItems: GroupListItem[] = []; + + /** + * A data source which provides a sorted, paginated list of + * all root-level items. + */ + connectionGroupsDataSource: DataSource | null = null; + + /** + * The sort order to apply to the root items. + */ + private readonly sortOrder = new SortOrder(['weight', 'name']); + + /** + * Inject required services. + */ + constructor(private activeConnectionService: ActiveConnectionService, + private dataSourceService: DataSourceService, + private requestService: RequestService, + private dataSourceBuilderService: DataSourceBuilderService, + protected sortService: SortService) { + } + + /** + * Creates a new data source for the root items. + */ + ngOnInit(): void { + + this.connectionGroupsDataSource = this.dataSourceBuilderService + .getBuilder() + // Start with an empty list + .source(this.rootItems) + // Sort according to the specified sort order + .sort(of(this.sortOrder)) + // Paginate using the GuacPagerComponent + .paginate(this.pager.page) + .build(); + + } + + /** + * Returns the number of active usages of a given connection. + * + * @param dataSource + * The identifier of the data source containing the given + * connection. + * + * @param connection + * The connection whose active connections should be counted. + * + * @returns + * The number of currently-active usages of the given + * connection. + */ + private countActiveConnections(dataSource: string, connection: Connection): number { + if (this.connectionCount[dataSource] === undefined + || connection.identifier === undefined + || this.connectionCount[dataSource][connection.identifier] === undefined) + return 0; + + return (this.connectionCount)[dataSource][connection.identifier]; + } + + /** + * Returns whether a {@link GroupListItem} of the given type can be + * displayed. If there is no template associated with the given + * type, then a {@link GroupListItem} of that type cannot be + * displayed. + * + * @param type + * The type to check. + * + * @returns + * true if the given {@link GroupListItem} type can be displayed, + * false otherwise. + */ + isVisible(type: string): boolean { + return !!this.templates[type]; + } + + /** + * Toggle the open/closed status of a group list item. + * + * @param groupListItem + * The list item to expand, which should represent a + * connection group. + */ + toggleExpanded(groupListItem: GroupListItem): void { + groupListItem.expanded = !groupListItem.expanded; + } + + /** + * Set contents whenever the connection group is assigned or changed. + */ + ngOnChanges(changes: SimpleChanges): void { + if (!changes['connectionGroups']) + return; + + // Reset stored data + const dataSources: string[] = []; + this.rootItems = []; + this.connectionCount = {}; + + // If connection groups are given, add them to the interface + if (this.connectionGroups) { + + // Add each provided connection group + for (const dataSource in this.connectionGroups) { + const connectionGroup = this.connectionGroups[dataSource]; + + let rootItem: GroupListItem; + + // Prepare data source for active connection counting + dataSources.push(dataSource); + this.connectionCount[dataSource] = {}; + + // If the provided connection group is already a + // GroupListItem, no need to create a new item + if (connectionGroup instanceof GroupListItem) + rootItem = connectionGroup; + + // Create root item for current connection group + else + rootItem = GroupListItem.fromConnectionGroup(dataSource, connectionGroup, + this.isVisible(GroupListItem.Type.CONNECTION), + this.isVisible(GroupListItem.Type.SHARING_PROFILE), + this.countActiveConnections.bind(this)); + + // If root group is to be shown, add it as a root item + if (this.showRootGroup) + this.rootItems.push(rootItem); + + // Otherwise, add its children as root items + else { + rootItem.children.forEach(child => { + this.rootItems.push(child); + }); + } + + } + + // Count active connections by connection identifier + this.dataSourceService.apply( + (dataSource: string) => this.activeConnectionService.getActiveConnections(dataSource), + dataSources + ) + .then((activeConnectionMap) => { + + // Within each data source, count each active connection by identifier + for (const dataSource in activeConnectionMap) { + const activeConnections = activeConnectionMap[dataSource]; + + for (const connectionID in activeConnections) { + const activeConnection = activeConnections[connectionID]; + + // If counter already exists, increment + const identifier = activeConnection.connectionIdentifier!; + if (this.connectionCount[dataSource][identifier]) + this.connectionCount[dataSource][identifier]++; + + // Otherwise, initialize counter to 1 + else + this.connectionCount[dataSource][identifier] = 1; + + } + } + + }, this.requestService.PROMISE_DIE); + + } + + // Invoke item decorator, if provided + if (this.decorator) + this.decorator(this.rootItems); + + // Update the data source with the new root items + this.connectionGroupsDataSource?.updateSource(this.rootItems); + + } + + +} diff --git a/guacamole/src/main/frontend/src/app/groupList/groupListModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/group-list.module.ts similarity index 56% rename from guacamole/src/main/frontend/src/app/groupList/groupListModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/group-list.module.ts index eb37ac5c76..17f0c78015 100644 --- a/guacamole/src/main/frontend/src/app/groupList/groupListModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/group-list.module.ts @@ -17,12 +17,31 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { ListModule } from '../list/list.module'; +import { GuacGroupListFilterComponent } from './components/guac-group-list-filter/guac-group-list-filter.component'; +import { GuacGroupListComponent } from './components/guac-group-list/guac-group-list.component'; + /** * Module for displaying the contents of a connection group, allowing the user * to select individual connections or groups. */ -angular.module('groupList', [ - 'navigation', - 'list', - 'rest' -]); +@NgModule({ + declarations: [ + GuacGroupListComponent, + GuacGroupListFilterComponent + ], + imports : [ + CommonModule, + ListModule, + FormsModule + ], + exports : [ + GuacGroupListComponent, + GuacGroupListFilterComponent + ] +}) +export class GroupListModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/ConnectionGroupDataSource.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/ConnectionGroupDataSource.ts new file mode 100644 index 0000000000..01af9c15f4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/ConnectionGroupDataSource.ts @@ -0,0 +1,314 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import _ from 'lodash'; +import { map, Observable, of } from 'rxjs'; +import { FilterService } from '../../list/services/filter.service'; +import { FilterPattern } from '../../list/types/FilterPattern'; +import { ConnectionGroup } from '../../rest/types/ConnectionGroup'; +import { GroupListItem } from './GroupListItem'; + +/** + * A data source which provides a filtered view of a map of + * the given connection groups. + */ +export class ConnectionGroupDataSource { + + /** + * An observable which emits the filtered connection groups. + */ + filteredConnectionGroups!: Observable>; + + /** + * The pattern object to use when filtering connections. + */ + private readonly connectionFilterPattern: FilterPattern; + + /** + * The pattern object to use when filtering connection groups. + */ + private readonly connectionGroupFilterPattern: FilterPattern; + + /** + * Creates a new ConnectionGroupDataSource which provides a filtered view + * of the given connection groups. + * + * @param filterService + * The filter service which will be used to filter the connection groups. + * + * @param source + * The connection groups to filter, as a map of data source + * identifier to corresponding root group. The type of each + * item within the original map is preserved within the + * filtered map. + * + * @param searchString + * The connection groups to filter, as a map of data source + * identifier to corresponding root group. The filtered subset + * of this map will be exposed as filteredConnectionGroups. + * + * @param connectionProperties + * An array of expressions to filter against for each connection in + * the hierarchy of connections and groups in the provided map. + * These expressions must be Angular expressions which resolve to + * properties on the connections in the provided map. + * + * @param connectionGroupProperties + * An array of expressions to filter against for each connection group + * in the hierarchy of connections and groups in the provided map. + * These expressions must be Angular expressions which resolve to + * properties on the connection groups in the provided map. + */ + constructor(private filterService: FilterService, + private source: Record, + private searchString: Observable | null, + private connectionProperties: string[], + private connectionGroupProperties: string[]) { + + this.connectionFilterPattern = new FilterPattern(this.connectionProperties); + this.connectionGroupFilterPattern = new FilterPattern(this.connectionGroupProperties); + + this.computeData(); + } + + /** + * TODO: Document + */ + private computeData(): void { + + this.filteredConnectionGroups = + (this.searchString || of('')) + .pipe( + map((searchString) => { + return this.applyFilter(searchString); + }) + ); + } + + /** + * Sets the source array and recomputes the data. + * + * @param source + * The new source array. + */ + updateSource(source: Record): void { + this.source = source; + this.computeData(); + } + + /** + * Changes the search string used to filter the connection groups + * to the given value. + * + * @param searchString + * The new search string to use when filtering the connection + * groups. + */ + setSearchString(searchString: Observable): void { + this.searchString = searchString; + this.computeData(); + } + + /** + * Applies the current filter predicate, filtering all provided + * connection groups. + * TODO: Document + */ + applyFilter(searchString: string): Record { + + // Do not apply any filtering (and do not flatten) if no + // search string is provided + if (!searchString) { + return this.source; + } + + // Compile filter patterns with new search string + this.connectionFilterPattern.compile(searchString); + this.connectionGroupFilterPattern.compile(searchString); + + // Re-filter any provided groups + const connectionGroups = this.source; + const filteredConnectionGroups: Record = {}; + if (connectionGroups) { + for (const dataSource in connectionGroups) { + const connectionGroup = connectionGroups[dataSource]; + + let filteredGroup: ConnectionGroup | GroupListItem; + + // Flatten and filter depending on type + if (connectionGroup instanceof GroupListItem) { + filteredGroup = this.flattenGroupListItem(connectionGroup); + this.filterGroupListItem(filteredGroup); + } else { + filteredGroup = this.flattenConnectionGroup(connectionGroup); + this.filterConnectionGroup(filteredGroup); + } + + // Store now-filtered root + filteredConnectionGroups[dataSource] = filteredGroup; + + } + } + + return filteredConnectionGroups; + + } + + /** + * Replaces the set of children within the given GroupListItem such + * that only children which match the filter predicate for the + * current search string are present. + * + * @param item + * The GroupListItem whose children should be filtered. + */ + private filterGroupListItem(item: GroupListItem): void { + item.children = item.children.filter(child => { + + // Filter connections and connection groups by + // given pattern + switch (child.type) { + + case GroupListItem.Type.CONNECTION: + return this.connectionFilterPattern?.predicate(child.wrappedItem); + + case GroupListItem.Type.CONNECTION_GROUP: + return this.connectionGroupFilterPattern?.predicate(child.wrappedItem); + + } + + // Include all other children + return true; + + }); + } + + /** + * Replaces the set of child connections and connection groups + * within the given connection group such that only children which + * match the filter predicate for the current search string are + * present. + * + * @param connectionGroup + * The connection group whose children should be filtered. + */ + private filterConnectionGroup(connectionGroup: ConnectionGroup): void { + connectionGroup.childConnections = this.filterService + .filterByPredicate(connectionGroup.childConnections, this.connectionFilterPattern.predicate as any); + + connectionGroup.childConnectionGroups = this.filterService + .filterByPredicate(connectionGroup.childConnectionGroups, this.connectionGroupFilterPattern.predicate as any); + } + + /** + * Flattens the connection group hierarchy of the given connection + * group such that all descendants are copied as immediate + * children. The hierarchy of nested connection groups is otherwise + * completely preserved. A connection or connection group nested + * two or more levels deep within the hierarchy will thus appear + * within the returned connection group in two places: in its + * original location AND as an immediate child. + * + * @param connectionGroup + * The connection group whose descendents should be copied as + * first-level children. + * + * @returns + * A new connection group completely identical to the provided + * connection group, except that absolutely all descendents + * have been copied into the first level of children. + */ + private flattenConnectionGroup(connectionGroup: ConnectionGroup): ConnectionGroup { + + // Replace connection group with shallow copy + connectionGroup = new ConnectionGroup(connectionGroup); + + // Ensure child arrays are defined and independent copies + connectionGroup.childConnections = _.cloneDeep(connectionGroup.childConnections) || []; + connectionGroup.childConnectionGroups = _.cloneDeep(connectionGroup.childConnectionGroups) || []; + + // Flatten all children to the top-level group + connectionGroup.childConnectionGroups?.forEach(child => { + + const flattenedChild = this.flattenConnectionGroup(child); + + // Merge all child connections + Array.prototype.push.apply( + connectionGroup.childConnections, + flattenedChild.childConnections! + ); + + // Merge all child connection groups + Array.prototype.push.apply( + connectionGroup.childConnectionGroups, + flattenedChild.childConnectionGroups! + ); + + }); + + return connectionGroup; + + } + + /** + * Flattens the connection group hierarchy of the given + * GroupListItem such that all descendants are copied as immediate + * children. The hierarchy of nested items is otherwise completely + * preserved. A connection or connection group nested two or more + * levels deep within the hierarchy will thus appear within the + * returned item in two places: in its original location AND as an + * immediate child. + * + * @param item + * The GroupListItem whose descendents should be copied as + * first-level children. + * + * @returns + * A new GroupListItem completely identical to the provided + * item, except that absolutely all descendents have been + * copied into the first level of children. + */ + private flattenGroupListItem(item: GroupListItem): GroupListItem { + + // Replace item with shallow copy + item = new GroupListItem(item); + + // Ensure children are defined and independent copies + item.children = _.cloneDeep(item.children) || []; + + // Flatten all children to the top-level group + item.children.forEach(child => { + if (child.type === GroupListItem.Type.CONNECTION_GROUP) { + + const flattenedChild = this.flattenGroupListItem(child); + + // Merge all children + Array.prototype.push.apply( + item.children, + flattenedChild.children + ); + + } + }); + + return item; + + } + +} diff --git a/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/GroupListItem.ts similarity index 50% rename from guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/GroupListItem.ts index 2255cb4c9f..89b249f666 100644 --- a/guacamole/src/main/frontend/src/app/groupList/types/GroupListItem.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/group-list/types/GroupListItem.ts @@ -17,224 +17,222 @@ * under the License. */ +import { ClientIdentifierService } from '../../navigation/service/client-identifier.service'; +import { ClientIdentifier } from '../../navigation/types/ClientIdentifier'; +import { Connection } from '../../rest/types/Connection'; +import { ConnectionGroup } from '../../rest/types/ConnectionGroup'; +import { SharingProfile } from '../../rest/types/SharingProfile'; +import { Optional } from '../../util/utility-types'; + /** * Provides the GroupListItem class definition. */ -angular.module('groupList').factory('GroupListItem', ['$injector', function defineGroupListItem($injector) { +export class GroupListItem { - // Required types - var ClientIdentifier = $injector.get('ClientIdentifier'); - var ConnectionGroup = $injector.get('ConnectionGroup'); + /** + * The identifier of the data source associated with the connection, + * connection group, or sharing profile this item represents. + */ + dataSource: string; + + /** + * The unique identifier associated with the connection, connection + * group, or sharing profile this item represents. + */ + identifier?: string; + + /** + * The human-readable display name of this item. + */ + name?: string; + + /** + * The unique identifier of the protocol, if this item represents a + * connection. If this item does not represent a connection, this + * property is not applicable. + */ + protocol?: string; + + /** + * All children items of this item. If this item contains no children, + * this will be an empty array. + */ + children: GroupListItem[]; + + /** + * The type of object represented by this GroupListItem. Standard types + * are defined by GroupListItem.Type, but custom types are also legal. + */ + type: string; + + /** + * Whether this item, or items of the same type, can contain children. + * This may be true even if this particular item does not presently + * contain children. + */ + expandable: boolean; + + /** + * Whether this item represents a balancing connection group. + */ + balancing: boolean; + + /** + * Whether the children items should be displayed. + */ + expanded: boolean; + + /** + * Returns the number of currently active users for this connection, + * connection group, or sharing profile, if known. If unknown, null may + * be returned. + * + * @returns + * The number of currently active users for this connection, + * connection group, or sharing profile. + */ + getActiveConnections: () => number | null; + + /** + * Returns the unique string identifier that must be used when + * connecting to a connection or connection group represented by this + * GroupListItem. + * + * @returns + * The client identifier associated with the connection or + * connection group represented by this GroupListItem, or null if + * this GroupListItem cannot have an associated client identifier. + */ + getClientIdentifier: () => string | null; + + + /** + * Returns the relative URL of the client page that connects to the + * connection or connection group represented by this GroupListItem. + * + * @returns + * The relative URL of the client page that connects to the + * connection or connection group represented by this GroupListItem, + * or null if this GroupListItem cannot be connected to. + */ + getClientURL: () => string | null; + + /** + * The connection, connection group, or sharing profile whose data is + * exposed within this GroupListItem. If the type of this GroupListItem + * is not one of the types defined by GroupListItem.Type, then this + * value may be anything. + */ + wrappedItem: Connection | ConnectionGroup | SharingProfile | any; + + /** + * The sorting weight to apply when displaying this GroupListItem. This + * weight is relative only to other sorting weights. If two items have + * the same weight, they will be sorted based on their names. + * + * @default 0 + */ + weight: number; /** * Creates a new GroupListItem, initializing the properties of that * GroupListItem with the corresponding properties of the given template. * - * @constructor - * @param {GroupListItem|Object} [template={}] + * @param template * The object whose properties should be copied within the new * GroupListItem. */ - var GroupListItem = function GroupListItem(template) { - - // Use empty object by default - template = template || {}; + constructor(template: Optional) { - /** - * The identifier of the data source associated with the connection, - * connection group, or sharing profile this item represents. - * - * @type String - */ this.dataSource = template.dataSource; - - /** - * The unique identifier associated with the connection, connection - * group, or sharing profile this item represents. - * - * @type String - */ this.identifier = template.identifier; - - /** - * The human-readable display name of this item. - * - * @type String - */ this.name = template.name; - - /** - * The unique identifier of the protocol, if this item represents a - * connection. If this item does not represent a connection, this - * property is not applicable. - * - * @type String - */ this.protocol = template.protocol; - - /** - * All children items of this item. If this item contains no children, - * this will be an empty array. - * - * @type GroupListItem[] - */ this.children = template.children || []; - - /** - * The type of object represented by this GroupListItem. Standard types - * are defined by GroupListItem.Type, but custom types are also legal. - * - * @type String - */ this.type = template.type; + this.expandable = template.expandable || false; + this.balancing = template.balancing || false; + this.expanded = template.expanded || false; - /** - * Whether this item, or items of the same type, can contain children. - * This may be true even if this particular item does not presently - * contain children. - * - * @type Boolean - */ - this.expandable = template.expandable; - - /** - * Whether this item represents a balancing connection group. - * - * @type Boolean - */ - this.balancing = template.balancing; + this.getActiveConnections = template.getActiveConnections || (() => null); - /** - * Whether the children items should be displayed. - * - * @type Boolean - */ - this.expanded = template.expanded; - - /** - * Returns the number of currently active users for this connection, - * connection group, or sharing profile, if known. If unknown, null may - * be returned. - * - * @returns {Number} - * The number of currently active users for this connection, - * connection group, or sharing profile. - */ - this.getActiveConnections = template.getActiveConnections || (function getActiveConnections() { - return null; - }); - - /** - * Returns the unique string identifier that must be used when - * connecting to a connection or connection group represented by this - * GroupListItem. - * - * @returns {String} - * The client identifier associated with the connection or - * connection group represented by this GroupListItem, or null if - * this GroupListItem cannot have an associated client identifier. - */ - this.getClientIdentifier = template.getClientIdentifier || function getClientIdentifier() { + this.getClientIdentifier = template.getClientIdentifier || (() => { // If the item is a connection, generate a connection identifier if (this.type === GroupListItem.Type.CONNECTION) - return ClientIdentifier.toString({ - dataSource : this.dataSource, - type : ClientIdentifier.Types.CONNECTION, - id : this.identifier + return ClientIdentifierService.getString({ + dataSource: this.dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : this.identifier }); // If the item is a connection group, generate a connection group identifier if (this.type === GroupListItem.Type.CONNECTION_GROUP && this.balancing) - return ClientIdentifier.toString({ - dataSource : this.dataSource, - type : ClientIdentifier.Types.CONNECTION_GROUP, - id : this.identifier + return ClientIdentifierService.getString({ + dataSource: this.dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : this.identifier }); // Otherwise, no such identifier can exist return null; - }; + }); - /** - * Returns the relative URL of the client page that connects to the - * connection or connection group represented by this GroupListItem. - * - * @returns {String} - * The relative URL of the client page that connects to the - * connection or connection group represented by this GroupListItem, - * or null if this GroupListItem cannot be connected to. - */ - this.getClientURL = template.getClientURL || function getClientURL() { + this.getClientURL = template.getClientURL || (() => { // There is a client page for this item only if it has an // associated client identifier - var identifier = this.getClientIdentifier(); + const identifier = this.getClientIdentifier(); if (identifier) - return '#/client/' + encodeURIComponent(identifier); + return '/client/' + encodeURIComponent(identifier); return null; - }; + }); - /** - * The connection, connection group, or sharing profile whose data is - * exposed within this GroupListItem. If the type of this GroupListItem - * is not one of the types defined by GroupListItem.Type, then this - * value may be anything. - * - * @type Connection|ConnectionGroup|SharingProfile|* - */ this.wrappedItem = template.wrappedItem; - - /** - * The sorting weight to apply when displaying this GroupListItem. This - * weight is relative only to other sorting weights. If two items have - * the same weight, they will be sorted based on their names. - * - * @type Number - * @default 0 - */ this.weight = template.weight || 0; - - }; + } /** * Creates a new GroupListItem using the contents of the given connection. * - * @param {String} dataSource + * @param dataSource * The identifier of the data source containing the given connection * group. * - * @param {ConnectionGroup} connection + * @param connection * The connection whose contents should be represented by the new * GroupListItem. * - * @param {Boolean} [includeSharingProfiles=true] + * @param includeSharingProfiles * Whether sharing profiles should be included in the contents of the * resulting GroupListItem. By default, sharing profiles are included. * - * @param {Function} [countActiveConnections] + * @param countActiveConnections * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections * known at the time this function was called is used instead. This * function will be passed, in order, the data source identifier and * the connection in question. * - * @returns {GroupListItem} + * @returns * A new GroupListItem which represents the given connection. */ - GroupListItem.fromConnection = function fromConnection(dataSource, - connection, includeSharingProfiles, countActiveConnections) { + static fromConnection(dataSource: string, + connection: Connection, + includeSharingProfiles = true, + countActiveConnections?: (dataSource: string, connection: Connection) => number): GroupListItem { - var children = []; + const children: GroupListItem[] = []; // Add any sharing profiles if (connection.sharingProfiles && includeSharingProfiles !== false) { connection.sharingProfiles.forEach(function addSharingProfile(child) { children.push(GroupListItem.fromSharingProfile(dataSource, - child, countActiveConnections)); + child)); // TODO additional third parameter 'countActiveConnections'? }); } @@ -242,91 +240,94 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi return new GroupListItem({ // Identifying information - name : connection.name, - identifier : connection.identifier, - protocol : connection.protocol, - dataSource : dataSource, + name : connection.name, + identifier: connection.identifier, + protocol : connection.protocol, + dataSource: dataSource, // Type information - expandable : includeSharingProfiles !== false, - type : GroupListItem.Type.CONNECTION, + expandable: includeSharingProfiles !== false, + type : GroupListItem.Type.CONNECTION, // Already-converted children - children : children, + children: children, // Count of currently active connections using this connection - getActiveConnections : function getActiveConnections() { + getActiveConnections: function getActiveConnections() { // Use getter, if provided if (countActiveConnections) return countActiveConnections(dataSource, connection); - return connection.activeConnections; + return connection.activeConnections === undefined ? null : connection.activeConnections; }, // Wrapped item - wrappedItem : connection + wrappedItem: connection }); - }; + } /** * Creates a new GroupListItem using the contents and descendants of the * given connection group. * - * @param {String} dataSource + * @param dataSource * The identifier of the data source containing the given connection * group. * - * @param {ConnectionGroup} connectionGroup + * @param connectionGroup * The connection group whose contents and descendants should be * represented by the new GroupListItem and its descendants. - * - * @param {Boolean} [includeConnections=true] + * + * @param includeConnections * Whether connections should be included in the contents of the * resulting GroupListItem. By default, connections are included. * - * @param {Boolean} [includeSharingProfiles=true] + * @param includeSharingProfiles * Whether sharing profiles should be included in the contents of the * resulting GroupListItem. By default, sharing profiles are included. * - * @param {Function} [countActiveConnections] + * @param countActiveConnections * A getter which returns the current number of active connections for * the given connection. If omitted, the number of active connections * known at the time this function was called is used instead. This * function will be passed, in order, the data source identifier and * the connection group in question. * - * @param {Function} [countActiveConnectionGroups] + * @param countActiveConnectionGroups * A getter which returns the current number of active connections for * the given connection group. If omitted, the number of active * connections known at the time this function was called is used * instead. This function will be passed, in order, the data source * identifier and the connection group in question. * - * @returns {GroupListItem} + * @returns * A new GroupListItem which represents the given connection group, * including all descendants. */ - GroupListItem.fromConnectionGroup = function fromConnectionGroup(dataSource, - connectionGroup, includeConnections, includeSharingProfiles, - countActiveConnections, countActiveConnectionGroups) { + static fromConnectionGroup(dataSource: string, + connectionGroup: ConnectionGroup, + includeConnections = true, + includeSharingProfiles = true, + countActiveConnections?: (dataSource: string, connection: Connection) => number, + countActiveConnectionGroups?: (dataSource: string, connectionGroup: ConnectionGroup) => number): GroupListItem { - var children = []; + const children: GroupListItem[] = []; // Add any child connections if (connectionGroup.childConnections && includeConnections !== false) { - connectionGroup.childConnections.forEach(function addChildConnection(child) { + connectionGroup.childConnections.forEach(child => { children.push(GroupListItem.fromConnection(dataSource, child, includeSharingProfiles, countActiveConnections)); }); } - // Add any child groups + // Add any child groups if (connectionGroup.childConnectionGroups) { - connectionGroup.childConnectionGroups.forEach(function addChildGroup(child) { + connectionGroup.childConnectionGroups.forEach(child => { children.push(GroupListItem.fromConnectionGroup(dataSource, child, includeConnections, includeSharingProfiles, countActiveConnections, countActiveConnectionGroups)); @@ -337,20 +338,20 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi return new GroupListItem({ // Identifying information - name : connectionGroup.name, - identifier : connectionGroup.identifier, - dataSource : dataSource, + name : connectionGroup.name, + identifier: connectionGroup.identifier, + dataSource: dataSource, // Type information - type : GroupListItem.Type.CONNECTION_GROUP, - balancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, - expandable : true, + type : GroupListItem.Type.CONNECTION_GROUP, + balancing : connectionGroup.type === ConnectionGroup.Type.BALANCING, + expandable: true, // Already-converted children - children : children, + children: children, // Count of currently active connection groups using this connection - getActiveConnections : function getActiveConnections() { + getActiveConnections: function getActiveConnections() { // Use getter, if provided if (countActiveConnectionGroups) @@ -362,84 +363,74 @@ angular.module('groupList').factory('GroupListItem', ['$injector', function defi // Wrapped item - wrappedItem : connectionGroup + wrappedItem: connectionGroup }); - }; + } /** * Creates a new GroupListItem using the contents of the given sharing * profile. * - * @param {String} dataSource + * @param dataSource * The identifier of the data source containing the given sharing * profile. * - * @param {SharingProfile} sharingProfile + * @param sharingProfile * The sharing profile whose contents should be represented by the new * GroupListItem. * - * @returns {GroupListItem} + * @returns * A new GroupListItem which represents the given sharing profile. */ - GroupListItem.fromSharingProfile = function fromSharingProfile(dataSource, - sharingProfile) { + static fromSharingProfile(dataSource: string, sharingProfile: SharingProfile): GroupListItem { // Return item representing the given sharing profile return new GroupListItem({ // Identifying information - name : sharingProfile.name, - identifier : sharingProfile.identifier, - dataSource : dataSource, + name : sharingProfile.name, + identifier: sharingProfile.identifier, + dataSource: dataSource, // Type information - type : GroupListItem.Type.SHARING_PROFILE, + type: GroupListItem.Type.SHARING_PROFILE, // Wrapped item - wrappedItem : sharingProfile + wrappedItem: sharingProfile }); - }; + } + +} + +export namespace GroupListItem { /** * All pre-defined types of GroupListItems. Note that, while these are the * standard types supported by GroupListItem and the related guacGroupList * directive, the type string is otherwise arbitrary and custom types are * legal. - * - * @type Object. */ - GroupListItem.Type = { - + export enum Type { /** * The standard type string of a GroupListItem which represents a * connection. - * - * @type String */ - CONNECTION : 'connection', + CONNECTION = 'connection', /** * The standard type string of a GroupListItem which represents a * connection group. - * - * @type String */ - CONNECTION_GROUP : 'connection-group', + CONNECTION_GROUP = 'connection-group', /** * The standard type string of a GroupListItem which represents a * sharing profile. - * - * @type String */ - SHARING_PROFILE : 'sharing-profile' - - }; - - return GroupListItem; - -}]); + SHARING_PROFILE = 'sharing-profile' + } +} diff --git a/guacamole/src/main/frontend/src/app/history/types/HistoryEntry.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/HistoryEntry.ts similarity index 62% rename from guacamole/src/main/frontend/src/app/history/types/HistoryEntry.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/HistoryEntry.ts index 5942086674..a9a0b044df 100644 --- a/guacamole/src/main/frontend/src/app/history/types/HistoryEntry.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/HistoryEntry.ts @@ -18,35 +18,33 @@ */ /** - * Provides the HistoryEntry class used by the guacHistory service. + * Provides the HistoryEntry class used by the GuacHistoryService. */ -angular.module('history').factory('HistoryEntry', [function defineHistoryEntry() { +export class HistoryEntry { + + /** + * The ID of the connection associated with this history entry, + * including type prefix. + */ + id: string; + + /** + * The thumbnail associated with the connection associated with this + * history entry. + */ + thumbnail: string; /** * A single entry in the connection history. - * + * * @constructor - * @param {String} id The ID of the connection. - * - * @param {String} thumbnail + * @param id The ID of the connection. + * + * @param thumbnail * The URL of the thumbnail to use to represent the connection. */ - var HistoryEntry = function HistoryEntry(id, thumbnail) { - - /** - * The ID of the connection associated with this history entry, - * including type prefix. - */ + constructor(id: string, thumbnail: string) { this.id = id; - - /** - * The thumbnail associated with the connection associated with this - * history entry. - */ this.thumbnail = thumbnail; - - }; - - return HistoryEntry; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.spec.ts new file mode 100644 index 0000000000..5a7cd6f3bf --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.spec.ts @@ -0,0 +1,95 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { LocalStorageService } from '../storage/local-storage.service'; + +import { GuacHistoryService } from './guac-history.service'; + +describe('GuacHistoryService Unit-Test', () => { + let service: GuacHistoryService; + + const fakeLocalStorageService = jasmine.createSpyObj( + 'LocalStorageService', + { + getItem : null, + setItem : undefined, + removeItem: undefined, + } + ); + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [{ provide: LocalStorageService, useValue: fakeLocalStorageService }] + }); + service = TestBed.inject(GuacHistoryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + it('should update thumbnail', () => { + service.updateThumbnail('id', 'thumbnail'); + expect(service.recentConnections.length).toBe(1); + expect(service.recentConnections[0].id).toBe('id'); + expect(service.recentConnections[0].thumbnail).toBe('thumbnail'); + }); + + it('should replace existing HistoryEntry', () => { + service.updateThumbnail('1', 'thumbnail1'); + service.updateThumbnail('2', 'thumbnail2'); + service.updateThumbnail('3', 'thumbnail3'); + + service.updateThumbnail('2', 'thumbnail22'); + + expect(service.recentConnections.length).toBe(3); + expect(service.recentConnections[0].id).toBe('2'); + expect(service.recentConnections[0].thumbnail).toBe('thumbnail22'); + }); + + +}); + +describe('GuacHistoryService Integration-Test', () => { + + let service: GuacHistoryService; + + beforeAll(() => { + localStorage.clear(); + }); + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(GuacHistoryService); + }); + + it('adds history entries', () => { + service.updateThumbnail('1', 'thumbnail1'); + service.updateThumbnail('2', 'thumbnail2'); + service.updateThumbnail('3', 'thumbnail3'); + + expect(service.recentConnections.length).toBe(3); + }); + + it('loads history entries from local storage', () => { + expect(service.recentConnections.length).toBe(3); + }); + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.ts new file mode 100644 index 0000000000..d9f512d45a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/guac-history.service.ts @@ -0,0 +1,106 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import _ from 'lodash'; +import { PreferenceService } from '../settings/services/preference.service'; +import { LocalStorageService } from '../storage/local-storage.service'; +import { HistoryEntry } from './HistoryEntry'; + +// The parameter name for getting the history from local storage +const GUAC_HISTORY_STORAGE_KEY = 'GUAC_HISTORY'; + +/** + * A service for reading and manipulating the Guacamole connection history. + */ +@Injectable({ + providedIn: 'root' +}) +export class GuacHistoryService { + + /** + * The top few recent connections, sorted in order of most recent access. + */ + recentConnections: HistoryEntry[] = []; + + /** + * Inject required services. + */ + constructor(private readonly localStorageService: LocalStorageService, + private readonly preferenceService: PreferenceService) { + // Init stored connection history from localStorage + const storedHistory = localStorageService.getItem(GUAC_HISTORY_STORAGE_KEY) || []; + if (storedHistory instanceof Array) + this.recentConnections = storedHistory; + } + + /** + * Remove from the list of connection history the item having the given + * identfier. + * + * @param id + * The identifier of the item to remove from the history list. + * + * @returns + * True if the removal was successful, otherwise false. + */ + removeEntry(id: string): boolean { + + return _.remove(this.recentConnections, entry => entry.id === id).length > 0; + + } + + /** + * Updates the thumbnail and access time of the history entry for the + * connection with the given ID. + * + * @param id + * The ID of the connection whose history entry should be updated. + * + * @param thumbnail + * The URL of the thumbnail image to associate with the history entry. + */ + updateThumbnail(id: string, thumbnail: string) { + + let i; + + // Remove any existing entry for this connection + for (i = 0; i < this.recentConnections.length; i++) { + if (this.recentConnections[i].id === id) { + this.recentConnections.splice(i, 1); + break; + } + } + + // Store new entry in history + this.recentConnections.unshift(new HistoryEntry( + id, + thumbnail + )); + + // Truncate history to ideal length + if (this.recentConnections.length > this.preferenceService.preferences.numberOfRecentConnections) + this.recentConnections.length = this.preferenceService.preferences.numberOfRecentConnections; + + // Save updated history + this.localStorageService.setItem(GUAC_HISTORY_STORAGE_KEY, this.recentConnections); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/history/historyModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/history.module.ts similarity index 80% rename from guacamole/src/main/frontend/src/app/history/historyModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/history.module.ts index c7ec7e103e..c1a36690db 100644 --- a/guacamole/src/main/frontend/src/app/history/historyModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/history/history.module.ts @@ -17,9 +17,18 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + + /** * The module for code relating to connection history. */ -angular.module('history', [ - 'storage' -]); +@NgModule({ + declarations: [], + imports : [ + CommonModule + ] +}) +export class HistoryModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.html new file mode 100644 index 0000000000..9757365e53 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.html @@ -0,0 +1,29 @@ + + + + + +
      + + + {{ item.name }} + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.ts new file mode 100644 index 0000000000..3bd6c27df4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection-group/connection-group.component.ts @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * TODO + */ +@Component({ + selector: 'guac-connection-group', + templateUrl: './connection-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionGroupComponent { + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.html new file mode 100644 index 0000000000..6662fcdff9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.html @@ -0,0 +1,35 @@ + + + + + +
      + + + {{ item.name }} + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.ts new file mode 100644 index 0000000000..bd0877de85 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/connection/connection.component.ts @@ -0,0 +1,38 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * TODO + */ +@Component({ + selector: 'guac-connection', + templateUrl: './connection.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionComponent { + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.html new file mode 100644 index 0000000000..5fa0a7a23d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.html @@ -0,0 +1,44 @@ + + +
      + + +

      {{ 'HOME.INFO_NO_RECENT_CONNECTIONS' | transloco }}

      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.ts new file mode 100644 index 0000000000..955c4a8ee0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/guac-recent-connections/guac-recent-connections.component.ts @@ -0,0 +1,188 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { GuacHistoryService } from '../../../history/guac-history.service'; +import { ClientIdentifierService } from '../../../navigation/service/client-identifier.service'; +import { ClientIdentifier } from '../../../navigation/types/ClientIdentifier'; +import { Connection } from '../../../rest/types/Connection'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { RecentConnection } from '../../types/RecentConnection'; + +/** + * A component which displays the recently-accessed connections nested beneath + * each of the given connection groups. + */ +@Component({ + selector: 'guac-recent-connections', + templateUrl: './guac-recent-connections.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacRecentConnectionsComponent implements OnChanges { + + /** + * The root connection groups to display, and all visible + * descendants, as a map of data source identifier to the root + * connection group within that data source. Recent connections + * will only be shown if they exist within this hierarchy, + * regardless of their existence within the history. + */ + @Input() rootGroups: Record = {}; + + /** + * Array of all known and visible recently-used connections. + */ + recentConnections: RecentConnection[] = []; + + /** + * Map of all visible objects, connections or connection groups, by + * object identifier. + */ + visibleObjects: Record = {}; + + /** + * Inject required services. + */ + constructor(private guacHistoryService: GuacHistoryService, + private clientIdentifierService: ClientIdentifierService) { + } + + /** + * Remove the connection from the recent connection list having the + * given identifier. + * + * @param recentConnection + * The recent connection to remove from the history list. + * + * @returns + * True if the removal was successful, otherwise false. + */ + removeRecentConnection(recentConnection: RecentConnection): boolean { + return (this.recentConnections.splice(this.recentConnections.indexOf(recentConnection), 1) + && this.guacHistoryService.removeEntry(recentConnection.entry.id)); + } + + /** + * Returns whether recent connections are available for display. + * + * @returns + * true if recent connections are present, false otherwise. + */ + hasRecentConnections(): boolean { + return !!this.recentConnections.length; + } + + /** + * Adds the given connection to the internal set of visible + * objects. + * + * @param dataSource + * The identifier of the data source associated with the + * given connection group. + * + * @param connection + * The connection to add to the internal set of visible objects. + */ + addVisibleConnection(dataSource: string, connection: Connection): void { + + // Add given connection to set of visible objects + this.visibleObjects[this.clientIdentifierService.getString({ + dataSource: dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : connection.identifier + })] = connection; + + } + + /** + * Adds the given connection group to the internal set of visible + * objects, along with any descendants. + * + * @param dataSource + * The identifier of the data source associated with the + * given connection group. + * + * @param connectionGroup + * The connection group to add to the internal set of visible + * objects, along with any descendants. + */ + addVisibleConnectionGroup(dataSource: string, connectionGroup: ConnectionGroup): void { + + // Add given connection group to set of visible objects + (this.visibleObjects)[this.clientIdentifierService.getString({ + dataSource: dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : connectionGroup.identifier + })] = connectionGroup; + + // Add all child connections + if (connectionGroup.childConnections) + connectionGroup.childConnections.forEach(child => { + this.addVisibleConnection(dataSource, child); + }); + + // Add all child connection groups + if (connectionGroup.childConnectionGroups) + connectionGroup.childConnectionGroups.forEach(child => { + this.addVisibleConnectionGroup(dataSource, child); + }); + + } + + /** + * Update visible objects when root groups are set + */ + ngOnChanges(changes: SimpleChanges): void { + + if (changes['rootGroups']) { + const rootGroups = changes['rootGroups'].currentValue as Record; + + // Clear connection arrays + this.recentConnections = []; + + // Produce collection of visible objects + this.visibleObjects = {}; + if (rootGroups) { + + for (const dataSource in rootGroups) { + const rootGroup = rootGroups[dataSource]; + this.addVisibleConnectionGroup(dataSource, rootGroup); + } + + } + + // Add any recent connections that are visible + this.guacHistoryService.recentConnections.forEach(historyEntry => { + + // Add recent connections for history entries with associated visible objects + if (historyEntry.id in this.visibleObjects) { + + const object = this.visibleObjects[historyEntry.id]; + this.recentConnections.push(new RecentConnection(object.name, historyEntry)); + + } + + }); + } + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.html new file mode 100644 index 0000000000..57ad22f1e9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.html @@ -0,0 +1,60 @@ + + +
      + +
      + + +
      +

      {{ 'HOME.SECTION_HEADER_RECENT_CONNECTIONS' | transloco }}

      + +
      + + + +
      +

      {{ 'HOME.SECTION_HEADER_ALL_CONNECTIONS' | transloco }}

      + + + +
      +
      + +
      + +
      + +
      + + + + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.ts new file mode 100644 index 0000000000..45e5f1c752 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/components/home/home.component.ts @@ -0,0 +1,134 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { + GuacGroupListFilterComponent +} from '../../../group-list/components/guac-group-list-filter/guac-group-list-filter.component'; +import { ConnectionGroupDataSource } from '../../../group-list/types/ConnectionGroupDataSource'; +import { FilterService } from '../../../list/services/filter.service'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { PreferenceService } from '../../../settings/services/preference.service'; +import { NonNullableProperties } from '../../../util/utility-types'; + +/** + * The component for the home page. + */ +@Component({ + selector: 'guac-home', + templateUrl: './home.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class HomeComponent implements OnInit { + + /** + * Map of data source identifier to the root connection group of that data + * source, or null if the connection group hierarchy has not yet been + * loaded. + */ + rootConnectionGroups: Record | null = null; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacGroupListFilterComponent, { static: true }) filter!: GuacGroupListFilterComponent; + + /** + * TODO + */ + rootConnectionGroupsDataSource: ConnectionGroupDataSource | null = null; + + /** + * Array of all connection properties that are filterable. + */ + readonly filteredConnectionProperties: string[] = [ + 'name' + ]; + + /** + * Array of all connection group properties that are filterable. + */ + readonly filteredConnectionGroupProperties: string[] = [ + 'name' + ]; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private dataSourceService: DataSourceService, + private requestService: RequestService, + private preferenceService: PreferenceService, + private filterService: FilterService) { + } + + ngOnInit(): void { + + // Create a new data source for the root connection groups + this.rootConnectionGroupsDataSource = new ConnectionGroupDataSource(this.filterService, + {}, + this.filter.searchStringChange, + this.filteredConnectionProperties, + this.filteredConnectionGroupProperties); + + // Retrieve root groups and all descendants + this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID), + this.authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(rootConnectionGroups => { + this.rootConnectionGroups = rootConnectionGroups; + this.rootConnectionGroupsDataSource?.updateSource(this.rootConnectionGroups); + }, this.requestService.PROMISE_DIE); + } + + + /** + * Returns whether the "Recent Connections" section should be displayed on + * the home screen. + * + * @returns + * true if recent connections should be displayed on the home screen, + * false otherwise. + */ + isRecentConnectionsVisible(): boolean { + return this.preferenceService.preferences.showRecentConnections; + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + + return this.rootConnectionGroups !== null + && this.rootConnectionGroupsDataSource !== null; + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/home.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/home.module.ts new file mode 100644 index 0000000000..08aeb075f1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/home.module.ts @@ -0,0 +1,55 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ClientModule } from '../client/client.module'; +import { GroupListModule } from '../group-list/group-list.module'; +import { NavigationModule } from '../navigation/navigation.module'; +import { RestModule } from '../rest/rest.module'; +import { ConnectionGroupComponent } from './components/connection-group/connection-group.component'; +import { ConnectionComponent } from './components/connection/connection.component'; +import { GuacRecentConnectionsComponent } from './components/guac-recent-connections/guac-recent-connections.component'; +import { HomeComponent } from './components/home/home.component'; + + +@NgModule({ + declarations: [ + GuacRecentConnectionsComponent, + ConnectionComponent, + ConnectionGroupComponent, + HomeComponent + ], + imports : [ + CommonModule, + TranslocoModule, + GroupListModule, + NavigationModule, + ClientModule, + RestModule, + RouterModule + ], + exports : [ + HomeComponent, + ] +}) +export class HomeModule { +} diff --git a/guacamole/src/main/frontend/src/app/home/styles/home.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/styles/home.css similarity index 100% rename from guacamole/src/main/frontend/src/app/home/styles/home.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/styles/home.css diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/ActiveConnection.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/ActiveConnection.ts new file mode 100644 index 0000000000..08b5e79987 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/ActiveConnection.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ManagedClient } from '../../client/types/ManagedClient'; + +/** + * A recently-user connection, visible to the current user, with an + * associated history entry. + * + * Used by the guacRecentConnections directive. + */ +export class ActiveConnection { + + /** + * Creates a new ActiveConnection. + * + * @param name + * The human-readable name of this connection. + * + * @param client + * The client associated with this active connection. + */ + constructor(public name: string, public client: ManagedClient) { + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/RecentConnection.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/RecentConnection.ts new file mode 100644 index 0000000000..5b19d5eb1d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/home/types/RecentConnection.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HistoryEntry } from '../../history/HistoryEntry'; + +/** + * A recently-user connection, visible to the current user, with an + * associated history entry. + * + * Used by the guacRecentConnections + * directive. + */ +export class RecentConnection { + + /** + * Creates a new RecentConnection. + * + * @param name + * The human-readable name of this connection. + * + * @param entry + * The history entry associated with this recent connection. + */ + constructor(public name: string | undefined, public entry: HistoryEntry) { + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.html new file mode 100644 index 0000000000..67f2e2e087 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.html @@ -0,0 +1,66 @@ + + +
      + + + + + + + + + + + + + + + + + + + + + + + + +
      + {{ 'IMPORT.TABLE_HEADER_ROW_NUMBER' | transloco }} + + {{ 'IMPORT.TABLE_HEADER_NAME' | transloco }} + + {{ 'IMPORT.TABLE_HEADER_GROUP' | transloco }} + + {{ 'IMPORT.TABLE_HEADER_PROTOCOL' | transloco }} + + {{ 'IMPORT.TABLE_HEADER_ERRORS' | transloco }} +
      {{ error.rowNumber }}{{ error.name }}{{ error.group }}{{ error.protocol }} +
        +
      • + {{ message }} +
      • +
      +
      + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.ts new file mode 100644 index 0000000000..c6780e38c9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-errors/connection-import-errors.component.ts @@ -0,0 +1,362 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import _ from 'lodash'; +import { firstValueFrom, of } from 'rxjs'; +import { GuacFilterComponent } from '../../../list/components/guac-filter/guac-filter.component'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { DirectoryPatch } from '../../../rest/types/DirectoryPatch'; +import { Error } from '../../../rest/types/Error'; +import { TranslatableMessage } from '../../../rest/types/TranslatableMessage'; +import { DisplayErrorList } from '../../types/DisplayErrorList'; +import { ImportConnectionError } from '../../types/ImportConnectionError'; +import { ParseError } from '../../types/ParseError'; +import { ParseResult } from '../../types/ParseResult'; + +/** + * A component that displays errors that occurred during parsing of a connection + * import file, or errors that were returned from the API during the connection + * batch creation attempt. + */ +@Component({ + selector: 'connection-import-errors', + templateUrl: './connection-import-errors.component.html', + standalone: false +}) +export class ConnectionImportErrorsComponent implements OnInit, OnChanges { + + /** + * The result of parsing the import file. Any errors in this file + * will be displayed to the user. + */ + @Input() parseResult: ParseResult | null = null; + + /** + * The error associated with an attempt to batch create the + * connections represented by the ParseResult, if the ParseResult + * had no errors. If the provided ParseResult has errors, no request + * should have been made, and any provided patch error will be + * ignored. + */ + @Input() patchFailure: Error | null = null; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacFilterComponent, { static: true }) filter!: GuacFilterComponent; + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * All connections with their associated errors for display. These may + * be either parsing failures, or errors returned from the API. Both + * error types will be adapted to a common display format, though the + * error types will never be mixed, because no REST request should ever + * be made if there are client-side parse errors. + */ + connectionErrors: ImportConnectionError[] = []; + + /** + * The sorted and paginated data source for the connection errors list. + */ + connectionErrorsDataSource: DataSource | null = null; + + /** + * SortOrder instance which maintains the sort order of the visible + * connection errors. + */ + errorOrder: SortOrder = new SortOrder([ + 'rowNumber', + 'name', + 'group', + 'protocol', + 'errors', + ]); + + /** + * Array of all connection error properties that are filterable. + */ + filteredErrorProperties: string[] = [ + 'rowNumber', + 'name', + 'group', + 'protocol', + 'errors', + ]; + + /** + * TODO + */ + constructor(private dataSourceBuilderService: DataSourceBuilderService, + private translocoService: TranslocoService) { + } + + ngOnInit(): void { + + this.connectionErrorsDataSource = this.dataSourceBuilderService + .getBuilder() + // Start with an empty list + .source(this.connectionErrors) + // Filter based on the search string provided by the guac-filter component + .filter(this.filter.searchStringChange, this.filteredErrorProperties) + // Sort according to the specified sort order + .sort(of(this.errorOrder)) + // Paginate using the GuacPagerComponent + .paginate(this.pager.page) + .build(); + + } + + + /** + * Generate a ImportConnectionError representing any errors associated + * with the row at the given index within the given parse result. + * + * @param parseResult + * The result of parsing the connection import file. + * + * @param index + * The current row within the patches array, 0-indexed. + * + * @param row + * The current row within the original connection, 0-indexed. + * If any REMOVE patches are present, this may be greater than + * the index. + * + * @returns + * The connection error object associated with the given row in the + * given parse result. + */ + private generateConnectionError = (parseResult: ParseResult, index: number, row: number): ImportConnectionError => { + + // Get the patch associated with the current row + const patch = parseResult.patches[index]; + + // The value of a patch is just the Connection object + const connection = patch.value!; + + return new ImportConnectionError({ + + // Add 1 to the provided row to get the position in the file + rowNumber: row + 1, + + // Basic connection information - name, group, and protocol. + name : connection.name!, + group : parseResult.groupPaths[index], + protocol: connection.protocol, + + // The human-readable error messages + // The human-readable error messages + errors: new DisplayErrorList( + [...(parseResult.errors[index] || [])]) + }); + }; + + ngOnChanges(changes: SimpleChanges): void { + + if (changes['parseResult']) { + + const parseResult: ParseResult | null = changes['parseResult'].currentValue; + this.parseResultChanged(parseResult); + + } + + if (changes['patchFailure']) { + const patchFailure: Error | null = changes['patchFailure'].currentValue; + this.patchFailureChanged(patchFailure); + } + } + + /** + * If a new connection patch failure is seen, update the display list + * + * @param patchFailure + * The new patch failure to display. + */ + private patchFailureChanged(patchFailure: Error | null): void { + + // Do not attempt to process anything before the data has loaded + if (!patchFailure || !this.parseResult) + return; + + // All promises from all translation requests. The scope will not be + // updated until all translations are ready. + const translationPromises: Promise[] = []; + + // Any error returned from the API specifically associated with the + // preceding REMOVE patch + let removeError: TranslatableMessage | null = null; + + // Fetch the API error, if any, of the patch at the given index + const getAPIError = (index: number): TranslatableMessage | null => + _.get(patchFailure, ['patches', index, 'error']) ?? null; + + // The row number for display. Unlike the index, this number will + // skip any REMOVE patches. In other words, this is the index of + // connections within the original import file. + let row = 0; + + // Set up the list of connection errors based on the existing parse + // result, with error messages fetched from the patch failure + const connectionErrors = this.parseResult.patches.reduce( + (errors: ImportConnectionError[], patch, index) => { + + // Do not process display REMOVE patches - they are always + // followed by ADD patches containing the actual content + // (and errors, if any) + if (patch.op === DirectoryPatch.Operation.REMOVE) { + + // Save the API error, if any, so it can be displayed + // alongside the connection information associated with the + // following ADD patch + removeError = getAPIError(index); + + // Do not add an entry for this remove patch - it should + // always be followed by a corresponding CREATE patch + // containing the relevant connection information + return errors; + + } + + // Generate a connection error for display + const connectionError = this.generateConnectionError( + this.parseResult!, index, row++); + + // Add the error associated with the previous REMOVE patch, if + // any, to the error associated with the current patch, if any + const apiErrors = [removeError, getAPIError(index)]; + + // Clear the previous REMOVE patch error after consuming it + removeError = null; + + // Go through each potential API error + apiErrors.forEach(error => + + // If the API error exists, fetch the translation and + // update it when it's ready + error && translationPromises.push(firstValueFrom(this.translocoService.selectTranslate( + error.key!, error.variables)) + .then(translatedError => { + connectionError.errors.getArray().push(translatedError); + } + ))); + + errors.push(connectionError); + return errors; + + }, []); + + // Once all the translations have been completed, update the + // connectionErrors all in one go, to ensure no excessive reloading + Promise.all(translationPromises).then(() => { + this.connectionErrors = connectionErrors; + this.connectionErrorsDataSource?.updateSource(connectionErrors); + }); + + } + + /** + * If a new parse result with errors is seen, update the display list. + * + * @param parseResult + * The new parse result to display. + */ + private parseResultChanged(parseResult: ParseResult | null): void { + + // Do not process if there are no errors in the provided result + if (!parseResult || !parseResult.hasErrors) + return; + + // All promises from all translation requests. The scope will not be + // updated until all translations are ready. + const translationPromises: Promise[] = []; + + // The parse result should only be updated on a fresh file import; + // therefore it should be safe to skip checking the patch errors + // entirely - if set, they will be from the previous file and no + // longer relevant. + + // The row number for display. Unlike the index, this number will + // skip any REMOVE patches. In other words, this is the index of + // connections within the original import file. + let row = 0; + + // Set up the list of connection errors based on the updated parse + // result + const connectionErrors = parseResult.patches.reduce( + (errors: ImportConnectionError[], patch, index) => { + + // Do not process display REMOVE patches - they are always + // followed by ADD patches containing the actual content + // (and errors, if any) + if (patch.op === DirectoryPatch.Operation.REMOVE) + return errors; + + // Generate a connection error for display + const connectionError = this.generateConnectionError( + parseResult, index, row++); + + // Go through the errors and check if any are translateable + connectionError.errors.getArray().forEach( + (error: any, errorIndex) => { + + // If this error is a ParseError, it can be translated. + // NOTE: Generally one would translate error messages in the + // template, but in this case, the connection errors need to + // be raw strings in order to enable sorting and filtering. + if (error instanceof ParseError) + + // Fetch the translation and update it when it's ready + translationPromises.push(firstValueFrom(this.translocoService.selectTranslate( + error.key, error.variables)) + .then(translatedError => { + connectionError.errors.getArray()[errorIndex] = translatedError; + })); + + // If the error is not a known translatable type, add the + // message directly to the error array + else + connectionError.errors.getArray()[errorIndex] = ( + error.message ? error.message : error); + + }); + + errors.push(connectionError); + return errors; + + }, []); + + // Once all the translations have been completed, update the + // connectionErrors all in one go, to ensure no excessive reloading + Promise.all(translationPromises).then(() => { + this.connectionErrors = connectionErrors; + this.connectionErrorsDataSource?.updateSource(connectionErrors); + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.html new file mode 100644 index 0000000000..bbf6a1094d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.html @@ -0,0 +1,48 @@ + + +
      + +
      +

      {{ 'IMPORT.SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE' | transloco }}

      + +
      + +

      {{ 'IMPORT.HELP_FILE_TYPE_HEADER' | transloco }}

      +

      {{ 'IMPORT.HELP_FILE_TYPE_DESCRIPTION' | transloco }}

      + +

      {{ 'IMPORT.SECTION_HEADER_CSV' | transloco }}

      +

      {{ 'IMPORT.HELP_CSV_DESCRIPTION' | transloco }}

      +

      {{ 'IMPORT.HELP_CSV_MORE_DETAILS' | transloco }}

      +
      {{ 'IMPORT.HELP_CSV_EXAMPLE' | transloco }}
      + +

      {{ 'IMPORT.SECTION_HEADER_JSON' | transloco }}

      +

      {{ 'IMPORT.HELP_JSON_DESCRIPTION' | transloco }}

      +

      {{ 'IMPORT.HELP_JSON_MORE_DETAILS' | transloco }}

      +
      {{ 'IMPORT.HELP_JSON_EXAMPLE' | transloco }}
      + +

      {{ 'IMPORT.SECTION_HEADER_YAML' | transloco }}

      +

      {{ 'IMPORT.HELP_YAML_DESCRIPTION' | transloco }}

      +
      {{ 'IMPORT.HELP_YAML_EXAMPLE' | transloco }}
      + +
        +
      1. {{ 'IMPORT.HELP_SEMICOLON_FOOTNOTE' | transloco }}
      2. +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.ts new file mode 100644 index 0000000000..46615d0f24 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/connection-import-file-help/connection-import-file-help.component.ts @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component } from '@angular/core'; + +/** + * A component which displays help text for the connection import. + */ +@Component({ + selector: 'guac-connection-import-file-help', + templateUrl: './connection-import-file-help.component.html', + standalone: false +}) +export class ConnectionImportFileHelpComponent { + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.html new file mode 100644 index 0000000000..52d22c4e62 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.html @@ -0,0 +1,104 @@ + + +
      + +
      +

      {{ 'IMPORT.SECTION_HEADER_CONNECTION_IMPORT' | transloco }}

      + +
      + +
      +
      {{ fileName }}
      + +
      + +
      + +
      + {{ 'IMPORT.HELP_UPLOAD_FILE_TYPES' | transloco }} + {{ 'IMPORT.ACTION_VIEW_FORMAT_HELP' | transloco }} + +
      + +
      + +
      {{ 'IMPORT.HELP_UPLOAD_DROP_TITLE' | transloco }}
      + + + + {{ 'IMPORT.ACTION_BROWSE' | transloco }} + + +
      {{ fileName }}
      + +
      + +
        +
      • + + + +
      • +
      • + + + +
      • +
      + +
      + +
      + + +
      + +
      + + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.ts new file mode 100644 index 0000000000..cae17cf10c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/components/import-connections/import-connections.component.ts @@ -0,0 +1,733 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { Router } from '@angular/router'; +import _ from 'lodash'; +import { firstValueFrom } from 'rxjs'; +import { GuacNotificationService } from '../../../notification/services/guac-notification.service'; +import { ConnectionService } from '../../../rest/service/connection.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { UserGroupService } from '../../../rest/service/user-group.service'; +import { UserService } from '../../../rest/service/user.service'; +import { DirectoryPatch } from '../../../rest/types/DirectoryPatch'; +import { DirectoryPatchResponse } from '../../../rest/types/DirectoryPatchResponse'; +import { Error } from '../../../rest/types/Error'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { User } from '../../../rest/types/User'; +import { UserGroup } from '../../../rest/types/UserGroup'; +import { ConnectionCSVService } from '../../services/connection-csv.service'; +import { ConnectionParseService } from '../../services/connection-parse.service'; +import { ConnectionImportConfig } from '../../types/ConnectionImportConfig'; +import { ParseError } from '../../types/ParseError'; +import { ParseResult } from '../../types/ParseResult'; +import Operation = DirectoryPatch.Operation; + +/** + * The allowed MIME type for CSV files. + */ +const CSV_MIME_TYPE: string = 'text/csv'; + +/** + * A fallback regular expression for CSV filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * CSV file. + */ +const CSV_FILENAME_REGEX: RegExp = /\.csv$/i; + +/** + * The allowed MIME type for JSON files. + */ +const JSON_MIME_TYPE: string = 'application/json'; + +/** + * A fallback regular expression for JSON filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * JSON file. + */ +const JSON_FILENAME_REGEX: RegExp = /\.json$/i; + +/** + * The allowed MIME types for YAML files. + * NOTE: There is no registered MIME type for YAML files. This may result in a + * wide variety of possible browser-supplied MIME types. + */ +const YAML_MIME_TYPES: string[] = [ + 'text/x-yaml', + 'text/yaml', + 'text/yml', + 'application/x-yaml', + 'application/x-yml', + 'application/yaml', + 'application/yml' +]; + +/** + * A fallback regular expression for YAML filenames, if no MIME type is provided + * by the browser. Any file that matches this regex will be considered to be a + * YAML file. + */ +const YAML_FILENAME_REGEX: RegExp = /\.ya?ml$/i; + +/** + * Possible signatures for zip files (which include most modern Microsoft office + * documents - most notable excel). If any file, regardless of extension, has + * these starting bytes, it's invalid and must be rejected. + * For more, see https://en.wikipedia.org/wiki/List_of_file_signatures and + * https://en.wikipedia.org/wiki/Magic_number_(programming)#Magic_numbers_in_files. + */ +const ZIP_SIGNATURES: string[] = [ + 'PK\u0003\u0004', + 'PK\u0005\u0006', + 'PK\u0007\u0008' +]; + +/* + * All file types supported for connection import. + */ +const LEGAL_MIME_TYPES: string[] = [CSV_MIME_TYPE, JSON_MIME_TYPE, ...YAML_MIME_TYPES]; + +/** + * The component for the connection import page. + */ +@Component({ + selector: 'guac-import-connections', + templateUrl: './import-connections.component.html', + encapsulation: ViewEncapsulation.None, + providers: [ConnectionParseService, ConnectionCSVService], + standalone: false +}) +export class ImportConnectionsComponent { + + /** + * The unique identifier of the data source to which connections are being + * imported. + */ + @Input('dataSource') dataSource!: string; + + /** + * The result of parsing the current upload, if successful. + */ + parseResult: ParseResult | null = null; + + /** + * The failure associated with the current attempt to create connections + * through the API, if any. + */ + patchFailure: Error | null = null; + + /** + * True if the file is fully uploaded and ready to be processed, or false + * otherwise. + */ + dataReady: boolean = false; + + /** + * True if the file upload has been aborted mid-upload, or false otherwise. + */ + aborted: boolean = false; + + /** + * True if fully-uploaded data is being processed, or false otherwise. + */ + processing: boolean = false; + + /** + * The MIME type of the uploaded file, if any. + */ + mimeType: string | null = null; + + /** + * The name of the file that's currently being uploaded, or has yet to + * be imported, if any. + */ + fileName: string | null = null; + + /** + * The raw string contents of the uploaded file, if any. + */ + fileData: string | null = null; + + /** + * The file reader currently being used to upload the file, if any. If + * null, no file upload is currently in progress. + */ + fileReader: FileReader | null = null; + + /** + * The configuration options for this import, to be chosen by the user. + */ + importConfig: ConnectionImportConfig = new ConnectionImportConfig(); + + /** + * Inject required services. + */ + constructor(private userService: UserService, + private userGroupService: UserGroupService, + private permissionService: PermissionService, + private connectionService: ConnectionService, + private guacNotification: GuacNotificationService, + private connectionParseService: ConnectionParseService, + private router: Router) { + } + + /** + * Clear all file upload state. + */ + private resetUploadState(): void { + + this.aborted = false; + this.dataReady = false; + this.processing = false; + this.fileData = null; + this.mimeType = null; + this.fileReader = null; + this.parseResult = null; + this.patchFailure = null; + this.fileName = null; + + } + + // Indicate that data is currently being loaded / processed if the file + // has been provided but not yet fully uploaded, or if the file is + // fully loaded and is currently being processed. + isLoading = (): boolean => ( + (this.fileName && !this.dataReady && !this.patchFailure) + || this.processing); + + // There are errors to display if the parse result generated errors, or + // if the patch request failed + hasErrors = () => + !!_.get(this, 'parseResult.hasErrors') || !!this.patchFailure; + + /** + * Create all users and user groups mentioned in the import file that don't + * already exist in the current data source. Return an object describing the + * result of the creation requests. + * + * @param parseResult + * The result of parsing the user-supplied import file. + * + * @return + * A promise resolving to an object containing the results of the calls + * to create the users and groups. + */ + private createUsersAndGroups(parseResult: ParseResult): Promise { + + return Promise.all([ + firstValueFrom(this.userService.getUsers(this.dataSource)), + firstValueFrom(this.userGroupService.getUserGroups(this.dataSource)) + ]).then(([existingUsers, existingGroups]) => { + + const userPatches = Object.keys(parseResult.users) + + // Filter out any existing users + .filter(identifier => !existingUsers[identifier]) + + // A patch to create each new user + .map(username => new DirectoryPatch({ + op : Operation.ADD, + path : '/', + value: new User({ username }) + })); + + const groupPatches = Object.keys(parseResult.groups) + + // Filter out any existing groups + .filter(identifier => !existingGroups[identifier]) + + // A patch to create each new user group + .map(identifier => new DirectoryPatch({ + op : Operation.ADD, + path : '/', + value: new UserGroup({ identifier }) + })); + + // Create all the users and groups + return Promise.all([ + firstValueFrom(this.userService.patchUsers(this.dataSource, userPatches)), + firstValueFrom(this.userGroupService.patchUserGroups( + this.dataSource, groupPatches)) + ]); + + }); + + } + + /** + * Grant read permissions for each user and group in the supplied parse + * result to each connection in their connection list. Note that there will + * be a separate request for each user and group. + * + * @param parseResult + * The result of successfully parsing a user-supplied import file. + * + * @param response + * The response from the PATCH API request. + * + * @returns + * A promise that will resolve with the result of every permission + * granting request. + */ + private grantConnectionPermissions(parseResult: ParseResult, response: DirectoryPatchResponse): Promise { + + // All connection grant requests, one per user/group + const userRequests: Promise[] = []; + const groupRequests: Promise[] = []; + + // Create a PermissionSet granting access to all connections at + // the provided indices within the provided parse result + const createPermissionSet = (indices: number[]) => + new PermissionSet({ + connectionPermissions: indices.reduce( + (permissions: Record, index) => { + const connectionId = response!.patches![index].identifier!; + permissions[connectionId] = [ + PermissionSet.ObjectPermissionType.READ]; + return permissions; + }, {}) + }); + + // Now that we've created all the users, grant access to each + _.forEach(parseResult.users, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `false` + userRequests.push(firstValueFrom(this.permissionService.patchPermissions( + this.dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is not for a group + false)))); + + // Now that we've created all the groups, grant access to each + _.forEach(parseResult.groups, (connectionIndices, identifier) => + + // Grant the permissions - note the group flag is `true` + groupRequests.push(firstValueFrom(this.permissionService.patchPermissions( + this.dataSource, identifier, + + // Create the permissions to these connections for this user + createPermissionSet(connectionIndices), + + // Do not remove any permissions + new PermissionSet(), + + // This call is for a group + true)))); + + // Return the result from all the permission granting calls + return Promise.all([...userRequests, ...groupRequests]); + + } + + /** + * Process a successfully parsed import file, creating any specified + * connections, creating and granting permissions to any specified users + * and user groups. If successful, the user will be shown a success message. + * If not, any errors will be displayed and any already-created entities + * will be rolled back. + * + * @param parseResult + * The result of parsing the user-supplied import file. + */ + private handleParseSuccess = (parseResult: ParseResult): void => { + + this.processing = false; + this.parseResult = parseResult; + + // If errors were encountered during file parsing, abort further + // processing - the user will have a chance to fix the errors and try + // again + if (parseResult.hasErrors) + return; + + // First, attempt to create the connections + firstValueFrom(this.connectionService.patchConnections(this.dataSource, parseResult.patches)) + .then(connectionResponse => + + // If connection creation is successful, create users and groups + this.createUsersAndGroups(parseResult).then(() => + + // Grant any new permissions to users and groups. NOTE: Any + // existing permissions for updated connections will NOT be + // removed - only new permissions will be added. + this.grantConnectionPermissions(parseResult, connectionResponse) + .then(() => { + + this.processing = false; + + // Display a success message if everything worked + this.guacNotification.showStatus({ + className: 'success', + title : 'IMPORT.DIALOG_HEADER_SUCCESS', + text : { + key : 'IMPORT.INFO_CONNECTIONS_IMPORTED_SUCCESS', + variables: { NUMBER: parseResult.connectionCount } + }, + + // Add a button to acknowledge and redirect to + // the connection listing page + actions: [{ + name : 'IMPORT.ACTION_ACKNOWLEDGE', + callback: () => { + + // Close the notification + this.guacNotification.showStatus(false); + + // Redirect to connection list page + this.router.navigate(['/settings', this.dataSource, 'connections']); + } + }] + }); + })) + + // If an error occurs while trying to create users or groups, + // display the error to the user. + .catch(this.handleError) + ) + + // If an error occurred when the call to create the connections was made, + // skip any further processing - the user will have a chance to fix the + // problems and try again + .catch(patchFailure => { + this.processing = false; + this.patchFailure = patchFailure; + }); + }; + + /** + * Display the provided error to the user in a dismissible dialog. + * + * @param error + * The error to display. + */ + private handleError = (error: ParseError | Error): void => { + + // Any error indicates that processing of the file has failed, so clear + // all upload state to allow for a fresh retry + this.resetUploadState(); + + let text; + + // If it's an import file parsing error + if (error instanceof ParseError) + text = { + + // Use the translation key if available + key : error.key || error.message, + variables: error.variables + }; + + // If it's a generic REST error + else if (error instanceof Error) + text = error.translatableMessage; + + // If it's an unknown type, just use the message directly + else + text = { key: error }; + + this.guacNotification.showStatus({ + className: 'error', + title : 'IMPORT.DIALOG_HEADER_ERROR', + text, + + // Add a button to hide the error + actions: [{ + name : 'IMPORT.ACTION_ACKNOWLEDGE', + callback: () => this.guacNotification.showStatus(false) + }] + }); + + }; + + /** + * Display the provided error which occurred during the file drop operation + * to the user in a dismissible dialog. + * + * @param errorTextKey + * The translation key of the error message to display. + */ + handleDropError = (errorTextKey: string): void => { + + this.guacNotification.showStatus({ + className: 'error', + title : 'APP.DIALOG_HEADER_ERROR', + text : { key: errorTextKey }, + + // Add a button to hide the error + actions: [{ + name : 'APP.ACTION_ACKNOWLEDGE', + callback: () => this.guacNotification.showStatus(false) + }] + }); + + }; + + /** + * Process the uploaded import file, importing the connections, granting + * connection permissions, or displaying errors to the user if there are + * problems with the provided file. + * + * @param mimeType + * The MIME type of the uploaded data file. + * + * @param data + * The raw string contents of the import file. + */ + private processData(mimeType: string, data: string): void { + + // Data processing has begun + this.processing = true; + + // The function that will process all the raw data and return a list of + // patches to be submitted to the API + let processDataCallback; + + // Choose the appropriate parse function based on the mimetype + if (mimeType === JSON_MIME_TYPE) + processDataCallback = (importConfig: ConnectionImportConfig, jsonData: string) => + this.connectionParseService.parseJSON(importConfig, jsonData); + + else if (mimeType === CSV_MIME_TYPE) + processDataCallback = (importConfig: ConnectionImportConfig, csvData: string) => + this.connectionParseService.parseCSV(importConfig, csvData); + + else if (YAML_MIME_TYPES.indexOf(mimeType) >= 0) + processDataCallback = (importConfig: ConnectionImportConfig, yamlData: string) => + this.connectionParseService.parseYAML(importConfig, yamlData); + + // The file type was validated before being uploaded - this should + // never happen + else + processDataCallback = () => { + throw new ParseError({ + message : 'Unexpected invalid file type: ' + mimeType, + key : '', + variables: undefined + }); + }; + + // Make the call to process the data into a series of patches + processDataCallback(this.importConfig, data) + + // Send the data off to be imported if parsing is successful + .then(this.handleParseSuccess) + + // Display any error found while parsing the file + .catch(this.handleError); + } + + /** + * Process the uploaded import data. Only usable if the upload is fully + * complete. + */ + import = (): void => this.processData(this.mimeType!, this.fileData!); + + /** + * Returns true if import should be disabled, or false if import should be + * allowed. + * + * @return + * True if import should be disabled, otherwise false. + */ + importDisabled = (): boolean => + + // Disable import if no data is ready + !this.dataReady || + + // Disable import if the file is currently being processed + this.processing; + + /** + * Cancel any in-progress upload, or clear any uploaded-but-errored-out + * batch. + */ + cancel(): void { + + // If the upload is in progress, stop it now; the FileReader will + // reset the upload state when it stops + if (this.fileReader) { + this.aborted = true; + this.fileReader.abort(); + } + + // Clear any upload state - there's no FileReader handler to do it + else + this.resetUploadState(); + + } + + /** + * Returns true if cancellation should be disabled, or false if + * cancellation should be allowed. + * + * @return + * True if cancellation should be disabled, or false if cancellation + * should be allowed. + */ + cancelDisabled = (): boolean => + + // Disable cancellation if the import has already been cancelled + this.aborted || + + // Disable cancellation if the file is currently being processed + this.processing || + + // Disable cancellation if no data is ready or being uploaded + !(this.fileReader || this.dataReady); + + /** + * Handle a provided File upload, reading all data onto the scope for + * import processing, should the user request an import. Note that this + * function is used as a callback for directives that invoke it with a file + * list, but directive-level checking should ensure that there is only ever + * one file provided at a time. + * + * @param files + * The files to upload onto the scope for further processing. There + * should only ever be a single file in the array. + */ + handleFiles = (files: FileList): void => { + + // There should only ever be a single file in the array + const file = files[0]; + + // The name and MIME type of the file as provided by the browser + let fileName = file.name; + let mimeType = file.type; + + // If no MIME type was provided by the browser at all, use REGEXes as a + // fallback to try to determine the file type. NOTE: Windows 10/11 are + // known to do this with YAML files. + if (!_.trim(mimeType).length) { + + // If the file name matches what we'd expect for a CSV file, set the + // CSV MIME type and move on + if (CSV_FILENAME_REGEX.test(fileName)) + mimeType = CSV_MIME_TYPE; + + // If the file name matches what we'd expect for a JSON file, set + // the JSON MIME type and move on + else if (JSON_FILENAME_REGEX.test(fileName)) + mimeType = JSON_MIME_TYPE; + + // If the file name matches what we'd expect for a JSON file, set + // one of the allowed YAML MIME types and move on + else if (YAML_FILENAME_REGEX.test(fileName)) + mimeType = YAML_MIME_TYPES[0]; + + else { + + // If none of the REGEXes pass, there's nothing more to be tried + this.handleError(new ParseError({ + message : 'Unknown type for file: ' + fileName, + key : 'IMPORT.ERROR_DETECTED_INVALID_TYPE', + variables: undefined + })); + return; + + } + + } + + // Check if the mimetype is one of the supported types, + // e.g. "application/json" or "text/csv" + else if (LEGAL_MIME_TYPES.indexOf(mimeType) < 0) { + + // If the provided file is not one of the supported types, + // display an error and abort processing + this.handleError(new ParseError({ + message : 'Invalid file type: ' + mimeType, + key : 'IMPORT.ERROR_INVALID_MIME_TYPE', + variables: { TYPE: mimeType } + })); + return; + + } + + // Save the name and type to the scope + this.fileName = fileName; + this.mimeType = mimeType; + + // Initialize upload state + this.aborted = false; + this.dataReady = false; + this.processing = false; + + // Save the file to the scope when ready + this.fileReader = new FileReader(); + this.fileReader.onloadend = (e => { + + // If the upload was explicitly aborted, clear any upload state and + // do not process the data + if (this.aborted) + this.resetUploadState(); + + else { + + const fileData = e.target!.result as string; + + // Check if the file has a header of a known-bad type + if (_.some(ZIP_SIGNATURES, + signature => fileData.startsWith(signature))) { + + // Throw an error and abort processing + this.handleError(new ParseError({ + message : 'Invalid file type detected', + key : 'IMPORT.ERROR_DETECTED_INVALID_TYPE', + variables: undefined + })); + return; + + } + + // Save the uploaded data + this.fileData = fileData; + + // Mark the data as ready + this.dataReady = true; + + // Clear the file reader from the scope now that this file is + // fully uploaded + this.fileReader = null; + + } + }); + + // Read all the data into memory + this.fileReader.readAsText(file); + }; + + /** + * Make the ConnectionImportConfig.ExistingConnectionMode enum available to + * the template. + */ + protected readonly ExistingConnectionMode = ConnectionImportConfig.ExistingConnectionMode; + + /** + * Make the ConnectionImportConfig.ExistingPermissionMode enum available to + * the template. + */ + protected readonly ExistingPermissionMode = ConnectionImportConfig.ExistingPermissionMode; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/import.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/import.module.ts new file mode 100644 index 0000000000..5fc37d02dc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/import.module.ts @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink } from '@angular/router'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ElementModule } from 'guacamole-frontend-lib'; +import { ListModule } from '../list/list.module'; +import { NavigationModule } from '../navigation/navigation.module'; +import { + ConnectionImportErrorsComponent +} from './components/connection-import-errors/connection-import-errors.component'; +import { + ConnectionImportFileHelpComponent +} from './components/connection-import-file-help/connection-import-file-help.component'; +import { ImportConnectionsComponent } from './components/import-connections/import-connections.component'; + +/** + * The module for code supporting importing user-supplied files. Currently, only + * connection import is supported. + */ +@NgModule({ + declarations: [ + ImportConnectionsComponent, + ConnectionImportFileHelpComponent, + ConnectionImportErrorsComponent + ], + imports : [ + CommonModule, + TranslocoModule, + RouterLink, + ElementModule, + NavigationModule, + FormsModule, + ListModule + ], + exports : [ + ImportConnectionsComponent, + ConnectionImportFileHelpComponent + ] +}) +export class ImportModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-csv.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-csv.service.ts new file mode 100644 index 0000000000..89cb3b36a0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-csv.service.ts @@ -0,0 +1,458 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import _ from 'lodash'; +import { forkJoin, map, Observable } from 'rxjs'; +import { SchemaService } from '../../rest/service/schema.service'; +import { ImportConnection } from '../types/ImportConnection'; +import { ParseError } from '../types/ParseError'; +import { TransformConfig } from '../types/TransformConfig'; + +// A suffix that indicates that a particular header refers to a parameter +const PARAMETER_SUFFIX = ' (parameter)'; + +// A suffix that indicates that a particular header refers to an attribute +const ATTRIBUTE_SUFFIX = ' (attribute)'; + +/** + * A service for parsing user-provided CSV connection data for bulk import. + */ +@Injectable() +export class ConnectionCSVService { + + constructor(private schemaService: SchemaService, + private route: ActivatedRoute) { + } + + /** + * Returns an observable that emits a object detailing the connection + * attributes for the current data source, as well as the connection + * parameters for every protocol, for the current data source. + * + * The object is emitted will contain an "attributes" key that maps to + * a set of attribute names, and a "protocolParameters" key that maps to an + * object mapping protocol names to sets of parameter names for that protocol. + * + * The intended use case for this object is to determine if there is a + * connection parameter or attribute with a given name, by e.g. checking the + * path `.protocolParameters[protocolName]` to see if a protocol exists, + * checking the path `.protocolParameters[protocolName][fieldName]` to see + * if a parameter exists for a given protocol, or checking the path + * `.attributes[fieldName]` to check if a connection attribute exists. + * + * @returns + * An observable that emits a object detailing the connection + * attributes and parameters for every protocol, for the current data + * source. + */ + private getFieldLookups(): Observable<{ + attributes: Record; + protocolParameters: Record>; + }> { + + // The current data source - the one that the connections will be + // imported into + const dataSource = this.route.snapshot.paramMap.get('dataSource')!; + + // Fetch connection attributes and protocols for the current data source + return forkJoin([ + this.schemaService.getConnectionAttributes(dataSource), + this.schemaService.getProtocols(dataSource) + ]).pipe( + map(([attributes, protocols]) => ({ + + // Translate the forms and fields into a flat map of attribute + // name to `true` boolean value + attributes: attributes.reduce( + (attributeMap: Record, form) => { + form.fields.forEach( + field => attributeMap[field.name] = true); + return attributeMap; + }, {}), + + // Translate the protocol definitions into a map of protocol + // name to map of field name to `true` boolean value + protocolParameters: _.mapValues( + protocols, protocol => protocol.connectionForms.reduce( + (protocolFieldMap: Record, form) => { + form.fields.forEach( + field => protocolFieldMap[field.name] = true); + return protocolFieldMap; + }, {})) + })) + ); + } + + /** + * Split a raw user-provided, semicolon-seperated list of identifiers into + * an array of identifiers. If identifiers contain semicolons, they can be + * escaped with backslashes, and backslashes can also be escaped using other + * backslashes. + * + * @param rawIdentifiers + * The raw string value as fetched from the CSV. + * + * @returns + * An array of identifier values. + */ + private splitIdentifiers(rawIdentifiers: string): string[] { + + // Keep track of whether a backslash was seen + let escaped = false; + + return _.reduce(rawIdentifiers, (identifiers, ch) => { + + // The current identifier will be the last one in the final list + let identifier = identifiers[identifiers.length - 1]; + + // If a semicolon is seen, set the "escaped" flag and continue + // to the next character + if (!escaped && ch == '\\') { + escaped = true; + return identifiers; + } + + // End the current identifier and start a new one if there's an + // unescaped semicolon + else if (!escaped && ch == ';') { + identifiers.push(''); + return identifiers; + } + + // In all other cases, just append to the identifier + else { + identifier += ch; + escaped = false; + } + + // Save the updated identifier to the list + identifiers[identifiers.length - 1] = identifier; + + return identifiers; + + }, ['']) + + // Filter out any 0-length (empty) identifiers + .filter(identifier => identifier.length); + + } + + /** + * Given a CSV header row, create and return a promise that will resolve to + * a function that can take a CSV data row and return a ImportConnection + * object. If an error occurs while parsing a particular row, the resolved + * function will throw a ParseError describing the failure. + * + * The provided CSV must contain columns for name and protocol. Optionally, + * the parentIdentifier of the target parent connection group, or a connection + * name path e.g. "ROOT/parent/child" may be included. Additionally, + * connection parameters or attributes can be included. + * + * The names of connection attributes and parameters are not guaranteed to + * be mutually exclusive, so the CSV import format supports a distinguishing + * suffix. A column may be explicitly declared to be a parameter using a + * " (parameter)" suffix, or an attribute using an " (attribute)" suffix. + * No suffix is required if the name is unique across connections and + * attributes. + * + * If a parameter or attribute name conflicts with the standard + * "name", "protocol", "group", or "parentIdentifier" fields, the suffix is + * required. + * + * If a failure occurs while attempting to create the transformer function, + * the promise will be rejected with a ParseError describing the failure. + * + * @returns + * A promise that will resolve to a function that translates a CSV data + * row (array of strings) to a ImportConnection object. + */ + getCSVTransformer(headerRow: string[]): Promise<(rows: string[]) => ImportConnection> { + + // A promise that will be resolved with the transformer or rejected if + // an error occurs + return new Promise<(row: string[]) => ImportConnection>((resolve, reject) => { + + this.getFieldLookups().subscribe(({ attributes, protocolParameters }) => { + + // All configuration required to generate a function that can + // transform a row of CSV into a connection object. + // NOTE: This is a single object instead of a collection of variables + // to ensure that no stale references are used - e.g. when one getter + // invokes another getter + const transformConfig: TransformConfig = { + + // Callbacks for required fields + nameGetter : undefined, + protocolGetter: undefined, + + // Callbacks for a parent group ID or group path + groupGetter : undefined, + parentIdentifierGetter: undefined, + + // Callbacks for user and user group identifiers + usersGetter : () => [], + userGroupsGetter: () => [], + + // Callbacks that will generate either connection attributes or + // parameters. These callbacks will return a {type, name, value} + // object containing the type ("parameter" or "attribute"), + // the name of the attribute or parameter, and the corresponding + // value. + parameterOrAttributeGetters: [] + + }; + + // A set of all headers that have been seen so far. If any of these + // are duplicated, the CSV is invalid. + const headerSet: Record = {}; + + // Iterate through the headers one by one + headerRow.forEach((rawHeader, index) => { + + // Trim to normalize all headers + const header = rawHeader.trim(); + + // Check if the header is duplicated + if (headerSet[header]) { + reject(new ParseError({ + message : 'Duplicate CSV Header: ' + header, + key : 'IMPORT.ERROR_DUPLICATE_CSV_HEADER', + variables: { HEADER: header } + })); + return; + } + + // Mark that this particular header has already been seen + headerSet[header] = true; + + // A callback that returns the field at the current index + const fetchFieldAtIndex = (row: string[]) => row[index]; + + // A callback that splits raw string identifier lists by + // semicolon characters into an array of identifiers + const identifierListCallback = (row: string[]) => + this.splitIdentifiers(fetchFieldAtIndex(row)); + + // Set up the name callback + if (header == 'name') + transformConfig.nameGetter = fetchFieldAtIndex; + + // Set up the protocol callback + else if (header == 'protocol') + transformConfig.protocolGetter = fetchFieldAtIndex; + + // Set up the group callback + else if (header == 'group') + transformConfig.groupGetter = fetchFieldAtIndex; + + // Set up the group parent ID callback + else if (header == 'parentIdentifier') + transformConfig.parentIdentifierGetter = fetchFieldAtIndex; + + // Set the user identifiers callback + else if (header == 'users') + transformConfig.usersGetter = ( + identifierListCallback); + + // Set the user group identifiers callback + else if (header == 'groups') + transformConfig.userGroupsGetter = ( + identifierListCallback); + + // At this point, any other header might refer to a connection + // parameter or to an attribute + + // A field may be explicitly specified as a parameter + else if (header.endsWith(PARAMETER_SUFFIX)) { + + // Push as an explicit parameter getter + const parameterName = header.replace(PARAMETER_SUFFIX, ''); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type : 'parameters', + name : parameterName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // A field may be explicitly specified as a parameter + else if (header.endsWith(ATTRIBUTE_SUFFIX)) { + + // Push as an explicit attribute getter + const attributeName = header.replace(ATTRIBUTE_SUFFIX, ''); + transformConfig.parameterOrAttributeGetters.push( + row => ({ + type : 'attributes', + name : attributeName, + value: fetchFieldAtIndex(row) + }) + ); + } + + // The field is ambiguous, either an attribute or parameter, + // so the getter will have to determine this for every row + else + transformConfig.parameterOrAttributeGetters.push(row => { + + // The name is just the value of the current header + const name = header; + + // The value is at the index that matches the position + // of the header + const value = fetchFieldAtIndex(row); + + // If no value is provided, do not check the validity + // of the parameter/attribute. Doing so would prevent + // the import of a list of mixed protocol types, where + // fields are only populated for protocols for which + // they are valid parameters. If a value IS provided, + // it must be a valid parameter or attribute for the + // current protocol, which will be checked below. + if (!value) + return {}; + + // The protocol may determine whether a field is + // a parameter or an attribute (or both) + const protocol = transformConfig.protocolGetter!(row); + + // Any errors encountered while processing this row + const errors = []; + + // Before checking whether it's an attribute or protocol, + // make sure this is a valid protocol to start + if (!protocolParameters[protocol]) + + // If the protocol is invalid, do not throw an error + // here - this will be handled further downstream + // by non-CSV-specific error handling + return {}; + + // Determine if the field refers to an attribute or a + // parameter (or both, which is an error) + const isAttribute = !!attributes[name]; + const isParameter = !!_.get( + protocolParameters, [protocol, name]); + + // If there is both an attribute and a protocol-specific + // parameter with the provided name, it's impossible to + // figure out which this should be + if (isAttribute && isParameter) + errors.push(new ParseError({ + message : 'Ambiguous CSV Header: ' + header, + key : 'IMPORT.ERROR_AMBIGUOUS_CSV_HEADER', + variables: { HEADER: header } + })); + + // It's neither an attribute or a parameter + else if (!isAttribute && !isParameter) + errors.push(new ParseError({ + message : 'Invalid CSV Header: ' + header, + key : 'IMPORT.ERROR_INVALID_CSV_HEADER', + variables: { HEADER: header } + })); + + // Choose the appropriate type + const type = isAttribute ? 'attributes' : 'parameters'; + + return { type, name, value, errors }; + }); + }); + + const { + nameGetter, protocolGetter, + parentIdentifierGetter, groupGetter, + usersGetter, userGroupsGetter, + parameterOrAttributeGetters + } = transformConfig; + + // Fail if the name wasn't provided. Note that this is a file-level + // error, not specific to any connection. + if (!nameGetter) + reject(new ParseError({ + message : 'The connection name must be provided', + key : 'IMPORT.ERROR_REQUIRED_NAME_FILE', + variables: undefined + })); + + // Fail if the protocol wasn't provided + if (!protocolGetter) + reject(new ParseError({ + message : 'The connection protocol must be provided', + key : 'IMPORT.ERROR_REQUIRED_PROTOCOL_FILE', + variables: undefined + })); + + // The function to transform a CSV row into a connection object + resolve((row: string[]) => { + + // Get name and protocol + const name = nameGetter!(row); + const protocol = protocolGetter!(row); + + // Get any users or user groups who should be granted access + const users = usersGetter(row); + const groups = userGroupsGetter(row); + + // Get the parent group ID and/or group path + const group = groupGetter && groupGetter(row); + const parentIdentifier = ( + parentIdentifierGetter && parentIdentifierGetter(row)); + + return new ImportConnection({ + // Fields that are not protocol-specific + name, + protocol, + parentIdentifier, + group, + users, + groups, + + // Fields that might potentially be either attributes or + // parameters, depending on the protocol + ...parameterOrAttributeGetters.reduce((values: Pick, getter) => { + + // Determine the type, name, and value + const { type, name, value, errors } = getter(row); + + // Set the value if available + if (type && name && value) + (values[type as 'parameters' | 'attributes'])[name] = value; + + // If there were errors + if (errors && errors.length) + values.errors = [...values.errors, ...errors]; + + // Continue on to the next attribute or parameter + return values; + + }, { parameters: {}, attributes: {}, errors: [] }) + } as unknown as ImportConnection); + + }); + + }); + + }); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-parse.service.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/import/services/connectionParseService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-parse.service.ts index 59717001f3..da2d178aa7 100644 --- a/guacamole/src/main/frontend/src/app/import/services/connectionParseService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/services/connection-parse.service.ts @@ -4,23 +4,36 @@ * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the - * 'License'); you may not use this file except in compliance + * "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 + * "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. */ -/* global _ */ - -import { parse as parseCSVData } from 'csv-parse/lib/sync' -import { parse as parseYAMLData } from 'yaml' +import { Injectable } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { parse as parseCSVData } from 'csv-parse/browser/esm/sync'; +import _ from 'lodash'; +import { firstValueFrom, map } from 'rxjs'; +import { parse as parseYAMLData } from 'yaml'; +import { ConnectionGroupService } from '../../rest/service/connection-group.service'; +import { SchemaService } from '../../rest/service/schema.service'; +import { Connection } from '../../rest/types/Connection'; +import { ConnectionGroup } from '../../rest/types/ConnectionGroup'; +import { DirectoryPatch } from '../../rest/types/DirectoryPatch'; +import { ConnectionImportConfig } from '../types/ConnectionImportConfig'; +import { ImportConnection } from '../types/ImportConnection'; +import { ParseError } from '../types/ParseError'; +import { ParseResult } from '../types/ParseResult'; +import { TreeLookups } from '../types/TreeLookups'; +import { ConnectionCSVService } from './connection-csv.service'; /** * A particularly unfriendly looking error that the CSV parser throws if a @@ -28,173 +41,120 @@ import { parse as parseYAMLData } from 'yaml' * never be displayed to the user since it makes it look like the application * is broken. As such, the code will attempt to filter out this error and print * something a bit more generic. Lowercased for slightly fuzzier matching. - * - * @type String */ -const BINARY_CSV_ERROR_MESSAGE = "Argument must be a Buffer".toLowerCase(); +const BINARY_CSV_ERROR_MESSAGE = 'Argument must be a Buffer'.toLowerCase(); + +/** + * The identifier of the root connection group, under which all other groups + * and connections exist. + */ +const ROOT_GROUP_IDENTIFIER: string = 'ROOT'; /** * A service for parsing user-provided JSON, YAML, or JSON connection data into * an appropriate format for bulk uploading using the PATCH REST endpoint. */ -angular.module('import').factory('connectionParseService', - ['$injector', function connectionParseService($injector) { - - // Required types - const Connection = $injector.get('Connection'); - const ConnectionImportConfig = $injector.get('ConnectionImportConfig'); - const DirectoryPatch = $injector.get('DirectoryPatch'); - const ImportConnection = $injector.get('ImportConnection'); - const ParseError = $injector.get('ParseError'); - const ParseResult = $injector.get('ParseResult'); - const TranslatableMessage = $injector.get('TranslatableMessage'); - - // Required services - const $q = $injector.get('$q'); - const $routeParams = $injector.get('$routeParams'); - const schemaService = $injector.get('schemaService'); - const connectionCSVService = $injector.get('connectionCSVService'); - const connectionGroupService = $injector.get('connectionGroupService'); - - const service = {}; +@Injectable() +export class ConnectionParseService { - /** - * The identifier of the root connection group, under which all other groups - * and connections exist. - * - * @type String - */ - const ROOT_GROUP_IDENTIFIER = 'ROOT'; + constructor(private route: ActivatedRoute, + private schemaService: SchemaService, + private connectionCSVService: ConnectionCSVService, + private connectionGroupService: ConnectionGroupService) { + } /** * Perform basic checks, common to all file types - namely that the parsed * data is an array, and contains at least one connection entry. Returns an * error if any of these basic checks fails. * - * @returns {ParseError} + * @returns * An error describing the parsing failure, if one of the basic checks * fails. */ - function performBasicChecks(parsedData) { + private performBasicChecks(parsedData: any): ParseError | undefined { // Make sure that the file data parses to an array (connection list) if (!(parsedData instanceof Array)) return new ParseError({ - message: 'Import data must be a list of connections', - key: 'IMPORT.ERROR_ARRAY_REQUIRED' + message : 'Import data must be a list of connections', + key : 'IMPORT.ERROR_ARRAY_REQUIRED', + variables: undefined }); // Make sure that the connection list is not empty - contains at least // one connection if (!parsedData.length) return new ParseError({ - message: 'The provided file is empty', - key: 'IMPORT.ERROR_EMPTY_FILE' + message : 'The provided file is empty', + key : 'IMPORT.ERROR_EMPTY_FILE', + variables: undefined }); - } - /** - * A collection of connection-group-tree-derived maps that are useful for - * processing connections. - * - * @constructor - * @param {TreeLookups|{}} template - * The object whose properties should be copied within the new - * ConnectionImportConfig. - */ - const TreeLookups = template => ({ - - /** - * A map of all known group paths to the corresponding identifier for - * that group. The is that a user-provided import file might directly - * specify a named group path like "ROOT", "ROOT/parent", or - * "ROOT/parent/child". This field field will map all of the above to - * the identifier of the appropriate group, if defined. - * - * @type Object. - */ - groupPathsByIdentifier: template.groupPathsByIdentifier || {}, - - /** - * A map of all known group identifiers to the path of the corresponding - * group. These paths are all of the form "ROOT/parent/child". - * - * @type Object. - */ - groupIdentifiersByPath: template.groupIdentifiersByPath || {}, - - /** - * A map of group identifier, to connection name, to connection - * identifier. These paths are all of the form "ROOT/parent/child". The - * idea is that existing connections can be found by checking if a - * connection already exists with the same parent group, and with the - * same name as an user-supplied import connection. - * - * @type Object. - */ - connectionIdsByGroupAndName : template.connectionIdsByGroupAndName || {} - - }); + return undefined; + } /** * Returns a promise that resolves to a TreeLookups object containing maps * useful for processing user-supplied connections to be imported, derived * from the current connection group tree, starting at the ROOT group. * - * @returns {Promise.} + * @returns * A promise that resolves to a TreeLookups object containing maps * useful for processing connections. */ - function getTreeLookups() { + private getTreeLookups() { // The current data source - defines all the groups that the connections // might be imported into - const dataSource = $routeParams.dataSource; + const dataSource = this.route.snapshot.paramMap.get('dataSource')!; - const deferredTreeLookups = $q.defer(); + return new Promise((resolve) => { - connectionGroupService.getConnectionGroupTree(dataSource).then( + + this.connectionGroupService.getConnectionGroupTree(dataSource).subscribe( rootGroup => { - const lookups = new TreeLookups({}); + const lookups = new TreeLookups({}); - // Add the specified group to the lookup, appending all specified - // prefixes, and then recursively call saveLookups for all children - // of the group, appending to the prefix for each level - const saveLookups = (prefix, group) => { + // Add the specified group to the lookup, appending all specified + // prefixes, and then recursively call saveLookups for all children + // of the group, appending to the prefix for each level + const saveLookups = (prefix: string, group: ConnectionGroup) => { - // To get the path for the current group, add the name - const currentPath = prefix + group.name; + // To get the path for the current group, add the name + const currentPath = prefix + group.name; - // Add the current path to the identifier map - lookups.groupPathsByIdentifier[currentPath] = group.identifier; + // Add the current path to the identifier map + lookups.groupPathsByIdentifier[currentPath] = group.identifier!; - // Add the current identifier to the path map - lookups.groupIdentifiersByPath[group.identifier] = currentPath; + // Add the current identifier to the path map + lookups.groupIdentifiersByPath[group.identifier!] = currentPath; - // Add each connection to the connection map - _.forEach(group.childConnections, - connection => _.setWith( - lookups.connectionIdsByGroupAndName, - [group.identifier, connection.name], - connection.identifier, Object)); + // Add each connection to the connection map + _.forEach(group.childConnections, + connection => _.setWith( + lookups.connectionIdsByGroupAndName, + [group.identifier!, connection.name!], + connection.identifier, Object)); - // Add each child group to the lookup - const nextPrefix = currentPath + "/"; - _.forEach(group.childConnectionGroups, - childGroup => saveLookups(nextPrefix, childGroup)); + // Add each child group to the lookup + const nextPrefix = currentPath + '/'; + _.forEach(group.childConnectionGroups, + childGroup => saveLookups(nextPrefix, childGroup)); - } + }; - // Start at the root group - saveLookups("", rootGroup); + // Start at the root group + saveLookups('', rootGroup); - // Resolve with the now fully-populated lookups - deferredTreeLookups.resolve(lookups); + // Resolve with the now fully-populated lookups + resolve(lookups); + + }); }); - return deferredTreeLookups.promise; } /** @@ -213,51 +173,54 @@ angular.module('import').factory('connectionParseService', * leading slash, or may omit the root identifier entirely. The group may * optionally end with a trailing slash. * - * @param {ConnectionImportConfig} importConfig + * @param importConfig * The configuration options selected by the user prior to import. * - * @returns {Promise.>} + * @returns * A promise that will resolve to a function that will apply various * connection tree based checks and transforms to this connection. */ - function getTreeTransformer(importConfig) { + private getTreeTransformer(importConfig: ConnectionImportConfig): Promise<(connection: ImportConnection) => ImportConnection> { // A map of group path with connection name, to connection object, used // for detecting duplicate connections within the import file itself const connectionsInFile = {}; - return getTreeLookups().then(treeLookups => connection => { + return this.getTreeLookups().then(treeLookups => connection => { - const { groupPathsByIdentifier, groupIdentifiersByPath, - connectionIdsByGroupAndName } = treeLookups; + const { + groupPathsByIdentifier, groupIdentifiersByPath, + connectionIdsByGroupAndName + } = treeLookups; const providedIdentifier = connection.parentIdentifier; // The normalized group path for this connection, of the form // "ROOT/parent/child" let group; - + // The identifier for the parent group of this connection let parentIdentifier; - + // The operator to apply for this connection let op = DirectoryPatch.Operation.ADD; - // If both are specified, the parent group is ambigious + // If both are specified, the parent group is ambiguous if (providedIdentifier && connection.group) { connection.errors.push(new ParseError({ - message: 'Only one of group or parentIdentifier can be set', - key: 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP' + message : 'Only one of group or parentIdentifier can be set', + key : 'IMPORT.ERROR_AMBIGUOUS_PARENT_GROUP', + variables: undefined })); return connection; } // If a parent group identifier is present, but not valid else if (providedIdentifier - && !groupPathsByIdentifier[providedIdentifier]) { + && !groupPathsByIdentifier[providedIdentifier]) { connection.errors.push(new ParseError({ - message: 'No group with identifier: ' + providedIdentifier, - key: 'IMPORT.ERROR_INVALID_GROUP_IDENTIFIER', + message : 'No group with identifier: ' + providedIdentifier, + key : 'IMPORT.ERROR_INVALID_GROUP_IDENTIFIER', variables: { IDENTIFIER: providedIdentifier } })); return connection; @@ -269,7 +232,7 @@ angular.module('import').factory('connectionParseService', group = groupPathsByIdentifier[providedIdentifier]; } - // If a user-supplied group path is provided, attempt to normalize + // If a user-supplied group path is provided, attempt to normalize // and match it to an existing connection group else if (connection.group) { @@ -280,13 +243,14 @@ angular.module('import').factory('connectionParseService', // If the provided group isn't a string, it can never be valid if (typeof group !== 'string') { connection.errors.push(new ParseError({ - message: 'Invalid group type - must be a string', - key: 'IMPORT.ERROR_INVALID_GROUP_TYPE' + message : 'Invalid group type - must be a string', + key : 'IMPORT.ERROR_INVALID_GROUP_TYPE', + variables: undefined })); return connection; } - // Allow the group to start with a leading slash instead instead + // Allow the group to start with a leading slash instead // of explicitly requiring the root connection group if (group.startsWith('/')) group = ROOT_GROUP_IDENTIFIER + group; @@ -305,8 +269,8 @@ angular.module('import').factory('connectionParseService', // If the group doesn't match anything in the tree if (!parentIdentifier) { connection.errors.push(new ParseError({ - message: 'No group found named: ' + connection.group, - key: 'IMPORT.ERROR_INVALID_GROUP', + message : 'No group found named: ' + connection.group, + key : 'IMPORT.ERROR_INVALID_GROUP', variables: { GROUP: connection.group } })); return connection; @@ -325,10 +289,10 @@ angular.module('import').factory('connectionParseService', // Error out if this is a duplicate of a connection already in the // file - if (!!_.get(connectionsInFile, path)) + if (!!_.get(connectionsInFile, path)) connection.errors.push(new ParseError({ - message: 'Duplicate connection in file: ' + path, - key: 'IMPORT.ERROR_DUPLICATE_CONNECTION_IN_FILE', + message : 'Duplicate connection in file: ' + path, + key : 'IMPORT.ERROR_DUPLICATE_CONNECTION_IN_FILE', variables: { NAME: connection.name, PATH: group } })); @@ -337,7 +301,7 @@ angular.module('import').factory('connectionParseService', // Check if this would be an update to an existing connection const existingIdentifier = _.get(connectionIdsByGroupAndName, - [parentIdentifier, connection.name]); + [parentIdentifier, connection.name]); // The default behavior is to create connections if no conflict let importMode = ImportConnection.ImportMode.CREATE; @@ -345,10 +309,10 @@ angular.module('import').factory('connectionParseService', // If updates to existing connections are disallowed if (existingIdentifier && importConfig.existingConnectionMode === - ConnectionImportConfig.ExistingConnectionMode.REJECT) + ConnectionImportConfig.ExistingConnectionMode.REJECT) connection.errors.push(new ParseError({ - message: 'Rejecting update to existing connection: ' + path, - key: 'IMPORT.ERROR_REJECT_UPDATE_CONNECTION', + message : 'Rejecting update to existing connection: ' + path, + key : 'IMPORT.ERROR_REJECT_UPDATE_CONNECTION', variables: { NAME: connection.name, PATH: group } })); @@ -356,14 +320,14 @@ angular.module('import').factory('connectionParseService', else if (existingIdentifier) { identifier = existingIdentifier; importMode = ImportConnection.ImportMode.REPLACE; - } - - else + } else importMode = ImportConnection.ImportMode.CREATE; // Set the import mode, normalized path, and validated identifier - return new ImportConnection({ ...connection, - importMode, group, identifier, parentIdentifier }); + return new ImportConnection({ + ...connection, + importMode, group, identifier, parentIdentifier + }); }); } @@ -380,41 +344,43 @@ angular.module('import').factory('connectionParseService', * If a parameter has no options (i.e. any string value is allowed), the * parameter name will map to a null value. * - * @returns {Promise.>>>} + * @returns * A promise that resolves to a map of all valid protocols to parameter * names to valid values. */ - function getProtocolParameterOptions() { + private getProtocolParameterOptions(): Promise | null>>> { // The current data source - the one that the connections will be // imported into - const dataSource = $routeParams.dataSource; + const dataSource = this.route.snapshot.paramMap.get('dataSource')!; // Fetch the protocols and convert to a set of valid protocol names - return schemaService.getProtocols(dataSource).then( - protocols => _.mapValues(protocols, ({connectionForms}) => { + return firstValueFrom(this.schemaService.getProtocols(dataSource).pipe(map( + protocols => _.mapValues(protocols, ({ connectionForms }) => { - const fieldMap = {}; + const fieldMap: Record | null> = {}; - // Go through all the connection forms and get the fields for each - connectionForms.forEach(({fields}) => fields.forEach(field => { + // Go through all the connection forms and get the fields for each + connectionForms.forEach(({ fields }) => fields.forEach(field => { - const { name, options } = field; + const { name, options } = field; - // Set the value to null to indicate that there are no options - if (!options) - fieldMap[name] = null; + // Set the value to null to indicate that there are no options + if (!options) + fieldMap[name] = null; - // Set the value to a map of lowercased/trimmed option values - // to actual option values - else - fieldMap[name] = _.mapKeys( - options, option => option.trim().toLowerCase()); - - })); + // Set the value to a map of lowercased/trimmed option values + // to actual option values + else + fieldMap[name] = _.mapKeys( + options, option => option.trim().toLowerCase()); + + })); - return fieldMap; - })); + return fieldMap; + })) + ) + ); } /** @@ -422,36 +388,38 @@ angular.module('import').factory('connectionParseService', * hierarchy dependent) checks and transforms to a provided connection, * returning the transformed connection. * - * @returns {Promise.>} + * @returns * A promise resolving to a function that will apply field-level * transforms and checks to a provided connection, returning the * transformed connection. */ - function getFieldTransformer() { + private getFieldTransformer(): Promise<(connection: ImportConnection) => ImportConnection> { - return getProtocolParameterOptions().then(protocols => connection => { + return this.getProtocolParameterOptions().then(protocols => connection => { // Ensure that a protocol was specified for this connection const protocol = connection.protocol; if (!protocol) connection.errors.push(new ParseError({ - message: 'Missing required protocol field', - key: 'IMPORT.ERROR_REQUIRED_PROTOCOL_CONNECTION' + message : 'Missing required protocol field', + key : 'IMPORT.ERROR_REQUIRED_PROTOCOL_CONNECTION', + variables: undefined })); // Ensure that a valid protocol was specified for this connection if (!protocols[protocol]) connection.errors.push(new ParseError({ - message: 'Invalid protocol: ' + protocol, - key: 'IMPORT.ERROR_INVALID_PROTOCOL', + message : 'Invalid protocol: ' + protocol, + key : 'IMPORT.ERROR_INVALID_PROTOCOL', variables: { PROTOCOL: protocol } })); // Ensure that a name was specified for this connection if (!connection.name) connection.errors.push(new ParseError({ - message: 'Missing required name field', - key: 'IMPORT.ERROR_REQUIRED_NAME_CONNECTION' + message : 'Missing required name field', + key : 'IMPORT.ERROR_REQUIRED_NAME_CONNECTION', + variables: undefined })); // Ensure that the specified user list, if any, is an array @@ -464,8 +432,9 @@ angular.module('import').factory('connectionParseService', else connection.errors.push(new ParseError({ - message: 'Invalid users list - must be an array', - key: 'IMPORT.ERROR_INVALID_USERS_TYPE' + message : 'Invalid users list - must be an array', + key : 'IMPORT.ERROR_INVALID_USERS_TYPE', + variables: undefined })); } @@ -480,12 +449,13 @@ angular.module('import').factory('connectionParseService', else connection.errors.push(new ParseError({ - message: 'Invalid groups list - must be an array', - key: 'IMPORT.ERROR_INVALID_USER_GROUPS_TYPE' + message : 'Invalid groups list - must be an array', + key : 'IMPORT.ERROR_INVALID_USER_GROUPS_TYPE', + variables: undefined })); - + } - + // If the protocol is not valid, there's no point in trying to check // parameter case sensitivity if (!protocols[protocol]) @@ -508,7 +478,7 @@ angular.module('import').factory('connectionParseService', // The validated / corrected option value for this connection // parameter, if any const validOptionValue = _.get( - protocols, [protocol, name, comparisonValue]); + protocols, [protocol, name, comparisonValue]); // If the provided value fuzzily matches a valid option value, // use the valid option value instead @@ -542,145 +512,143 @@ angular.module('import').factory('connectionParseService', * transform functions will be run on each entry in `connectionData` before * any other processing is done. * - * @param {ConnectionImportConfig} importConfig + * @param importConfig * The configuration options selected by the user prior to import. * - * @param {*[]} connectionData + * @param connectionData * An arbitrary array of data. This must evaluate to a ImportConnection * object after being run through all functions in `transformFunctions`. * - * @param {Function[]} transformFunctions + * @param transformFunctions * An array of transformation functions to run on each entry in * `connection` data. * - * @return {Promise.} + * @return * A promise resolving to ParseResult object representing the result of * parsing all provided connection data. */ - function parseConnectionData( - importConfig, connectionData, transformFunctions) { + private parseConnectionData( + importConfig: ConnectionImportConfig, connectionData: any[], transformFunctions: Function[]): Promise { // Check that the provided connection data array is not empty - const checkError = performBasicChecks(connectionData); + const checkError = this.performBasicChecks(connectionData); if (checkError) { - const deferred = $q.defer(); - deferred.reject(checkError); - return deferred.promise; + return Promise.reject(checkError); } let index = 0; // Get the tree transformer and relevant protocol information - return $q.all({ - fieldTransformer : getFieldTransformer(), - treeTransformer : getTreeTransformer(importConfig), - }) - .then(({fieldTransformer, treeTransformer}) => + return Promise.all([ + this.getFieldTransformer(), + this.getTreeTransformer(importConfig), + ]) + .then(([fieldTransformer, treeTransformer]) => connectionData.reduce((parseResult, data) => { - const { patches, users, groups, groupPaths } = parseResult; - - // Run the array data through each provided transform - let connectionObject = data; - _.forEach(transformFunctions, transform => { - connectionObject = transform(connectionObject); - }); - - // Apply the field level transforms - connectionObject = fieldTransformer(connectionObject); - - // Apply the connection group hierarchy transforms - connectionObject = treeTransformer(connectionObject); - - // If there are any errors for this connection, fail the whole batch - if (connectionObject.errors.length) - parseResult.hasErrors = true; + const { patches, users, groups, groupPaths } = parseResult; - // The value for the patch is a full-fledged Connection - const value = new Connection(connectionObject); - - // If a new connection is being created - if (connectionObject.importMode - === ImportConnection.ImportMode.CREATE) - - // Add a patch for creating the connection - patches.push(new DirectoryPatch({ - op: DirectoryPatch.Operation.ADD, - path: '/', - value - })); - - // The connection is being replaced, and permissions are only being - // added, not replaced - else if (importConfig.existingPermissionMode === - ConnectionImportConfig.ExistingPermissionMode.PRESERVE) - - // Add a patch for replacing the connection - patches.push(new DirectoryPatch({ - op: DirectoryPatch.Operation.REPLACE, - path: '/' + connectionObject.identifier, - value - })); - - // The connection is being replaced, and permissions are also being - // replaced - else { - - // Add a patch for removing the existing connection - patches.push(new DirectoryPatch({ - op: DirectoryPatch.Operation.REMOVE, - path: '/' + connectionObject.identifier - })); - - // Increment the index for the additional remove patch - index += 1; - - // Add a second patch for creating the replacement connection - patches.push(new DirectoryPatch({ - op: DirectoryPatch.Operation.ADD, - path: '/', - value - })); - - } - - // Save the connection group path into the parse result - groupPaths[index] = connectionObject.group; - - // Save the errors for this connection into the parse result - parseResult.errors[index] = connectionObject.errors; - - // Add this connection index to the list for each user - _.forEach(connectionObject.users, identifier => { - - // If there's an existing list, add the index to that - if (users[identifier]) - users[identifier].push(index); - - // Otherwise, create a new list with just this index - else - users[identifier] = [index]; - }); - - // Add this connection index to the list for each group - _.forEach(connectionObject.groups, identifier => { - - // If there's an existing list, add the index to that - if (groups[identifier]) - groups[identifier].push(index); - - // Otherwise, create a new list with just this index - else - groups[identifier] = [index]; - }); - - // Return the existing parse result state and continue on to the - // next connection in the file - index++; - parseResult.connectionCount++; - return parseResult; - - }, new ParseResult())); + // Run the array data through each provided transform + let connectionObject = data; + _.forEach(transformFunctions, transform => { + connectionObject = transform(connectionObject); + }); + + // Apply the field level transforms + connectionObject = fieldTransformer(connectionObject); + + // Apply the connection group hierarchy transforms + connectionObject = treeTransformer(connectionObject); + + // If there are any errors for this connection, fail the whole batch + if (connectionObject.errors.length) + parseResult.hasErrors = true; + + // The value for the patch is a full-fledged Connection + const value = new Connection(connectionObject); + + // If a new connection is being created + if (connectionObject.importMode + === ImportConnection.ImportMode.CREATE) + + // Add a patch for creating the connection + patches.push(new DirectoryPatch({ + op : DirectoryPatch.Operation.ADD, + path: '/', + value + })); + + // The connection is being replaced, and permissions are only being + // added, not replaced + else if (importConfig.existingPermissionMode === + ConnectionImportConfig.ExistingPermissionMode.PRESERVE) + + // Add a patch for replacing the connection + patches.push(new DirectoryPatch({ + op : DirectoryPatch.Operation.REPLACE, + path: '/' + connectionObject.identifier, + value + })); + + // The connection is being replaced, and permissions are also being + // replaced + else { + + // Add a patch for removing the existing connection + patches.push(new DirectoryPatch({ + op : DirectoryPatch.Operation.REMOVE, + path: '/' + connectionObject.identifier + })); + + // Increment the index for the additional remove patch + index += 1; + + // Add a second patch for creating the replacement connection + patches.push(new DirectoryPatch({ + op : DirectoryPatch.Operation.ADD, + path: '/', + value + })); + + } + + // Save the connection group path into the parse result + groupPaths[index] = connectionObject.group; + + // Save the errors for this connection into the parse result + parseResult.errors[index] = connectionObject.errors; + + // Add this connection index to the list for each user + _.forEach(connectionObject.users, identifier => { + + // If there's an existing list, add the index to that + if (users[identifier]) + users[identifier].push(index); + + // Otherwise, create a new list with just this index + else + users[identifier] = [index]; + }); + + // Add this connection index to the list for each group + _.forEach(connectionObject.groups, identifier => { + + // If there's an existing list, add the index to that + if (groups[identifier]) + groups[identifier].push(index); + + // Otherwise, create a new list with just this index + else + groups[identifier] = [index]; + }); + + // Return the existing parse result state and continue on to the + // next connection in the file + index++; + parseResult.connectionCount++; + return parseResult; + + }, new ParseResult())); } /** @@ -689,51 +657,52 @@ angular.module('import').factory('connectionParseService', * objects containing lists of user and user group identifiers to be granted * to each connection. * - * @param {ConnectionImportConfig} importConfig + * @param importConfig * The configuration options selected by the user prior to import. * - * @param {String} csvData + * @param csvData * The CSV-encoded connection list to process. * - * @return {Promise.} + * @return * A promise resolving to ParseResult object representing the result of * parsing all provided connection data. */ - service.parseCSV = function parseCSV(importConfig, csvData) { + parseCSV(importConfig: ConnectionImportConfig, csvData: string): Promise { // Convert to an array of arrays, one per CSV row (including the header) // NOTE: skip_empty_lines is required, or a trailing newline will error let parsedData; try { - parsedData = parseCSVData(csvData, {skip_empty_lines: true}); + parsedData = parseCSVData(csvData, { skip_empty_lines: true } as any); } - // If the CSV parser throws an error, reject with that error - catch(error) { + // If the CSV parser throws an error, reject with that error + catch (error: any) { const message = error.message; console.error(error); - const deferred = $q.defer(); + let deferred: Promise; // If the error message looks like the expected (and ugly) message // that's thrown when a binary file is provided, throw a more - // friendy error. + // friendly error. if (_.trim(message).toLowerCase() == BINARY_CSV_ERROR_MESSAGE) - deferred.reject(new ParseError({ - message: "CSV binary parse attempt error: " + error.message, - key: "IMPORT.ERROR_DETECTED_INVALID_TYPE" + deferred = Promise.reject(new ParseError({ + message : 'CSV binary parse attempt error: ' + error.message, + key : 'IMPORT.ERROR_DETECTED_INVALID_TYPE', + variables: undefined })); // Otherwise, pass the error from the library through to the user else - deferred.reject(new ParseError({ - message: "CSV Parse Failure: " + error.message, - key: "IMPORT.ERROR_PARSE_FAILURE_CSV", + deferred = Promise.reject(new ParseError({ + message : 'CSV Parse Failure: ' + error.message, + key : 'IMPORT.ERROR_PARSE_FAILURE_CSV', variables: { ERROR: error.message } })); - return deferred.promise; + return deferred; } // The header row - an array of string header values @@ -744,14 +713,14 @@ angular.module('import').factory('connectionParseService', // Generate the CSV transform function, and apply it to every row // before applying all the rest of the standard transforms - return connectionCSVService.getCSVTransformer(header).then( + return this.connectionCSVService.getCSVTransformer(header).then( csvTransformer => // Apply the CSV transform to every row - parseConnectionData( - importConfig, connectionData, [csvTransformer])); + this.parseConnectionData( + importConfig, connectionData, [csvTransformer])); - }; + } /** * Convert a provided YAML representation of a connection list into a JSON @@ -759,17 +728,17 @@ angular.module('import').factory('connectionParseService', * objects containing lists of user and user group identifiers to be granted * to each connection. * - * @param {ConnectionImportConfig} importConfig + * @param importConfig * The configuration options selected by the user prior to import. * - * @param {String} yamlData + * @param yamlData * The YAML-encoded connection list to process. * - * @return {Promise.} + * @return * A promise resolving to ParseResult object representing the result of * parsing all provided connection data. */ - service.parseYAML = function parseYAML(importConfig, yamlData) { + parseYAML(importConfig: ConnectionImportConfig, yamlData: string): Promise { // Parse from YAML into a javascript array let connectionData; @@ -777,23 +746,21 @@ angular.module('import').factory('connectionParseService', connectionData = parseYAMLData(yamlData); } - // If the YAML parser throws an error, reject with that error - catch(error) { + // If the YAML parser throws an error, reject with that error + catch (error: any) { console.error(error); - const deferred = $q.defer(); - deferred.reject(new ParseError({ - message: "YAML Parse Failure: " + error.message, - key: "IMPORT.ERROR_PARSE_FAILURE_YAML", + return Promise.reject(new ParseError({ + message : 'YAML Parse Failure: ' + error.message, + key : 'IMPORT.ERROR_PARSE_FAILURE_YAML', variables: { ERROR: error.message } })); - return deferred.promise; } // Produce a ParseResult, making sure that each record is converted to // the ImportConnection type before further parsing - return parseConnectionData(importConfig, connectionData, - [connection => new ImportConnection(connection)]); - }; + return this.parseConnectionData(importConfig, connectionData, + [(connection: any) => new ImportConnection(connection)]); + } /** * Convert a provided JSON-encoded representation of a connection list into @@ -801,17 +768,17 @@ angular.module('import').factory('connectionParseService', * as a list of objects containing lists of user and user group identifiers * to be granted to each connection. * - * @param {ConnectionImportConfig} importConfig + * @param importConfig * The configuration options selected by the user prior to import. * - * @param {String} jsonData + * @param jsonData * The JSON-encoded connection list to process. * - * @return {Promise.} + * @return * A promise resolving to ParseResult object representing the result of * parsing all provided connection data. */ - service.parseJSON = function parseJSON(importConfig, jsonData) { + parseJSON(importConfig: ConnectionImportConfig, jsonData: string): Promise { // Parse from JSON into a javascript array let connectionData; @@ -819,25 +786,21 @@ angular.module('import').factory('connectionParseService', connectionData = JSON.parse(jsonData); } - // If the JSON parse attempt throws an error, reject with that error - catch(error) { + // If the JSON parse attempt throws an error, reject with that error + catch (error: any) { console.error(error); - const deferred = $q.defer(); - deferred.reject(new ParseError({ - message: "JSON Parse Failure: " + error.message, - key: "IMPORT.ERROR_PARSE_FAILURE_JSON", + return Promise.reject(new ParseError({ + message : 'JSON Parse Failure: ' + error.message, + key : 'IMPORT.ERROR_PARSE_FAILURE_JSON', variables: { ERROR: error.message } })); - return deferred.promise; } // Produce a ParseResult, making sure that each record is converted to // the ImportConnection type before further parsing - return parseConnectionData(importConfig, connectionData, - [connection => new ImportConnection(connection)]); + return this.parseConnectionData(importConfig, connectionData, + [(connection: any) => new ImportConnection(connection)]); - }; - - return service; + } -}]); +} diff --git a/guacamole/src/main/frontend/src/app/import/styles/help.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/help.css similarity index 96% rename from guacamole/src/main/frontend/src/app/import/styles/help.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/help.css index 9cbdb5b182..7cb5ae1d4c 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/help.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/help.css @@ -43,7 +43,7 @@ .import.help pre { - background-color: rgba(0,0,0,0.15); + background-color: rgba(0, 0, 0, 0.15); padding: 10px; width: fit-content; diff --git a/guacamole/src/main/frontend/src/app/import/styles/import.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/import.css similarity index 96% rename from guacamole/src/main/frontend/src/app/import/styles/import.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/import.css index 5e2cd71e1e..d65b4e3dab 100644 --- a/guacamole/src/main/frontend/src/app/import/styles/import.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/styles/import.css @@ -53,7 +53,7 @@ width: fit-content; - border: 1px solid rgba(0,0,0,.25); + border: 1px solid rgba(0, 0, 0, .25); box-shadow: 1px 1px 2px rgb(0 0 0 / 25%); margin-left: auto; @@ -114,14 +114,14 @@ width: 500px; height: 200px; - background: rgba(0,0,0,.04); + background: rgba(0, 0, 0, .04); border: 1px solid black; } .file-upload-container .drop-target.file-present { - background: rgba(0,0,0,.15); + background: rgba(0, 0, 0, .15); } @@ -172,7 +172,7 @@ visibility: hidden; cursor: help; - + } .file-upload-container .import-config .help::after { @@ -187,4 +187,4 @@ position: relative; top: 4px; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/import/types/ConnectionImportConfig.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ConnectionImportConfig.ts similarity index 59% rename from guacamole/src/main/frontend/src/app/import/types/ConnectionImportConfig.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ConnectionImportConfig.ts index fb06730553..e58b2473f6 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ConnectionImportConfig.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ConnectionImportConfig.ts @@ -18,86 +18,84 @@ */ /** - * Service which defines the ConnectionImportConfig class. + * A representation of any user-specified configuration when + * batch-importing connections. */ -angular.module('import').factory('ConnectionImportConfig', [ - function defineConnectionImportConfig() { +export class ConnectionImportConfig { /** - * A representation of any user-specified configuration when - * batch-importing connections. + * The mode for handling connections that match existing connections. + */ + existingConnectionMode: ConnectionImportConfig.ExistingConnectionMode; + + /** + * The mode for handling permissions on existing connections that are + * being updated. Only meaningful if the importer is configured to + * replace existing connections. + */ + existingPermissionMode: ConnectionImportConfig.ExistingPermissionMode; + + /** + * Creates a new ConnectionImportConfig. * - * @constructor - * @param {ConnectionImportConfig|Object} [template={}] + * @param [template={}] * The object whose properties should be copied within the new * ConnectionImportConfig. */ - const ConnectionImportConfig = function ConnectionImportConfig(template) { - - // Use empty object by default - template = template || {}; + constructor(template: Partial = {}) { - /** - * The mode for handling connections that match existing connections. - * - * @type ConnectionImportConfig.ExistingConnectionMode - */ this.existingConnectionMode = template.existingConnectionMode - || ConnectionImportConfig.ExistingConnectionMode.REJECT; + || ConnectionImportConfig.ExistingConnectionMode.REJECT; - /** - * The mode for handling permissions on existing connections that are - * being updated. Only meaningful if the importer is configured to - * replace existing connections. - * - * @type ConnectionImportConfig.ExistingPermissionMode - */ this.existingPermissionMode = template.existingPermissionMode - || ConnectionImportConfig.ExistingPermissionMode.PRESERVE; + || ConnectionImportConfig.ExistingPermissionMode.PRESERVE; + + } + +} - }; +export namespace ConnectionImportConfig { /** * Valid modes for the behavior of the importer when an imported connection * already exists. */ - ConnectionImportConfig.ExistingConnectionMode = { + export enum ExistingConnectionMode { /** * Any Connection that has the same name and parent group as an existing * connection will cause the entire import to be rejected with an error. */ - REJECT : "REJECT", + REJECT = 'REJECT', /** * Replace/update any existing connections. */ - REPLACE : "REPLACE" + REPLACE = 'REPLACE' - }; + } /** * Valid modes for the behavior of the importer with respect to connection * permissions when existing connections are being replaced. */ - ConnectionImportConfig.ExistingPermissionMode = { + export enum ExistingPermissionMode { /** * Any new permissions specified in the imported connection will be * added to the existing connection, without removing any existing * permissions. */ - PRESERVE : "PRESERVE", + PRESERVE = 'PRESERVE', /** * Any existing permissions will be removed, ensuring that only the * users or groups specified in the import file will be granted to the * replaced connection after import. */ - REPLACE : "REPLACE" + REPLACE = 'REPLACE' - }; + } - return ConnectionImportConfig; -}]); \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/DisplayErrorList.ts similarity index 66% rename from guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/DisplayErrorList.ts index 26ee65d82e..5ae1bdfa23 100644 --- a/guacamole/src/main/frontend/src/app/import/types/DisplayErrorList.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/DisplayErrorList.ts @@ -17,39 +17,38 @@ * under the License. */ +import { ParseError } from './ParseError'; + /** - * Service which defines the DisplayErrorList class. + * A list of human-readable error messages, intended to be usable in a + * sortable / filterable table. */ -angular.module('import').factory('DisplayErrorList', [ - function defineDisplayErrorList() { +export class DisplayErrorList { + + /** + * The error messages that should be prepared for display. + */ + messages: (string | ParseError)[]; + + /** + * The single String message composed of all messages concatenated + * together. This will be used for filtering / sorting, and should only + * be calculated once, when toString() is called. + */ + concatenatedMessage: string | null; /** - * A list of human-readable error messages, intended to be usable in a - * sortable / filterable table. + * Creates a new DisplayErrorList. * - * @constructor - * @param {String[]} messages + * @param messages * The error messages that should be prepared for display. */ - const DisplayErrorList = function DisplayErrorList(messages) { + constructor(messages?: (string | ParseError)[]) { - /** - * The error messages that should be prepared for display. - * - * @type {String[]} - */ this.messages = messages || []; - - /** - * The single String message composed of all messages concatenated - * together. This will be used for filtering / sorting, and should only - * be calculated once, when toString() is called. - * - * @type {String} - */ this.concatenatedMessage = null; - }; + } /** * Return a sortable / filterable representation of all the error messages @@ -60,11 +59,11 @@ angular.module('import').factory('DisplayErrorList', [ * by sorting / filtering UI code will not regenerate the concatenated * message every time. * - * @returns {String} + * @returns * A sortable / filterable representation of the error messages wrapped * by this DisplayErrorList */ - DisplayErrorList.prototype.toString = function messageListToString() { + toString(): string { // Generate the concatenated message if not already generated if (!this.concatenatedMessage) @@ -78,14 +77,12 @@ angular.module('import').factory('DisplayErrorList', [ * Return the underlying array containing the raw error messages, wrapped * by this DisplayErrorList. * - * @returns {String[]} + * @returns * The underlying array containing the raw error messages, wrapped by * this DisplayErrorList */ - DisplayErrorList.prototype.getArray = function getUnderlyingArray() { + getArray(): (string | ParseError)[] { return this.messages; } - return DisplayErrorList; - -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnection.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnection.ts new file mode 100644 index 0000000000..37e9dfb110 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnection.ts @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Optional } from '../../util/utility-types'; +import { ParseError } from './ParseError'; + +/** + * A representation of a connection to be imported, as parsed from an + * user-supplied import file. + */ +export class ImportConnection { + + /** + * The unique identifier of the connection group that contains this + * connection. + */ + parentIdentifier: string; + + /** + * The path to the connection group that contains this connection, + * written as e.g. "ROOT/parent/child/group". + */ + group: string; + + /** + * The identifier of the connection being updated. Only meaningful if + * the replace operation is set. + */ + identifier: string; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + */ + name: string; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + */ + protocol: string; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use, arranged as name/value pairs. + */ + parameters: Record; + + /** + * Arbitrary name/value pairs which further describe this connection. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + */ + attributes: Record; + + /** + * The identifiers of all users who should be granted read access to + * this connection. + */ + users: string[]; + + /** + * The identifiers of all user groups who should be granted read access + * to this connection. + */ + groups: string[]; + + /** + * The mode import mode for this connection. If not otherwise specified, + * a brand new connection should be created. + */ + importMode: ImportConnection.ImportMode; + + /** + * Any errors specific to this connection encountered while parsing. + */ + errors: ParseError[]; + + /** + * Create a new ImportConnection. + * + * @param template + * The object whose properties should be copied within the new + * Connection. + */ + constructor(template: Optional) { + + this.parentIdentifier = template.parentIdentifier; + this.group = template.group; + this.identifier = template.identifier; + this.name = template.name; + this.protocol = template.protocol; + this.parameters = template.parameters || {}; + this.attributes = template.attributes || {}; + this.users = template.users || []; + this.groups = template.groups || []; + this.importMode = template.importMode || ImportConnection.ImportMode.CREATE; + this.errors = template.errors || []; + } + +} + +export namespace ImportConnection { + + /** + * The possible import modes for a given connection. + */ + export enum ImportMode { + + /** + * The connection should be created fresh. This mode is valid IFF there + * is no existing connection with the same name and parent group. + */ + CREATE = 'CREATE', + + /** + * This connection will replace the existing connection with the same + * name and parent group. + */ + REPLACE = 'REPLACE' + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnectionError.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnectionError.ts new file mode 100644 index 0000000000..67ad0131ef --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ImportConnectionError.ts @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Optional } from '../../util/utility-types'; +import { DisplayErrorList } from './DisplayErrorList'; + +/** + * A representation of the errors associated with a connection to be + * imported, along with some basic information connection information to + * identify the connection having the error, as returned from a parsed + * user-supplied import file. + */ +export class ImportConnectionError { + + /** + * The row number within the original connection import file for this + * connection. This should be 1-indexed. + */ + rowNumber: number; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + */ + name: String; + + /** + * The human-readable connection group path for this connection, of the + * form "ROOT/Parent/Child". + */ + group: String; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + */ + protocol: String; + + /** + * The error messages associated with this particular connection, if any. + */ + errors: DisplayErrorList; + + /** + * Creates a new ImportConnectionError. + * + * @param template + * The object whose properties should be copied within the new + * ImportConnectionError. + */ + constructor(template: Optional) { + + this.rowNumber = template.rowNumber; + this.name = template.name; + this.group = template.group; + this.protocol = template.protocol; + this.errors = template.errors || new DisplayErrorList(); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/import/types/ParseError.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseError.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/import/types/ParseError.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseError.ts index b4b3dd6470..db7d24496e 100644 --- a/guacamole/src/main/frontend/src/app/import/types/ParseError.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseError.ts @@ -18,47 +18,41 @@ */ /** - * Service which defines the ParseError class. + * An error representing a parsing failure when attempting to convert + * user-provided data into a list of Connection objects. */ -angular.module('import').factory('ParseError', [function defineParseError() { +export class ParseError { /** - * An error representing a parsing failure when attempting to convert - * user-provided data into a list of Connection objects. + * A human-readable message describing the error that occurred. + */ + message: string; + + /** + * The key associated with the translation string that used when + * displaying this message. + */ + key: string; + + /** + * The object which should be passed through to the translation service + * for the sake of variable substitution. Each property of the provided + * object will be substituted for the variable of the same name within + * the translation string. + */ + variables: any; + + /** + * Creates a new ParseError. * - * @constructor - * @param {ParseError|Object} [template={}] + * @param template * The object whose properties should be copied within the new * ParseError. */ - const ParseError = function ParseError(template) { - - // Use empty object by default - template = template || {}; + constructor(template: ParseError) { - /** - * A human-readable message describing the error that occurred. - * - * @type String - */ this.message = template.message; - - /** - * The key associated with the translation string that used when - * displaying this message. - * - * @type String - */ this.key = template.key; - - /** - * The object which should be passed through to the translation service - * for the sake of variable substitution. Each property of the provided - * object will be substituted for the variable of the same name within - * the translation string. - * - * @type Object - */ this.variables = template.variables; // If no translation key is available, fall back to the untranslated @@ -68,8 +62,6 @@ angular.module('import').factory('ParseError', [function defineParseError() { this.variables = { MESSAGE: this.message }; } - }; - - return ParseError; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseResult.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseResult.ts new file mode 100644 index 0000000000..e3294174a3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/ParseResult.ts @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Connection } from '../../rest/types/Connection'; +import { DirectoryPatch } from '../../rest/types/DirectoryPatch'; +import { ParseError } from './ParseError'; + +/** + * The result of parsing a connection import file - containing a list of + * API patches ready to be submitted to the PATCH REST API for batch + * connection creation/replacement, a set of users and user groups to grant + * access to each connection, a group path for every connection, and any + * errors that may have occurred while parsing each connection. + */ +export class ParseResult { + + /** + * An array of patches, ready to be submitted to the PATCH REST API for + * batch connection creation / replacement. Note that this array may + * contain more patches than connections from the original file - in the + * case that connections are being fully replaced, there will be a + * remove and a create patch for each replaced connection. + */ + patches: DirectoryPatch[]; + + /** + * An object whose keys are the user identifiers of users specified + * in the batch import, and whose values are an array of indices of + * connections within the patches array to which those users should be + * granted access. + */ + users: Record; + + /** + * An object whose keys are the user group identifiers of every user + * group specified in the batch import. i.e. a set of all user group + * identifiers. + */ + groups: Record; + + /** + * A map of connection index within the patch array, to connection group + * path for that connection, of the form "ROOT/Parent/Child". + */ + groupPaths: Record; + + /** + * An array of errors encountered while parsing the corresponding + * connection (at the same array index in the patches array). Each + * connection should have an array of errors. If empty, no errors + * occurred for this connection. + */ + errors: ParseError[][]; + + /** + * True if any errors were encountered while parsing the connections + * represented by this ParseResult. This should always be true if there + * are a non-zero number of elements in the errors list for any + * connection, or false otherwise. + */ + hasErrors: boolean; + + /** + * The integer number of unique connections present in the parse result. + * This may be less than the length of the patches array, if any REMOVE + * patches are present. + */ + connectionCount: number; + + /** + * Creates a new ParseResult. + */ + constructor(template: Partial = {}) { + + this.patches = template.patches || []; + this.users = template.users || {}; + this.groups = template.groups || {}; + this.groupPaths = template.groupPaths || {}; + this.errors = template.errors || []; + this.hasErrors = template.hasErrors || false; + this.connectionCount = template.connectionCount || 0; + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TransformConfig.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TransformConfig.ts new file mode 100644 index 0000000000..0ba1da02b2 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TransformConfig.ts @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ParseError } from './ParseError'; + +/** + * All configuration required to generate a function that can + * transform a row of CSV into a connection object. + */ +export interface TransformConfig { + + // Callbacks for required fields + nameGetter?: (row: string[]) => any; + protocolGetter?: (row: string[]) => any; + + // Callbacks for a parent group ID or group path + groupGetter?: (row: string[]) => any; + parentIdentifierGetter?: (row: string[]) => any; + + // Callbacks for user and user group identifiers + usersGetter: (row: string[]) => string[]; + userGroupsGetter: (row: string[]) => string[]; + + // Callbacks that will generate either connection attributes or + // parameters. These callbacks will return a {type, name, value} + // object containing the type ("parameter" or "attribute"), + // the name of the attribute or parameter, and the corresponding + // value. + parameterOrAttributeGetters: ((row: string[]) => { + type?: string; + name?: string; + value?: any; + errors?: ParseError[] + })[]; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TreeLookups.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TreeLookups.ts new file mode 100644 index 0000000000..6d82c3cbc4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/import/types/TreeLookups.ts @@ -0,0 +1,65 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * A collection of connection-group-tree-derived maps that are useful for + * processing connections. + */ +export class TreeLookups { + + /** + * A map of all known group paths to the corresponding identifier for + * that group. The is that a user-provided import file might directly + * specify a named group path like "ROOT", "ROOT/parent", or + * "ROOT/parent/child". This field field will map all of the above to + * the identifier of the appropriate group, if defined. + */ + groupPathsByIdentifier: Record; + + /** + * A map of all known group identifiers to the path of the corresponding + * group. These paths are all of the form "ROOT/parent/child". + */ + groupIdentifiersByPath: Record; + + /** + * A map of group identifier, to connection name, to connection + * identifier. These paths are all of the form "ROOT/parent/child". The + * idea is that existing connections can be found by checking if a + * connection already exists with the same parent group, and with the + * same name as an user-supplied import connection. + */ + connectionIdsByGroupAndName: Record; + + /** + * Creates a new TreeLookups object. + * + * @param template + * The object whose properties should be copied within the new + * ConnectionImportConfig. + */ + constructor(template: Partial) { + + this.groupPathsByIdentifier = template.groupPathsByIdentifier || {}; + this.groupIdentifiersByPath = template.groupIdentifiersByPath || {}; + this.connectionIdsByGroupAndName = template.connectionIdsByGroupAndName || {}; + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/config/default-headers.interceptor.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/config/default-headers.interceptor.ts new file mode 100644 index 0000000000..daf73d0239 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/config/default-headers.interceptor.ts @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + +@Injectable() +export class DefaultHeadersInterceptor implements HttpInterceptor { + + /** + * Set additional headers for GET and PATCH requests. + */ + intercept(request: HttpRequest, next: HttpHandler): Observable> { + + // Do not cache the responses of GET requests + if (request.method === 'GET') { + request = request.clone({ + setHeaders: { + 'Cache-Control': 'no-cache', + 'Pragma' : 'no-cache' + } + }); + } + + // Use "application/json" content type by default for PATCH requests + if (request.method === 'PATCH') { + request = request.clone({ + setHeaders: { + 'Content-Type': 'application/json' + } + }); + } + + return next.handle(request); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/index.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/index.module.ts new file mode 100644 index 0000000000..ed68048d30 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/index.module.ts @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { EscapePipe } from './pipes/escape.pipe'; + +/** + * The module for the root of the application. + */ +@NgModule({ + declarations: [ + EscapePipe + ], + imports : [ + CommonModule + ], + exports : [ + EscapePipe + ] +}) +export class IndexModule { +} diff --git a/guacamole/src/main/frontend/src/app/index/filters/escapeFilter.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/pipes/escape.pipe.ts similarity index 73% rename from guacamole/src/main/frontend/src/app/index/filters/escapeFilter.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/pipes/escape.pipe.ts index c1ec4e0102..f77624cddb 100644 --- a/guacamole/src/main/frontend/src/app/index/filters/escapeFilter.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/pipes/escape.pipe.ts @@ -17,19 +17,24 @@ * under the License. */ +import { Pipe, PipeTransform } from '@angular/core'; + /** - * A filter for making sure that a string is URI-encoded and + * A pipe for making sure that a string is URI-encoded and * that any characters with special meaning for URIs are escaped. */ -angular.module('index').filter('escape', [function escapeFilter() { +@Pipe({ + name: 'escape', + standalone: false +}) +export class EscapePipe implements PipeTransform { - return function escapeFilter(input) { + transform(value: string): string { - if (input) - return window.encodeURIComponent(input); - - return ''; + if (value) + return encodeURIComponent(value); - }; + return ''; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/apply-patches.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/apply-patches.service.ts new file mode 100644 index 0000000000..b62369efa7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/apply-patches.service.ts @@ -0,0 +1,163 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { loadRemoteModule } from '@angular-architects/native-federation'; +import { DOCUMENT } from '@angular/common'; +import { ApplicationRef, EnvironmentInjector, Inject, Injectable, Injector, ViewContainerRef } from '@angular/core'; +import $ from 'jquery'; +import { PatchService } from '../../rest/service/patch.service'; +import { PatchOperation } from '../types/PatchOperation'; + +/** + * Applies HTML patches defined within Guacamole extensions to the DOM. + */ +@Injectable({ + providedIn: 'root' +}) +export class ApplyPatchesService { + + /** + * All available HTML patches. + */ + private patches: { operations: PatchOperation[]; elements: Element[] }[] = []; + + /** + * Inject required services. + */ + constructor(@Inject(DOCUMENT) private document: Document, + private patchService: PatchService, + private injector: Injector, + private _appRef: ApplicationRef) { + + // Retrieve all patches + this.patchService.getPatches().subscribe(async patches => { + const domParser = new DOMParser(); + // Apply all defined patches + for (const patch of patches) { + + const parsedPatch = domParser.parseFromString(patch, 'text/html'); + const metaElements = Array.from(parsedPatch.querySelectorAll('meta')); + const otherElements = Array.from(parsedPatch.body.children); + + // Filter out and parse all applicable meta tags + const operations: PatchOperation[] = []; + let elements = await Promise.all([...metaElements, ...otherElements].filter((element) => { + + // Leave non-meta tags untouched + if (element.tagName !== 'META') + return true; + + // Only meta tags having a valid "name" attribute need + // to be filtered + const name = element.getAttribute('name'); + if (!name || !(name in PatchOperation.Operations)) + return true; + + // The "content" attribute must be present for any + // valid "name" meta tag + const content = element.getAttribute('content'); + if (!content) + return true; + + // Filter out and parse meta tag + operations.push(new PatchOperation(name, content)); + return false; + + }) + .map(async (element) => + await this.getPatchElement(element) + )) + ; + + // Add patch to list of available patches + this.patches.push({ + operations, + elements + }); + + } + }); + } + + /** + * TODO + * @param element + * @private + */ + public async getPatchElement(element: Element): Promise { + + // Check if the element should be replaced by an angular component via nativ federation + const nativeFederationModule = element.getAttribute('nativeFederationModule'); + const nativeFederationElement = element.getAttribute('nativeFederationElement'); + + if (nativeFederationModule && nativeFederationElement) { + return this.createNativeFederationAngularComponent(nativeFederationModule, nativeFederationElement) + } + + // Just return the element itself if native federation is not required + return element; + + } + + /** + * FIXME + * @param nativeFederationModule + * @param nativeFederationElement + * @private + */ + public async createNativeFederationAngularComponent(nativeFederationModule: string, nativeFederationElement: string): Promise { + + const module = await loadRemoteModule({ + remoteName: nativeFederationModule, + exposedModule: nativeFederationElement + }); + + // Locate a DOM node that would be used as a host. + const hostElement = document.createElement('div'); + + const viewContainerRef = this._appRef.components[0].injector.get(ViewContainerRef); + + const componentRef = viewContainerRef.createComponent(module[nativeFederationElement], { + environmentInjector: this.injector.get(EnvironmentInjector), + injector: this.injector + }); + componentRef.changeDetectorRef.detectChanges(); + componentRef.changeDetectorRef.markForCheck(); + + // this.renderer.appendChild(ref.nativeElement, modalElement); + + return hostElement; + + } + + /** + * Applies each available HTML patch to the DOM. + */ + applyPatches(): void { + + // Apply each operation implied by the meta tags + this.patches.forEach(({ operations, elements }) => { + operations.forEach(operation => { + operation.apply($(this.document.body), elements); + }); + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/extension-loader.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/extension-loader.service.ts new file mode 100644 index 0000000000..92d5836d02 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/extension-loader.service.ts @@ -0,0 +1,114 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { loadRemoteModule } from '@angular-architects/native-federation'; +import { HttpClient } from '@angular/common/http'; +import { Injectable, Injector } from '@angular/core'; +import { Route, Router, Routes } from '@angular/router'; +import { BootsrapExtensionFunction } from 'guacamole-frontend-ext-lib'; +import _ from 'lodash'; +import { lastValueFrom } from 'rxjs'; +import { appRoutes, fallbackRoute, titleResolver } from '../../app-routing.module'; +import { DEFAULT_BOOTSTRAP_FUNCTION_NAME, FederationRouteConfig } from '../types/FederationRouteConfig'; + +/** + * A service which dynamically loads all extensions defined in the + * native federation manifest, bootstraps them and adds their routes + * to the main application. + */ +@Injectable({ + providedIn: 'root' +}) +export class ExtensionLoaderService { + + /** + * Inject required services. + */ + constructor(private router: Router, + private injector: Injector, + private http: HttpClient) { + } + + /** + * TODO + */ + async loadExtensionAndSetRouterConfig(): Promise { + + const routeConfig = await lastValueFrom( + this.http.get('native-federation-config.json') + ); + + // Config is empty if no extensions are installed + if (_.isEmpty(routeConfig)) + return; + + this.buildRoutes(routeConfig) + .then(routes => this.router.resetConfig(routes)); + + } + + /** + * Iterates over all extensions defined in the module federation manifest + * to bootstrap them and add their routes to the main application. + * + * @param config + * The module federation manifest. + * + * @returns + * The routes of the main application and all extensions. + */ + async buildRoutes(config: FederationRouteConfig): Promise { + const extensionRoutes: Routes = []; + const keys = Object.keys(config); + + for (const key of keys) { + const entry = config[key]; + const bootstrapFunctionName = entry.bootstrapFunctionName || DEFAULT_BOOTSTRAP_FUNCTION_NAME; + const module = await loadRemoteModule({ + remoteName: key, + exposedModule: bootstrapFunctionName + }); + + // Bootstrap the extension + const bootstrapFunction: BootsrapExtensionFunction = module[bootstrapFunctionName]; + const moduleRoutes = bootstrapFunction(this.injector); + + // Add the routes of the extension to the routes of the main application + const extensionRoute: Route = { + path: entry.routePath, + children: moduleRoutes + }; + + // Use the title of the extension if set, otherwise fall back to the + // title of the main application. + if (entry.pageTitle) + extensionRoute.title = entry.pageTitle; + else { + extensionRoute.data = { title: 'APP.NAME' }; + extensionRoute.title = titleResolver; + } + + + extensionRoutes.push(extensionRoute); + } + + return [...appRoutes, ...extensionRoutes, fallbackRoute]; + } + +} diff --git a/guacamole/src/main/frontend/src/app/index/services/iconService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/icon.service.ts similarity index 63% rename from guacamole/src/main/frontend/src/app/index/services/iconService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/icon.service.ts index ed9e0574f4..83f115a547 100644 --- a/guacamole/src/main/frontend/src/app/index/services/iconService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/icon.service.ts @@ -17,30 +17,30 @@ * under the License. */ +import { Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import $ from 'jquery'; +import { filter } from 'rxjs'; + /** * A service for updating or resetting the favicon of the current page. */ -angular.module('index').factory('iconService', ['$rootScope', function iconService($rootScope) { - - var service = {}; +@Injectable({ + providedIn: 'root' +}) +export class IconService { /** * The URL of the image used for the low-resolution (64x64) favicon. This * MUST match the URL which is set statically within index.html. - * - * @constant - * @type String */ - var DEFAULT_SMALL_ICON_URL = 'images/logo-64.png'; + private readonly DEFAULT_SMALL_ICON_URL = 'images/logo-64.png'; /** * The URL of the image used for the high-resolution (144x144) favicon. This * MUST match the URL which is set statically within index.html. - * - * @constant - * @type String */ - var DEFAULT_LARGE_ICON_URL = 'images/logo-144.png'; + private readonly DEFAULT_LARGE_ICON_URL = 'images/logo-144.png'; /** * JQuery-wrapped array of all link tags which point to the small, @@ -48,7 +48,7 @@ angular.module('index').factory('iconService', ['$rootScope', function iconServi * * @type Element[] */ - var smallIcons = $('link[rel=icon][href="' + DEFAULT_SMALL_ICON_URL + '"]'); + private smallIcons = $('link[rel=icon][href="' + this.DEFAULT_SMALL_ICON_URL + '"]'); /** * JQuery-wrapped array of all link tags which point to the large, @@ -56,50 +56,60 @@ angular.module('index').factory('iconService', ['$rootScope', function iconServi * * @type Element[] */ - var largeIcons = $('link[rel=icon][href="' + DEFAULT_LARGE_ICON_URL + '"]'); + private largeIcons = $('link[rel=icon][href="' + this.DEFAULT_LARGE_ICON_URL + '"]'); + + constructor(private router: Router) { + + // Automatically reset page icons after navigation + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => { + this.setDefaultIcons(); + }); + } /** * Generates an icon by scaling the provided image to fit the given * dimensions, returning a canvas containing the generated icon. * - * @param {HTMLCanvasElement} canvas + * @param canvas * A canvas element containing the image which should be scaled to * produce the contents of the generated icon. * - * @param {Number} width + * @param width * The width of the icon to generate, in pixels. * - * @param {Number} height + * @param height * The height of the icon to generate, in pixels. * - * @returns {HTMLCanvasElement} + * @returns * A new canvas element having the given dimensions and containing the * provided image, scaled to fit. */ - var generateIcon = function generateIcon(canvas, width, height) { + private generateIcon(canvas: HTMLCanvasElement, width: number, height: number): HTMLCanvasElement { // Create icon canvas having the provided dimensions - var icon = document.createElement('canvas'); + const icon = document.createElement('canvas'); icon.width = width; icon.height = height; // Calculate the scale factor necessary to fit the provided image // within the icon dimensions - var scale = Math.min(width / canvas.width, height / canvas.height); + const scale = Math.min(width / canvas.width, height / canvas.height); // Calculate the dimensions and position of the scaled image within // the icon, offsetting the image such that it is centered - var scaledWidth = canvas.width * scale; - var scaledHeight = canvas.height * scale; - var offsetX = (width - scaledWidth) / 2; - var offsetY = (height - scaledHeight) / 2; + const scaledWidth = canvas.width * scale; + const scaledHeight = canvas.height * scale; + const offsetX = (width - scaledWidth) / 2; + const offsetY = (height - scaledHeight) / 2; // Draw the icon, scaling the provided image as necessary - var context = icon.getContext('2d'); - context.drawImage(canvas, offsetX, offsetY, scaledWidth, scaledHeight); + const context = icon.getContext('2d'); + context?.drawImage(canvas, offsetX, offsetY, scaledWidth, scaledHeight); return icon; - }; + } /** * Temporarily sets the icon of the current page to the contents of the @@ -108,41 +118,34 @@ angular.module('index').factory('iconService', ['$rootScope', function iconServi * page icons. The page icons will be automatically reset to their original * values upon navigation. * - * @param {HTMLCanvasElement} canvas + * @param canvas * The canvas element containing the icon. If this value is null or * undefined, this function has no effect. */ - service.setIcons = function setIcons(canvas) { + setIcons(canvas: HTMLCanvasElement | null | undefined): void { // Do nothing if no canvas provided if (!canvas) return; // Assign low-resolution (64x64) icon - var smallIcon = generateIcon(canvas, 64, 64); - smallIcons.attr('href', smallIcon.toDataURL('image/png')); + const smallIcon = this.generateIcon(canvas, 64, 64); + this.smallIcons.attr('href', smallIcon.toDataURL('image/png')); // Assign high-resolution (144x144) icon - var largeIcon = generateIcon(canvas, 144, 144); - largeIcons.attr('href', largeIcon.toDataURL('image/png')); + const largeIcon = this.generateIcon(canvas, 144, 144); + this.largeIcons.attr('href', largeIcon.toDataURL('image/png')); - }; + } /** * Resets the icons of the current page to their original values, undoing * any previous calls to setIcons(). This function is automatically invoked * upon navigation. */ - service.setDefaultIcons = function setDefaultIcons() { - smallIcons.attr('href', DEFAULT_SMALL_ICON_URL); - largeIcons.attr('href', DEFAULT_LARGE_ICON_URL); + setDefaultIcons() { + this.smallIcons.attr('href', this.DEFAULT_SMALL_ICON_URL); + this.largeIcons.attr('href', this.DEFAULT_LARGE_ICON_URL); }; - // Automatically reset page icons after navigation - $rootScope.$on('$routeChangeSuccess', function resetIcon() { - service.setDefaultIcons(); - }); - - return service; - -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/style-loader.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/style-loader.service.ts new file mode 100644 index 0000000000..7b99e1da00 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/services/style-loader.service.ts @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Inject, Injectable, Renderer2, RendererFactory2 } from '@angular/core'; + + +const STYLE_ELEMENT_ID = 'loaded-style-'; + +/** + * Service for dynamically loading additional styles at runtime. + * Used to load the styles of extensions. + */ +@Injectable({ + providedIn: 'root' +}) +export class StyleLoaderService { + + /** + * The Angular renderer used to manipulate the DOM. + */ + private renderer: Renderer2; + + /** + * The build identifier of the Guacamole build that produced + * the index.html. + */ + private buildMeta: string; + + /** + * Inject required Services. + */ + constructor(@Inject(DOCUMENT) private document: Document, + private rendererFactory: RendererFactory2) { + this.renderer = this.rendererFactory.createRenderer(null, null); + this.buildMeta = this.renderer.selectRootElement('meta[name=build]')?.content; + } + + /** + * Loads the given CSS file by adding a link element for it to + * the head of the page. + * + * @param styleName + * The name of the CSS file that should be loaded. + */ + loadStyle(styleName: string): void { + + const elementId = STYLE_ELEMENT_ID + styleName; + + // If the element already exists there is nothing to do + if (this.document.getElementById(elementId)) + return; + + // Create a link element via Angular's renderer + const styleElement = this.renderer.createElement('link') as HTMLLinkElement; + + // Add the style to the head section + this.renderer.appendChild(this.document.head, styleElement); + + // Set type of the link item and path to the css file + this.renderer.setProperty(styleElement, 'rel', 'stylesheet'); + this.renderer.setProperty(styleElement, 'id', elementId); + this.renderer.setProperty(styleElement, 'href', `${styleName}?b=${this.buildMeta}`); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/index/styles/animation.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/animation.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/animation.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/animation.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/automatic-login-rejected.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/automatic-login-rejected.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/automatic-login-rejected.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/buttons.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/buttons.css similarity index 99% rename from guacamole/src/main/frontend/src/app/index/styles/buttons.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/buttons.css index 2f2ce9e2e0..0c88098da6 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/buttons.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/buttons.css @@ -23,7 +23,7 @@ a.button { } input[type="submit"], button, a.button { - + -webkit-appearance: none; text-decoration: none; @@ -51,8 +51,8 @@ input[type="submit"]:hover, button:hover, a.button:hover { input[type="submit"]:active, button:active, a.button:active { background-color: #2C2C2C; - - box-shadow: + + box-shadow: inset 1px 1px 0.25em rgba(0, 0, 0, 0.25), -1px -1px 0.25em rgba(0, 0, 0, 0.25), 1px 1px 0.25em rgba(255, 255, 255, 0.25); diff --git a/guacamole/src/main/frontend/src/app/index/styles/cloak.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/cloak.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/cloak.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/cloak.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/fatal-page-error.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/fatal-page-error.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/fatal-page-error.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/font-carlito.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/font-carlito.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/font-carlito.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/font-carlito.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/headers.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/headers.css similarity index 99% rename from guacamole/src/main/frontend/src/app/index/styles/headers.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/headers.css index 457eb5210b..45189a8b2d 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/headers.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/headers.css @@ -18,7 +18,7 @@ */ h1 { - + margin: 0; padding: 0.5em; @@ -56,7 +56,7 @@ h2 { display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; diff --git a/guacamole/src/main/frontend/src/app/index/styles/input.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/input.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/input.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/input.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/lists.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/lists.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/lists.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/lists.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/loading.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/loading.css similarity index 98% rename from guacamole/src/main/frontend/src/app/index/styles/loading.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/loading.css index d7d81aeefa..8e2be43a7a 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/loading.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/loading.css @@ -39,25 +39,25 @@ display: block; position: absolute; content: ''; - + /* Dictated by size of image */ width: 96px; height: 96px; margin-left: -48px; margin-top: -48px; - + top: 50%; left: 50%; - + background-image: url('images/cog.svg'); background-size: 96px 96px; background-position: center center; background-repeat: no-repeat; - + animation: spinning-cog 4s linear infinite; -moz-animation: spinning-cog 4s linear infinite; -webkit-animation: spinning-cog 4s linear infinite; - + } @keyframes spinning-cog { diff --git a/guacamole/src/main/frontend/src/app/index/styles/logged-out.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/logged-out.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/logged-out.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/logged-out.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/other-connections.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/other-connections.css similarity index 97% rename from guacamole/src/main/frontend/src/app/index/styles/other-connections.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/other-connections.css index 3fc8ff3848..805b7e7564 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/other-connections.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/other-connections.css @@ -56,7 +56,7 @@ background-repeat: no-repeat; background-size: contain; background-position: center center; - background-image: url(images/arrows/right.svg); + background-image: url('images/arrows/right.svg'); opacity: 0.5; } @@ -66,7 +66,7 @@ } #other-connections .client-panel.hidden .client-panel-handle { - background-image: url(images/arrows/left.svg); + background-image: url('images/arrows/left.svg'); } #other-connections .client-panel-connection-list { diff --git a/guacamole/src/main/frontend/src/app/index/styles/sorted-tables.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/sorted-tables.css similarity index 100% rename from guacamole/src/main/frontend/src/app/index/styles/sorted-tables.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/sorted-tables.css diff --git a/guacamole/src/main/frontend/src/app/index/styles/status.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/status.css similarity index 99% rename from guacamole/src/main/frontend/src/app/index/styles/status.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/status.css index 7fd63672dc..94b2f6e148 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/status.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/status.css @@ -30,7 +30,7 @@ overflow: auto; text-align: left; - + } .global-status-modal .notification .body { diff --git a/guacamole/src/main/frontend/src/app/index/styles/ui.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/ui.css similarity index 99% rename from guacamole/src/main/frontend/src/app/index/styles/ui.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/ui.css index 453831fa8a..0559d51cbc 100644 --- a/guacamole/src/main/frontend/src/app/index/styles/ui.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/styles/ui.css @@ -221,7 +221,7 @@ div.section { margin-left: 13px; padding-left: 13px; } - + .connection-group.empty.balancer .icon { background-image: url('images/protocol-icons/guac-monitor.svg'); } diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/FederationRouteConfig.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/FederationRouteConfig.ts new file mode 100644 index 0000000000..e30b9a6b41 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/FederationRouteConfig.ts @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * TODO + */ +export const DEFAULT_BOOTSTRAP_FUNCTION_NAME = 'bootsrapExtension'; + +/** + * TODO + */ +export interface FederationRouteConfigEntry { + + /** + * The name of the function that will be called by the shell to bootstrap the extension. + * + * @default DEFAULT_BOOTSTRAP_FUNCTION_NAME + */ + bootstrapFunctionName?: string; + + /** + * The title of the page to be displayed in the browser tab when the route is active. + * If not set, the title of the main application will be used. Specific routes can + * override this value by setting the title property on the route. + */ + pageTitle?: string; + + /** + * The path where the routes of the extension should be mounted. + */ + routePath: string; +} + +/** + * A map with the name of the remote module as key and FederationRouteConfigEntry as value. + */ +export type FederationRouteConfig = Record; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/PatchOperation.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/PatchOperation.ts new file mode 100644 index 0000000000..ea70009ca7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/index/types/PatchOperation.ts @@ -0,0 +1,142 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Represents a single HTML patching operation which will be applied + * to the raw HTML of a template. The name of the patching operation + * MUST be one of the valid names defined within + * PatchOperation.Operations. + */ +export class PatchOperation { + + /** + * Creates a new PatchOperation. + * + * @param name + * The name of the patching operation that will be applied. Valid + * names are defined within PatchOperation.Operations. + * + * @param selector + * The CSS selector which determines which elements within a + * template will be affected by the patch operation. + */ + constructor(private name: string, private selector: string) { + } + + /** + * Applies this patch operation to the template defined by the + * given root element, which must be a single element wrapped by + * JQuery. + * + * @param root + * The JQuery-wrapped root element of the template to which + * this patch operation should be applied. + * + * @param elements + * The elements which should be applied by the patch + * operation. For example, if the patch operation is inserting + * elements, these are the elements that will be inserted. + */ + apply(root: JQuery, elements: Element[]): void { + PatchOperation.Operations[this.name](root, this.selector, elements); + } + + /** + * Mapping of all valid patch operation names to their corresponding + * implementations. Each implementation accepts the same three + * parameters: the root element of the template being patched, the CSS + * selector determining which elements within the template are patched, + * and an array of elements which make up the body of the patch. + */ + static Operations: Record = { + + /** + * Inserts the given elements before the elements matched by the + * provided CSS selector. + */ + 'before': (root, selector, elements) => { + root.find(selector).before(elements); + }, + + /** + * Inserts the given elements after the elements matched by the + * provided CSS selector. + */ + 'after': (root, selector, elements) => { + root.find(selector).after(elements); + }, + + /** + * Replaces the elements matched by the provided CSS selector with + * the given elements. + */ + 'replace': (root, selector, elements) => { + root.find(selector).replaceWith(elements); + }, + + /** + * Inserts the given elements within the elements matched by the + * provided CSS selector, before any existing children. + */ + 'before-children': (root, selector, elements) => { + root.find(selector).prepend(elements); + }, + + /** + * Inserts the given elements within the elements matched by the + * provided CSS selector, after any existing children. + */ + 'after-children': (root, selector, elements) => { + root.find(selector).append(elements); + }, + + /** + * Inserts the given elements within the elements matched by the + * provided CSS selector, replacing any existing children. + */ + 'replace-children': (root, selector, elements) => { + root.find(selector).empty().append(elements); + } + + }; + +} + +export namespace PatchOperation { + + /** + * A function which applies a specific patch operation to the + * given element. + * + * @param root + * The JQuery-wrapped root element of the template being + * patched. + * + * @param selector + * The CSS selector which determines where this patch operation + * should be applied within the template defined by root. + * + * @param elements + * The contents of the patch which should be applied to the + * template defined by root at the locations selected by the + * given CSS selector. + */ + export type PatchOperationFunction = (root: JQuery, selector: string, elements: Element[]) => void; +} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.html new file mode 100644 index 0000000000..301561fddd --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.html @@ -0,0 +1,29 @@ + + +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.ts new file mode 100644 index 0000000000..1ccdab34b1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-filter/guac-filter.component.ts @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +/** + * A component which provides a filtering text input field. + */ +@Component({ + selector: 'guac-filter', + templateUrl: './guac-filter.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacFilterComponent { + + /** + * The property to which a subset of the provided array will be + * assigned. + */ + searchStringChange: BehaviorSubject = new BehaviorSubject(''); + + /** + * The placeholder text to display within the filter input field + * when no filter has been provided. + */ + @Input() placeholder?: string; + + /** + * The filter search string to use to restrict the displayed items. + */ + protected searchString: string | null = null; + + /** + * TODO: Document + */ + searchStringChanged(searchString: string) { + this.searchStringChange.next(searchString); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.html new file mode 100644 index 0000000000..2cd516ed0f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.html @@ -0,0 +1,47 @@ + + +
      + + +
      +
      + + +
      ...
      + + +
        +
      • {{ pageNumber }} +
      • +
      + + +
      ...
      + + +
      +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.ts new file mode 100644 index 0000000000..17240be128 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-pager/guac-pager.component.ts @@ -0,0 +1,289 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; + +/** + * The default size of a page, if not provided via the pageSize + * attribute. + */ +const DEFAULT_PAGE_SIZE = 10; + +/** + * The default maximum number of page choices to provide, if a + * value is not provided via the pageCount attribute. + */ +const DEFAULT_PAGE_COUNT = 11; + +/** + * Event data emitted by GuacPagerComponent when the user selects a + * new page. + */ +export interface PagerEvent { + + /** + * The index of the page that is currently selected. + */ + pageIndex: number; + + /** + * The current page size. + */ + pageSize: number; + + /** + * The index of the page that was previously selected. + */ + previousPageIndex: number; +} + +/** + * A component which provides pagination controls, along with a paginated + * subset of the elements of some given array. + */ +@Component({ + selector: 'guac-pager', + templateUrl: './guac-pager.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacPagerComponent implements OnChanges { + + /** + * Behavior subject which emits information about the current page + * whenever the user selects a new page. + */ + page: BehaviorSubject = new BehaviorSubject({ + pageIndex : 0, + pageSize : 0, + previousPageIndex: 0 + }); + + /** + * The maximum number of items per page. + * + * @default {@link DEFAULT_PAGE_SIZE} + */ + @Input() pageSize: number | null | undefined = DEFAULT_PAGE_SIZE; + + /** + * The maximum number of page choices to provide, regardless of the + * total number of pages. + * + * @default {@link DEFAULT_PAGE_COUNT} + */ + @Input() pageCount: number = DEFAULT_PAGE_COUNT; + + /** + * The length of the array to paginate. + */ + @Input({ required: true }) length!: number | null; + + /** + * The number of the first selectable page. + */ + firstPage = 1; + + /** + * The number of the page immediately before the currently-selected + * page. + */ + previousPage = 1; + + /** + * The number of the currently-selected page. + */ + currentPage = 1; + + /** + * The number of the page immediately after the currently-selected + * page. + */ + nextPage = 1; + + /** + * The number of the last selectable page. + */ + lastPage = 1; + + /** + * An array of relevant page numbers that the user may want to jump + * to directly. + */ + pageNumbers: number[] = []; + + /** + * Updates the displayed page number choices. + */ + private updatePageNumbers(): void { + + // Get page count + const pageCount = this.pageCount || DEFAULT_PAGE_COUNT; + + // Determine start/end of page window + let windowStart = this.currentPage - (pageCount - 1) / 2; + let windowEnd = windowStart + pageCount - 1; + + // Shift window as necessary if it extends beyond the first page + if (windowStart < this.firstPage) { + windowEnd = Math.min(this.lastPage, windowEnd - windowStart + this.firstPage); + windowStart = this.firstPage; + } + + // Shift window as necessary if it extends beyond the last page + else if (windowEnd > this.lastPage) { + windowStart = Math.max(1, windowStart - windowEnd + this.lastPage); + windowEnd = this.lastPage; + } + + // Generate list of relevant page numbers + this.pageNumbers = []; + for (let pageNumber = windowStart; pageNumber <= windowEnd; pageNumber++) + this.pageNumbers.push(pageNumber); + + } + + /** + * Iterates through the bound items array, splitting it into pages + * based on the current page size. + */ + private updatePages(): void { + + // Get current items and page size + const length = this.length || 0; + const pageSize = this.pageSize || DEFAULT_PAGE_SIZE; + + // Update minimum and maximum values + this.firstPage = 1; + this.lastPage = Math.max(1, Math.ceil(length / pageSize)); + + // Select an appropriate page + const adjustedCurrentPage = Math.min(this.lastPage, Math.max(this.firstPage, this.currentPage)); + this.selectPage(adjustedCurrentPage); + + } + + /** + * Selects the page having the given number, assigning that page to + * the property bound to the page attribute. If no such page + * exists, the property will be set to undefined instead. Valid + * page numbers begin at 1. + * + * @param page + * The number of the page to select. Valid page numbers begin + * at 1. + */ + selectPage(page: number): void { + + // Select the chosen page + this.currentPage = page; + this.page.next({ + pageIndex : page - 1, + pageSize : this.pageSize || DEFAULT_PAGE_SIZE, + previousPageIndex: this.previousPage + }); + + // Update next/previous page numbers + this.nextPage = Math.min(this.lastPage, this.currentPage + 1); + this.previousPage = Math.max(this.firstPage, this.currentPage - 1); + + // Update which page numbers are shown + this.updatePageNumbers(); + + } + + /** + * Returns whether the given page number can be legally selected + * via selectPage(), resulting in a different page being shown. + * + * @param page + * The page number to check. + * + * @returns + * true if the page having the given number can be selected, + * false otherwise. + */ + canSelectPage(page: number): boolean { + return page !== this.currentPage + && page >= this.firstPage + && page <= this.lastPage; + } + + /** + * Returns whether the page having the given number is currently + * selected. + * + * @param page + * The page number to check. + * + * @returns + * true if the page having the given number is currently + * selected, false otherwise. + */ + isSelected(page: number): boolean { + return page === this.currentPage; + } + + /** + * Returns whether pages exist before the first page listed in the + * pageNumbers array. + * + * @returns + * true if pages exist before the first page listed in the + * pageNumbers array, false otherwise. + */ + hasMorePagesBefore(): boolean { + const firstPageNumber = this.pageNumbers[0]; + return firstPageNumber !== this.firstPage; + } + + /** + * Returns whether pages exist after the last page listed in the + * pageNumbers array. + * + * @returns + * true if pages exist after the last page listed in the + * pageNumbers array, false otherwise. + */ + hasMorePagesAfter(): boolean { + const lastPageNumber = this.pageNumbers[this.pageNumbers.length - 1]; + return lastPageNumber !== this.lastPage; + } + + ngOnChanges(changes: SimpleChanges): void { + + // Use default page size if none is specified + if (changes['pageSize']) + this.pageSize = this.pageSize || DEFAULT_PAGE_SIZE; + + + // Update available pages when available items or page count is changed + if (changes['length'] || changes['pageSize']) + this.updatePages(); + + + // Update available page numbers when page count is changed + if (changes['length'] || changes['pageCount']) + this.updatePageNumbers(); + + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.html new file mode 100644 index 0000000000..e7859b61be --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.html @@ -0,0 +1,22 @@ + + +
      + {{ displayName }} +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.ts new file mode 100644 index 0000000000..d6334e739b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/components/guac-user-item/guac-user-item.component.ts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { take } from 'rxjs'; +import { AuthenticationResult } from '../../../auth/types/AuthenticationResult'; + +/** + * A component which graphically represents an individual user. + */ +@Component({ + selector: 'guac-user-item', + templateUrl: './guac-user-item.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacUserItemComponent implements OnChanges { + + /** + * The username of the user represented by this guacUserItem. + */ + @Input() username?: string | undefined; + + /** + * The string to display when listing the user having the provided + * username. Generally, this will be the username itself, but can + * also be an arbitrary human-readable representation of the user, + * or null if the display name is not yet determined. + */ + displayName: string | null; + + constructor(private translocoService: TranslocoService) { + this.displayName = null; + } + + /** + * Returns whether the username provided to this directive denotes + * a user that authenticated anonymously. + * + * @returns {Boolean} + * true if the username provided represents an anonymous user, + * false otherwise. + */ + isAnonymous(): boolean { + return this.username === AuthenticationResult.ANONYMOUS_USERNAME; + } + + ngOnChanges(changes: SimpleChanges): void { + + // Update display name whenever provided username changes + if (changes['username']) { + + const username = changes['username'].currentValue as string; + + // If the user is anonymous, pull the display name for anonymous + // users from the translation service + if (this.isAnonymous()) { + this.translocoService.selectTranslate('LIST.TEXT_ANONYMOUS_USER') + .pipe(take(1)) + .subscribe(anonymousDisplayName => { + this.displayName = anonymousDisplayName; + }); + } + + // For all other users, use the username verbatim + else + this.displayName = username; + } + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/directives/guac-sort-order.directive.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/directives/guac-sort-order.directive.ts new file mode 100644 index 0000000000..6eff9e9756 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/directives/guac-sort-order.directive.ts @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Directive, + ElementRef, + EventEmitter, + HostListener, + Input, + OnChanges, + OnInit, + Output, + Renderer2, + SimpleChanges +} from '@angular/core'; +import { SortOrder } from '../types/SortOrder'; + +/** + * Updates the priority of the sorting property given by "guacSortProperty" + * within the SortOrder object given by "guacSortOrder". The CSS classes + * "sort-primary" and "sort-descending" will be applied to the associated + * element depending on the priority and sort direction of the given property. + * + * The associated element will automatically be assigned the "sortable" CSS + * class. + */ +@Directive({ + selector: '[guacSortOrder]', + standalone: false +}) +export class GuacSortOrderDirective implements OnInit, OnChanges { + + /** + * The object defining the sorting order. + */ + @Input({ required: true }) guacSortOrder: SortOrder | null = null; + + /** + * Event raised when the sort order changes due to user interaction. + */ + @Output() sortOrderChange = new EventEmitter(); + + /** + * The name of the property whose priority within the sort order + * is controlled by this directive. + */ + @Input() sortProperty!: string; + + /** + * Update sort order when clicked. + */ + @HostListener('click') onClick() { + + if (!this.guacSortOrder) return; + + this.guacSortOrder.togglePrimary(this.sortProperty); + this.onSortOrderChange(); + + // Emit the new sort order + this.sortOrderChange.emit(new SortOrder(this.guacSortOrder.predicate)); + } + + /** + * The element associated with this directive. + */ + private readonly element: Element; + + /** + * Inject required services and references. + */ + constructor(elementRef: ElementRef, private renderer: Renderer2) { + this.element = elementRef.nativeElement; + } + + /** + * Assign "sortable" class to associated element + */ + ngOnInit(): void { + this.renderer.addClass(this.element, 'sortable'); + } + + /** + * Returns whether the sort property defined via the + * "guacSortProperty" attribute is the primary sort property of + * the associated sort order. + * + * @returns + * true if the sort property defined via the + * "guacSortProperty" attribute is the primary sort property, + * false otherwise. + */ + private isPrimary(): boolean { + return this.guacSortOrder?.primary === this.sortProperty; + } + + /** + * Returns whether the primary property of the sort order is + * sorted in descending order. + * + * @returns + * true if the primary property of the sort order is sorted in + * descending order, false otherwise. + */ + private isDescending(): boolean { + return !!this.guacSortOrder?.descending; + } + + ngOnChanges(changes: SimpleChanges): void { + + if (changes['guacSortOrder']) { + this.onSortOrderChange(); + } + + } + + onSortOrderChange(): void { + // Add/remove "sort-primary" class depending on sort order + const primary = this.isPrimary(); + + if (primary) + this.renderer.addClass(this.element, 'sort-primary'); + else + this.renderer.removeClass(this.element, 'sort-primary'); + + // Add/remove "sort-descending" class depending on sort order + const descending = this.isDescending(); + + if (descending) + this.renderer.addClass(this.element, 'sort-descending'); + else + this.renderer.removeClass(this.element, 'sort-descending'); + + } + + +} diff --git a/guacamole/src/main/frontend/src/app/list/listModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/list.module.ts similarity index 50% rename from guacamole/src/main/frontend/src/app/list/listModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/list.module.ts index 845c8c968c..8232d9b237 100644 --- a/guacamole/src/main/frontend/src/app/list/listModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/list.module.ts @@ -17,10 +17,35 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { GuacFilterComponent } from './components/guac-filter/guac-filter.component'; +import { GuacPagerComponent } from './components/guac-pager/guac-pager.component'; +import { GuacUserItemComponent } from './components/guac-user-item/guac-user-item.component'; +import { GuacSortOrderDirective } from './directives/guac-sort-order.directive'; + /** * Module for displaying, sorting, and filtering the contents of a list, split * into multiple pages. */ -angular.module('list', [ - 'auth' -]); +@NgModule({ + declarations: [ + GuacFilterComponent, + GuacPagerComponent, + GuacSortOrderDirective, + GuacUserItemComponent + ], + imports : [ + CommonModule, + FormsModule + ], + exports : [ + GuacFilterComponent, + GuacPagerComponent, + GuacSortOrderDirective, + GuacUserItemComponent + ] +}) +export class ListModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.spec.ts new file mode 100644 index 0000000000..b9a640da0d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.spec.ts @@ -0,0 +1,216 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { from, map, of } from 'rxjs'; +import { TestScheduler } from 'rxjs/internal/testing/TestScheduler'; +import { getTestScheduler } from '../../util/test-helper'; +import { PagerEvent } from '../components/guac-pager/guac-pager.component'; +import { DataSource } from '../types/DataSource'; +import { SortOrder } from '../types/SortOrder'; +import { DataSourceBuilderService } from './data-source-builder.service'; + +interface Person { + id: number; + name: string; +} + +describe('DataSourceBuilderService', () => { + let service: DataSourceBuilderService; + let testScheduler: TestScheduler; + const source: Person[] = [ + { id: 1, name: 'John' }, + { id: 2, name: 'Jane' }, + { id: 3, name: 'Jack' }, + { id: 4, name: 'Jill' }, + { id: 5, name: 'Joe' }, + { id: 6, name: 'Jenny' }, + { id: 7, name: 'Jim' }, + { id: 8, name: 'Jen' }, + { id: 9, name: 'Jesse' }, + { id: 10, name: 'Jasmine' } + ]; + + beforeEach(() => { + testScheduler = getTestScheduler(); + TestBed.configureTestingModule({}); + service = TestBed.inject(DataSourceBuilderService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); + + describe('source', () => { + + it('should return source and complete if noting else is configured', () => { + testScheduler.run(({ expectObservable }) => { + + const dataSource = service.getBuilder().source(source).build(); + + const data = dataSource.data; + + expectObservable(data).toBe('(0|)', [source]); + + }); + }); + + }); + describe('totalLength', () => { + + + it('should return source length if no filter is configured', () => { + testScheduler.run(({ expectObservable }) => { + + const dataSource = service.getBuilder().source(source).build(); + const length = dataSource.totalLength; + + expectObservable(length).toBe('(a|)', { a: source.length }); + + }); + }); + + + it('should return length filtered source', () => { + testScheduler.run(({ expectObservable }) => { + + const dataSource: DataSource = service.getBuilder() + .source(source) + .filter(of('ji'), ['name']) + .build(); + + const length = dataSource.totalLength; + + expectObservable(length).toBe('(a|)', { a: 2 }); + + }); + }); + + }); + + describe('sort', () => { + + it('should return source sorted by name ascending', () => { + testScheduler.run(({ expectObservable }) => { + + const dataSource: DataSource = service.getBuilder() + .source(source) + .sort(of(new SortOrder(['name']))) + .build(); + + + const data = dataSource.data + // Map to ids + .pipe(map(data => data.map(p => p.id))); + + expectObservable(data).toBe('(0|)', [[3, 2, 10, 8, 6, 9, 4, 7, 5, 1]]); + + }); + }); + + it('should return source sorted by name descending', () => { + testScheduler.run(({ expectObservable }) => { + + const dataSource: DataSource = service.getBuilder() + .source(source) + .sort(of(new SortOrder(['-name']))) + .build(); + + const data = dataSource.data + // Map to ids + .pipe(map(data => data.map(p => p.id))); + + expectObservable(data).toBe('(0|)', [[1, 5, 7, 4, 9, 6, 8, 10, 2, 3]]); + + }); + }); + + }); + + describe('paginate', () => { + + + it('should return source paginated', () => { + + testScheduler.run(({ expectObservable }) => { + + const pageEvents: PagerEvent[] = [ + { pageIndex: 0, pageSize: 3, previousPageIndex: 0 }, + { pageIndex: 1, pageSize: 3, previousPageIndex: 0 }, + { pageIndex: 2, pageSize: 3, previousPageIndex: 1 }, + { pageIndex: 3, pageSize: 3, previousPageIndex: 2 }, + ]; + + + const dataSource: DataSource = service.getBuilder() + .source(source) + .paginate(from(pageEvents)) + .build(); + + const page = dataSource.data + // Map to ids + .pipe(map(data => data.map(p => p.id))); + + expectObservable(page).toBe('(0 1 2 3|)', [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [10] + ]); + + }); + + }); + + }); + + + describe('updateSource', () => { + + it('should update source and emit new data', () => { + testScheduler.run(({ expectObservable, flush }) => { + + const firstSource: Person[] = source.slice(0, 5); + const secondSource: Person[] = source.slice(5, 10); + + const dataSource: DataSource = service.getBuilder() + .source(firstSource) + .sort(of(new SortOrder(['-name']))) + .build(); + + let data = dataSource.data + // Map to ids + .pipe(map(data => data.map(p => p.id))); + + expectObservable(data).toBe('(0|)', [[1, 5, 4, 2, 3]]); + + flush(); + + dataSource.updateSource(secondSource); + data = dataSource.data + // Map to ids + .pipe(map(data => data.map(p => p.id))); + + expectObservable(data).toBe('(0|)', [[7, 9, 6, 8, 10]]); + + }); + }); + + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.ts new file mode 100644 index 0000000000..a27e98cf9f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/data-source-builder.service.ts @@ -0,0 +1,49 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { DataSourceBuilder } from '../types/DataSourceBuilder'; +import { FilterService } from './filter.service'; +import { PaginationService } from './pagination.service'; +import { SortService } from './sort.service'; + +/** + * A service that allows to create a DataSource instance by using a builder. + * A data source can be configured with a filter, a sort order and pagination. + */ +@Injectable({ + providedIn: 'root' +}) +export class DataSourceBuilderService { + + /** + * Inject required services. + */ + constructor(private sortService: SortService, + private filterService: FilterService, + private paginationService: PaginationService) { + } + + /** + * Creates a new DataSourceBuilder. + */ + getBuilder(): DataSourceBuilder { + return new DataSourceBuilder(this.sortService, this.filterService, this.paginationService); + } +} diff --git a/guacamole/src/main/frontend/src/app/form/controllers/textFieldController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/filter.service.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/form/controllers/textFieldController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/filter.service.ts index 8dd134ab5b..857c0f6494 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/textFieldController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/filter.service.ts @@ -17,24 +17,30 @@ * under the License. */ +import { Injectable } from '@angular/core'; /** - * Controller for text fields. + * TODO: Document + * Replacement for AngularJS filter function https://docs.angularjs.org/api/ng/filter/filter */ -angular.module('form').controller('textFieldController', ['$scope', '$injector', - function textFieldController($scope, $injector) { +@Injectable({ + providedIn: 'root' +}) +export class FilterService { /** - * The ID of the datalist element that should be associated with the text - * field, providing a set of known-good values. If no such values are - * defined, this will be null. + * TODO: Document * - * @type String + * @template T + * The type of the elements in the target array. + * + * @param target + * @param predicate */ - $scope.dataListId = null; - - // Generate unique ID for datalist, if applicable - if ($scope.field.options && $scope.field.options.length) - $scope.dataListId = $scope.fieldId + '-datalist'; + filterByPredicate(target: T[] | null | undefined, predicate: (value: T, index: number, array: T[]) => boolean): T[] { + if (!target) + return []; -}]); + return target.filter(predicate); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/pagination.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/pagination.service.ts new file mode 100644 index 0000000000..44e2b5bb1b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/pagination.service.ts @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; + +/** + * Service for the pagination of an arbitrary array. + */ +@Injectable({ + providedIn: 'root' +}) +export class PaginationService { + + /** + * Extracts a page from the given source. + * + * @template T + * The type of the elements in the source array. + * + * @param source + * The source from which the page should be extracted. + * + * @param pageIndex + * The index of the page that should be returned. + * + * @param pageSize + * The size of each page. + * + * @returns + * The page with the given index. + */ + paginate(source: T[] | null, pageIndex: number, pageSize: number): T[] { + + if (!source) + return []; + + const startIndex = pageIndex * pageSize; + const endIndex = startIndex + pageSize; + + return source.slice(startIndex, endIndex); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.spec.ts new file mode 100644 index 0000000000..72fc7f8459 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.spec.ts @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { SortService } from './sort.service'; + +describe('orderByPredicate', () => { + + let service: SortService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SortService] + }); + + service = TestBed.get(SortService); + }); + + + it('should sort the collection in ascending order based on a single predicate', () => { + const collection = [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 25 }, + { name: 'Bob', age: 35 }, + ]; + const predicates = ['age']; + + const result = service.orderByPredicate(collection, predicates); + + expect(result).toEqual([ + { name: 'Jane', age: 25 }, + { name: 'John', age: 30 }, + { name: 'Bob', age: 35 }, + ]); + }); + + it('should sort the collection in descending order based on a single predicate', () => { + const collection = [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 25 }, + { name: 'Bob', age: 35 }, + ]; + const predicates = ['-age']; + + const result = service.orderByPredicate(collection, predicates); + + expect(result).toEqual([ + { name: 'Bob', age: 35 }, + { name: 'John', age: 30 }, + { name: 'Jane', age: 25 }, + ]); + }); + + it('should sort the collection based on multiple predicates', () => { + const collection = [ + { name: 'John', age: 30, salary: 50000 }, + { name: 'Jane', age: 25, salary: 60000 }, + { name: 'Bob', age: 35, salary: 40000 }, + { name: 'Alice', age: 25, salary: 50000 }, + ]; + const predicates = ['age', '-salary']; + + const result = service.orderByPredicate(collection, predicates); + + expect(result).toEqual([ + { name: 'Jane', age: 25, salary: 60000 }, + { name: 'Alice', age: 25, salary: 50000 }, + { name: 'John', age: 30, salary: 50000 }, + { name: 'Bob', age: 35, salary: 40000 }, + ]); + }); + + it('should return the same collection if no predicates are provided', () => { + const collection = [ + { name: 'John', age: 30 }, + { name: 'Jane', age: 25 }, + { name: 'Bob', age: 35 }, + ]; + const predicates: string[] = []; + + const result = service.orderByPredicate(collection, predicates); + + expect(result).toEqual(collection); + }); + + it('should return an empty collection if the input collection is empty', () => { + const collection: any[] = []; + const predicates = ['age']; + + const result = service.orderByPredicate(collection, predicates); + + expect(result).toEqual([]); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.ts new file mode 100644 index 0000000000..eb6db25c06 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/services/sort.service.ts @@ -0,0 +1,90 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import AngularExpression from 'angular-expressions'; +import _ from 'lodash'; + +/** + * TODO: Document + */ +@Injectable({ + providedIn: 'root' +}) +export class SortService { + + /** + * TODO Document + * @template T + * The type of the elements in the target array. + * + * @param target + * @param iteratees + */ + orderBy(target: T[], iteratees?: (keyof T)[]): T[] { + return _.orderBy(target, iteratees); + } + + /** + * Sorts the given collection by the given predicate. + * The predicate is a string (AngularJS expression) that can be compiled into a function. + * This function will be evaluated against each item and the result will be used for sorting. + * The predicate can be prefixed with a minus sign to indicate descending order. + * + * @template T + * The type of the elements in the collection array. + * + * @param collection + * The collection to sort. + * + * @param predicates + * A list of predicates to be used for sorting. + * + * @returns + * The sorted collection. + */ + orderByPredicate(collection: T[] | null | undefined, predicates: string[]): T[] { + if (!collection || collection.length === 0) { + return []; + } + + // Predicates starting with a minus sign indicate descending order + const orders = predicates.map(p => p.startsWith('-') ? 'desc' : 'asc'); + + const expressions = predicates.map(p => { + const expression = p.startsWith('-') || p.startsWith('+') ? p.substring(1) : p; + // Compare strings in a case-insensitive manner + return (item: T) => { + const value = AngularExpression.compile(expression)(item); + + // If the value is a number, return it as-is + if (typeof value === 'number') + return value; + + // Try to convert to string and compare in a case-insensitive manner + if (value?.toString) + return value.toString().toLowerCase(); + + return null; + }; + }); + + return _.orderBy(collection, expressions, orders); + } +} diff --git a/guacamole/src/main/frontend/src/app/list/styles/filter.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/filter.css similarity index 100% rename from guacamole/src/main/frontend/src/app/list/styles/filter.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/filter.css diff --git a/guacamole/src/main/frontend/src/app/list/styles/pager.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/pager.css similarity index 98% rename from guacamole/src/main/frontend/src/app/list/styles/pager.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/pager.css index 7baa665b87..2b0793a79b 100644 --- a/guacamole/src/main/frontend/src/app/list/styles/pager.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/pager.css @@ -47,7 +47,7 @@ } .pager .set-page, -.pager .more-pages { +.pager .more-pages:not([hidden]) { display: inline-block; padding: 0.25em; text-align: center; diff --git a/guacamole/src/main/frontend/src/app/list/styles/user-item.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/user-item.css similarity index 100% rename from guacamole/src/main/frontend/src/app/list/styles/user-item.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/styles/user-item.css diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSource.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSource.ts new file mode 100644 index 0000000000..3ec5148c6e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSource.ts @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { combineLatest, finalize, map, Observable, of, Subject, takeUntil } from 'rxjs'; +import { PagerEvent } from '../components/guac-pager/guac-pager.component'; +import { FilterService } from '../services/filter.service'; +import { PaginationService } from '../services/pagination.service'; +import { SortService } from '../services/sort.service'; +import { FilterPattern } from './FilterPattern'; +import { SortOrder } from './SortOrder'; + +/** + * A data source which wraps a source array and applies filtering, sorting and pagination. + * + * @template T + * The type of the elements in the data source. + */ +export class DataSource { + + /** + * An observable which emits the filtered, sorted and paginated data of the source array. + */ + data!: Observable; + + /** + * An observable which emits the length of the filtered source array. + */ + totalLength!: Observable; + + /** + * The pattern object to use when filtering items. + */ + private readonly filterPattern: FilterPattern | null = null; + + /** + * A subject which emits when the currently loaded data is replaced by new data. + */ + private dataComplete!: Subject; + + /** + * Creates a new DataSource, which wraps the given source array and + * applies filtering, sorting and pagination. + */ + constructor(private sortService: SortService, + private filterService: FilterService, + private paginationService: PaginationService, + private source: T[], + private searchString: Observable | null, + private filterProperties: string[] | null, + private sort: Observable | null, + private paginate: Observable | null) { + + if (filterProperties !== null) + this.filterPattern = new FilterPattern(filterProperties); + + this.computeData(); + } + + /** + * TODO: Document + */ + private computeData(): void { + this.dataComplete = new Subject(); + + this.totalLength = (this.searchString || of(null as string | null)).pipe( + // Also complete when the data observable completes. + takeUntil(this.dataComplete), + map(filter => { + return this.applyFilter(this.source, filter).length; + }), + ); + + // If an operation is null, replace it with an observable which emits null so that combineLatest + // will emit when the other operations emit. + const operations = [ + this.searchString || of(null as string | null), + this.sort || of(null as SortOrder | null), + this.paginate || of(null as PagerEvent | null), + ] as const; + + this.data = combineLatest(operations).pipe( + map(([searchString, sort, pagination]) => { + let data: T[] = this.source; + + // Filter, sort and paginate the data + data = this.applyFilter(data, searchString); + data = this.orderByPredicate(data, sort); + data = this.applyPagination(data, pagination); + + return data; + } + ), + + // Complete the totalLength observable when the data observable completes + finalize(() => { + this.dataComplete.next(); + this.dataComplete.complete(); + }) + ); + } + + /** + * Sets the source array and recomputes the data. + * + * @param source + * The new source array. + */ + updateSource(source: T[]): void { + this.source = source; + this.computeData(); + } + + /** + * TODO: Document + * Applies the given filter to the given data. + * @param data + * @param searchString + */ + private applyFilter(data: T[], searchString: string | null): T[] { + if (searchString === null || searchString === '' || this.filterPattern === null) + return data; + + this.filterPattern.compile(searchString); + return this.filterService.filterByPredicate(data, this.filterPattern.predicate as any); + } + + /** + * TODO: Document + * @param data + * @param sortOrder + */ + private orderByPredicate(data: T[], sortOrder: SortOrder | null): T[] { + if (sortOrder === null) + return data; + + return this.sortService.orderByPredicate(data, sortOrder.predicate); + } + + /** + * TODO: Document + * @param data + * @param pagination + */ + private applyPagination(data: T[], pagination: PagerEvent | null): T[] { + if (pagination === null || pagination.pageSize === 0) + return data; + + return this.paginationService.paginate(data, pagination.pageIndex, pagination.pageSize); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSourceBuilder.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSourceBuilder.ts new file mode 100644 index 0000000000..cc4682e722 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/DataSourceBuilder.ts @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Observable } from 'rxjs'; +import { PagerEvent } from '../components/guac-pager/guac-pager.component'; +import { FilterService } from '../services/filter.service'; +import { PaginationService } from '../services/pagination.service'; +import { SortService } from '../services/sort.service'; + +import { DataSource } from './DataSource'; +import { SortOrder } from './SortOrder'; + +/** + * A builder that allows to create a DataSource instance. A data source can be + * configured with a filter, a sort order and pagination. + * + * @template T + * The type of the elements in the data source. + */ +export class DataSourceBuilder { + + private _source?: T[]; + private _searchString: Observable | null = null; + private _filterProperties: string[] | null = null; + private _sortOrder: Observable | null = null; + private _pagerEvent: Observable | null = null; + + /** + * Creates a new DataSourceBuilder. + */ + constructor(private sortService: SortService, + private filterService: FilterService, + private paginationService: PaginationService) { + } + + /** + * Set the source on which the data operations should be applied. + * + * @param source + * The source to be used. + * + * @returns + * The current builder instance for further chaining. + */ + source(source: T[]): DataSourceBuilder { + this._source = source; + return this; + } + + /** + * An Observable of TODO + * + * @param searchString + * The filter search string to use to restrict the displayed items. + * + * @param filterProperties + * An array of expressions to filter against for each object in the + * items array. These expressions must be Angular expressions + * which resolve to properties on the objects in the items array. + * + * @returns + * The current builder instance for further chaining. + */ + filter(searchString: Observable, filterProperties: string[]): DataSourceBuilder { + this._searchString = searchString; + this._filterProperties = filterProperties; + return this; + } + + /** + * An Observable of the sort order that should be applied to the source. Each + * time the sort order changes, the source will be sorted accordingly. + * + * @param sortOrder + * The sort order that should be applied. + * + * @returns + * The current builder instance for further chaining. + */ + sort(sortOrder: Observable): DataSourceBuilder { + this._sortOrder = sortOrder; + return this; + } + + /** + * An Observable of pagination events that should be used extract a page of the source. Each + * time a page event is emitted, the source will be updated accordingly. + * + * @param pagerEvent + * The sort order that should be applied. + * + * @returns + * The current builder instance for further chaining. + */ + paginate(pagerEvent: Observable): DataSourceBuilder { + this._pagerEvent = pagerEvent; + return this; + } + + /** + * Build the data source with the current configuration. + * + * @returns + * The data source with the current configuration. + */ + build(): DataSource { + + if (!this._source) + throw new Error('No source provided'); + + return new DataSource( + this.sortService, + this.filterService, + this.paginationService, + this._source, + this._searchString, + this._filterProperties, + this._sortOrder, + this._pagerEvent + ); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterPattern.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterPattern.ts new file mode 100644 index 0000000000..db65a42590 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterPattern.ts @@ -0,0 +1,247 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import AngularExpression from 'angular-expressions'; +import _ from 'lodash'; +import { FilterToken } from './FilterToken'; +import { IPv4Network } from './IPv4Network'; +import { IPv6Network } from './IPv6Network'; + +/** + * Handles compilation of filtering predicates as used by + * the {@link FilterService}. Predicates are compiled from a user- + * specified search string. + * TODO: Rewrite documentation + */ +export class FilterPattern { + + /** + * Array of getters corresponding to the Angular expressions provided + * to the constructor of this class. The functions returns are those + * produced by the $parse service. + */ + private readonly getters: Function[]; + + /** + * The current filtering predicate. + */ + predicate: Function; + + /** + * Creates a new FilterPattern. + * + * @param expressions + * The expressions whose values are to be filtered. + */ + constructor(expressions: string[]) { + this.getters = expressions.map(expression => AngularExpression.compile(expression)); + this.predicate = this.nullPredicate; + } + + /** + * Filter predicate which simply matches everything. This function + * always returns true. + * + * @returns + * true. + */ + private nullPredicate(): boolean { + return true; + } + + /** + * Determines whether the given object matches the given filter pattern + * token. + * + * @param object + * The object to match the token against. + * + * @param token + * The token from the tokenized filter pattern to match aginst the + * given object. + * + * @returns + * true if the object matches the token, false otherwise. + */ + private matchesToken(object: object, token: FilterToken): boolean { + + // Match depending on token type + switch (token.type) { + + // Simple string literal + case 'LITERAL': + return this.matchesString(object, token.value); + + // IPv4 network address / subnet + case 'IPV4_NETWORK': + return this.matchesIPv4(object, token.value); + + // IPv6 network address / subnet + case 'IPV6_NETWORK': + return this.matchesIPv6(object, token.value); + + // Unsupported token type + default: + return false; + + } + + } + + /** + * Determines whether the given object contains properties that match + * the given string, according to the provided getters. + * + * @param object + * The object to match against. + * + * @param str + * The string to match. + * + * @returns + * true if the object matches the given string, false otherwise. + */ + private matchesString(object: object, str: string): boolean { + + // For each defined getter + for (let i = 0; i < this.getters.length; i++) { + + // Retrieve value of current getter + let value: any; + if (_.isObjectLike(object)) + value = (this.getters)[i](object); + // If the given object is a primitive value use the value directly + else + value = object; + + // If the value matches the pattern, the whole object matches + if (String(value).toLowerCase().indexOf(str) !== -1) + return true; + + } + + // No matches found + return false; + + } + + /** + * Determines whether the given object contains properties that match + * the given IPv4 network, according to the provided getters. + * + * @param object + * The object to match against. + * + * @param network + * The IPv4 network to match. + * + * @returns + * true if the object matches the given network, false otherwise. + */ + private matchesIPv4(object: object, network: IPv4Network): boolean { + + // For each defined getter + for (let i = 0; i < this.getters.length; i++) { + + // Test each possible IPv4 address within the string against + // the given IPv4 network + const addresses = String((this.getters)[i](object)).split(/[^0-9.]+/); + for (let j = 0; j < addresses.length; j++) { + const value = IPv4Network.parse(addresses[j]); + if (value && network.contains(value)) + return true; + } + + } + + // No matches found + return false; + + } + + /** + * Determines whether the given object contains properties that match + * the given IPv6 network, according to the provided getters. + * + * @param object + * The object to match against. + * + * @param network + * The IPv6 network to match. + * + * @returns + * true if the object matches the given network, false otherwise. + */ + matchesIPv6(object: object, network: IPv6Network): boolean { + + // For each defined getter + for (let i = 0; i < this.getters.length; i++) { + + // Test each possible IPv6 address within the string against + // the given IPv6 network + const addresses = String((this.getters)[i](object)).split(/[^0-9A-Fa-f:]+/); + for (let j = 0; j < addresses.length; j++) { + const value = IPv6Network.parse(addresses[j]); + if (value && network.contains(value)) + return true; + } + + } + + // No matches found + return false; + + } + + /** + * Compiles the given pattern string, assigning the resulting filter + * predicate. The resulting predicate will accept only objects that + * match the given pattern. + * + * @param pattern + * The pattern to compile. + */ + compile(pattern: string): void { + + // If no pattern provided, everything matches + if (!pattern) { + this.predicate = this.nullPredicate; + return; + } + + // Tokenize pattern, converting to lower case for case-insensitive matching + const tokens = FilterToken.tokenize(pattern.toLowerCase()); + + // Return predicate which matches against the value of any getter in the getters array + this.predicate = (object: object) => { + + // False if any token does not match + for (let i = 0; i < tokens.length; i++) { + if (!this.matchesToken(object, tokens[i])) + return false; + } + + // True if all tokens matched + return true; + + }; + + } + +} diff --git a/guacamole/src/main/frontend/src/app/list/types/FilterToken.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterToken.ts similarity index 67% rename from guacamole/src/main/frontend/src/app/list/types/FilterToken.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterToken.ts index d512d38db2..902d16bac6 100644 --- a/guacamole/src/main/frontend/src/app/list/types/FilterToken.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/FilterToken.ts @@ -17,81 +17,74 @@ * under the License. */ +import { IPv4Network } from './IPv4Network'; +import { IPv6Network } from './IPv6Network'; + /** - * A service for defining the FilterToken class. + * An arbitrary token having an associated type and value. */ -angular.module('list').factory('FilterToken', ['$injector', - function defineFilterToken($injector) { +export class FilterToken { - // Required types - var IPv4Network = $injector.get('IPv4Network'); - var IPv6Network = $injector.get('IPv6Network'); + /** + * The input string that was consumed to produce this token. + */ + consumed: string; /** - * An arbitrary token having an associated type and value. + * The type of this token. Each legal type name is a property within + * FilterToken.Types. + */ + type: string; + + /** + * The value of this token. + */ + value: any; + + /** + * Creates a new FilterToken. * - * @constructor - * @param {String} consumed + * @param consumed * The input string consumed to produce this token. * - * @param {String} type + * @param type * The type of this token. Each legal type name is a property within * FilterToken.Types. * - * @param {Object} value + * @param value * The value of this token. The type of this value is determined by * the token type. */ - var FilterToken = function FilterToken(consumed, type, value) { + constructor(consumed: string, type: string, value: any) { - /** - * The input string that was consumed to produce this token. - * - * @type String - */ this.consumed = consumed; - - /** - * The type of this token. Each legal type name is a property within - * FilterToken.Types. - * - * @type String - */ this.type = type; - - /** - * The value of this token. - * - * @type Object - */ this.value = value; - }; + } /** * All legal token types, and corresponding functions which match them. * Each function returns the parsed token, or null if no such token was * found. - * - * @type Object. */ - FilterToken.Types = { + static Types: Record FilterToken | null> = { /** * An IPv4 address or subnet. The value of an IPV4_NETWORK token is an * IPv4Network. */ - IPV4_NETWORK: function parseIPv4(str) { + IPV4_NETWORK: function parseIPv4(str: string): FilterToken | null { - var pattern = /^\S+/; + const pattern = /^\S+/; // Read first word via regex - var matches = pattern.exec(str); + const matches = pattern.exec(str); if (!matches) return null; // Validate and parse as IPv4 address - var network = IPv4Network.parse(matches[0]); + const network = IPv4Network.parse(matches[0]); if (!network) return null; @@ -103,17 +96,17 @@ angular.module('list').factory('FilterToken', ['$injector', * An IPv6 address or subnet. The value of an IPV6_NETWORK token is an * IPv6Network. */ - IPV6_NETWORK: function parseIPv6(str) { + IPV6_NETWORK: function parseIPv6(str: string): FilterToken | null { - var pattern = /^\S+/; + const pattern = /^\S+/; // Read first word via regex - var matches = pattern.exec(str); + const matches = pattern.exec(str); if (!matches) return null; // Validate and parse as IPv6 address - var network = IPv6Network.parse(matches[0]); + const network = IPv6Network.parse(matches[0]); if (!network) return null; @@ -125,12 +118,12 @@ angular.module('list').factory('FilterToken', ['$injector', * A string literal, which may be quoted. The value of a LITERAL token * is a String. */ - LITERAL: function parseLiteral(str) { + LITERAL: function parseLiteral(str: string): FilterToken | null { - var pattern = /^"([^"]*)"|^\S+/; + const pattern = /^"([^"]*)"|^\S+/; // Validate against pattern - var matches = pattern.exec(str); + const matches = pattern.exec(str); if (!matches) return null; @@ -147,12 +140,12 @@ angular.module('list').factory('FilterToken', ['$injector', * Arbitrary contiguous whitespace. The value of a WHITESPACE token is * a String. */ - WHITESPACE: function parseWhitespace(str) { + WHITESPACE: function parseWhitespace(str: string): FilterToken | null { - var pattern = /^\s+/; + const pattern = /^\s+/; // Validate against pattern - var matches = pattern.exec(str); + const matches = pattern.exec(str); if (!matches) return null; @@ -167,33 +160,33 @@ angular.module('list').factory('FilterToken', ['$injector', * Tokenizes the given string, returning an array of tokens. Whitespace * tokens are dropped. * - * @param {String} str + * @param str * The string to tokenize. * - * @returns {FilterToken[]} + * @returns * All tokens identified within the given string, in order. */ - FilterToken.tokenize = function tokenize(str) { + static tokenize(str: string): FilterToken[] { - var tokens = []; + const tokens: FilterToken[] = []; /** * Returns the first token on the current string, removing the token * from that string. * - * @returns FilterToken + * @returns * The first token on the string, or null if no tokens match. */ - var popToken = function popToken() { + const popToken = (): FilterToken | null => { // Attempt to find a matching token - for (var type in FilterToken.Types) { + for (const type in FilterToken.Types) { // Get matching function for current type - var matcher = FilterToken.Types[type]; + const matcher = FilterToken.Types[type]; // If token matches, return the matching group - var token = matcher(str); + const token = matcher(str); if (token) { str = str.substring(token.consumed.length); return token; @@ -210,7 +203,7 @@ angular.module('list').factory('FilterToken', ['$injector', while (str) { // Remove first token - var token = popToken(); + const token = popToken(); if (!token) break; @@ -222,8 +215,5 @@ angular.module('list').factory('FilterToken', ['$injector', return tokens; - }; - - return FilterToken; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv4Network.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv4Network.ts new file mode 100644 index 0000000000..f93d4381c8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv4Network.ts @@ -0,0 +1,111 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Represents an IPv4 network as a pairing of base address and netmask, + * both of which are in binary form. To obtain an IPv4Network from + * standard CIDR or dot-decimal notation, use IPv4Network.parse(). + */ +export class IPv4Network { + + /** + * The binary address of this network. This will be a 32-bit quantity. + */ + address: number; + + /** + * The binary netmask of this network. This will be a 32-bit quantity. + */ + netmask: number; + + /** + * Creates a new IPv4Network. + * + * @param address + * The IPv4 address of the network in binary form. + * + * @param netmask + * The IPv4 netmask of the network in binary form. + */ + constructor(address: number, netmask: number) { + this.address = address; + this.netmask = netmask; + } + + /** + * Tests whether the given network is entirely within this network, + * taking into account the base addresses and netmasks of both. + * + * @param other + * The network to test. + * + * @returns + * true if the other network is entirely within this network, false + * otherwise. + */ + contains(other: IPv4Network): boolean { + return this.address === (other.address & other.netmask & this.netmask); + } + + /** + * Parses the given string as an IPv4 address or subnet, returning an + * IPv4Network object which describes that address or subnet. + * + * @param str + * The string to parse. + * + * @returns + * The parsed network, or null if the given string is not valid. + */ + static parse(str: string): IPv4Network | null { + + // Regex which matches the general form of IPv4 addresses + const pattern = /^([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})\.([0-9]{1,3})(?:\/([0-9]{1,2}))?$/; + + // Parse IPv4 address via regex + const match = pattern.exec(str); + if (!match) + return null; + + // Parse netmask, if given + let netmask = 0xFFFFFFFF; + if (match[5]) { + const bits = parseInt(match[5]); + if (bits > 0 && bits <= 32) + netmask = 0xFFFFFFFF << (32 - bits); + } + + // Read each octet onto address + let address = 0; + for (let i = 1; i <= 4; i++) { + + // Validate octet range + const octet = parseInt(match[i]); + if (octet > 255) + return null; + + // Shift on octet + address = (address << 8) | octet; + + } + + return new IPv4Network(address, netmask); + + } +} diff --git a/guacamole/src/main/frontend/src/app/list/types/IPv6Network.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv6Network.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/list/types/IPv6Network.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv6Network.ts index 7fa85cbf4a..a0a1c9861a 100644 --- a/guacamole/src/main/frontend/src/app/list/types/IPv6Network.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/IPv6Network.ts @@ -18,77 +18,65 @@ */ /** - * A service for defining the IPv6Network class. + * Represents an IPv6 network as a pairing of base address and netmask, + * both of which are in binary form. To obtain an IPv6Network from + * standard CIDR notation, use IPv6Network.parse(). */ -angular.module('list').factory('IPv6Network', [ - function defineIPv6Network() { +export class IPv6Network { /** - * Represents an IPv6 network as a pairing of base address and netmask, - * both of which are in binary form. To obtain an IPv6Network from - * standard CIDR notation, use IPv6Network.parse(). + * The 128-bit binary address of this network as an array of eight + * 16-bit numbers. + */ + addressGroups: number[]; + + /** + * The 128-bit binary netmask of this network as an array of eight + * 16-bit numbers. + */ + netmaskGroups: number[]; + + /** + * Creates a new IPv6Network. * - * @constructor - * @param {Number[]} addressGroups - * Array of eight IPv6 address groups in binary form, each group being + * @param addressGroups + * Array of eight IPv6 address groups in binary form, each group being * 16-bit number. * - * @param {Number[]} netmaskGroups - * Array of eight IPv6 netmask groups in binary form, each group being + * @param netmaskGroups + * Array of eight IPv6 netmask groups in binary form, each group being * 16-bit number. */ - var IPv6Network = function IPv6Network(addressGroups, netmaskGroups) { - - /** - * Reference to this IPv6Network. - * - * @type IPv6Network - */ - var network = this; - - /** - * The 128-bit binary address of this network as an array of eight - * 16-bit numbers. - * - * @type Number[] - */ + constructor(addressGroups: number[], netmaskGroups: number[]) { this.addressGroups = addressGroups; - - /** - * The 128-bit binary netmask of this network as an array of eight - * 16-bit numbers. - * - * @type Number - */ this.netmaskGroups = netmaskGroups; + } - /** - * Tests whether the given network is entirely within this network, - * taking into account the base addresses and netmasks of both. - * - * @param {IPv6Network} other - * The network to test. - * - * @returns {Boolean} - * true if the other network is entirely within this network, false - * otherwise. - */ - this.contains = function contains(other) { - - // Test that each masked 16-bit quantity matches the address - for (var i=0; i < 8; i++) { - if (network.addressGroups[i] !== (other.addressGroups[i] - & other.netmaskGroups[i] - & network.netmaskGroups[i])) - return false; - } - - // All 16-bit numbers match - return true; + /** + * Tests whether the given network is entirely within this network, + * taking into account the base addresses and netmasks of both. + * + * @param other + * The network to test. + * + * @returns + * true if the other network is entirely within this network, false + * otherwise. + */ + contains(other: IPv6Network): boolean { + + // Test that each masked 16-bit quantity matches the address + for (let i = 0; i < 8; i++) { + if (this.addressGroups[i] !== (other.addressGroups[i] + & other.netmaskGroups[i] + & this.netmaskGroups[i])) + return false; + } - }; + // All 16-bit numbers match + return true; - }; + } /** * Generates a netmask having the given number of ones on the left side. @@ -96,16 +84,16 @@ angular.module('list').factory('IPv6Network', [ * will be an array of eight numbers, where each number corresponds to a * 16-bit group of an IPv6 netmask. * - * @param {Number} bits + * @param bits * The number of ones to include on the left side of the netmask. All * other bits will be zeroes. * - * @returns {Number[]} + * @returns * The generated netmask, having the given number of ones. */ - var generateNetmask = function generateNetmask(bits) { + private static generateNetmask(bits: number): number[] { - var netmask = []; + const netmask: number[] = []; // Only generate up to 128 bits bits = Math.min(128, bits); @@ -126,85 +114,85 @@ angular.module('list').factory('IPv6Network', [ return netmask; - }; + } /** * Splits the given IPv6 address or partial address into its corresponding * 16-bit groups. * - * @param {String} str + * @param str * The IPv6 address or partial address to split. - * - * @returns Number[] + * + * @returns * The numeric values of all 16-bit groups within the given IPv6 * address. */ - var splitAddress = function splitAddress(str) { + private static splitAddress(str: string): number[] { - var address = []; + const address: number[] = []; // Split address into groups - var groups = str.split(':'); + const groups = str.split(':'); // Parse the numeric value of each group - angular.forEach(groups, function addGroup(group) { - var value = parseInt(group || '0', 16); + groups.forEach(group => { + const value = parseInt(group || '0', 16); address.push(value); }); return address; - }; + } /** * Parses the given string as an IPv6 address or subnet, returning an * IPv6Network object which describes that address or subnet. * - * @param {String} str + * @param str * The string to parse. * - * @returns {IPv6Network} + * @returns * The parsed network, or null if the given string is not valid. */ - IPv6Network.parse = function parse(str) { + static parse(str: string): IPv6Network | null { // Regex which matches the general form of IPv6 addresses - var pattern = /^([0-9a-f]{0,4}(?::[0-9a-f]{0,4}){0,7})(?:\/([0-9]{1,3}))?$/; + const pattern = /^([0-9a-f]{0,4}(?::[0-9a-f]{0,4}){0,7})(?:\/([0-9]{1,3}))?$/; // Parse rudimentary IPv6 address via regex - var match = pattern.exec(str); + const match = pattern.exec(str); if (!match) return null; // Extract address and netmask from parse results - var unparsedAddress = match[1]; - var unparsedNetmask = match[2]; + const unparsedAddress = match[1]; + const unparsedNetmask = match[2]; // Parse netmask - var netmask; + let netmask; if (unparsedNetmask) - netmask = generateNetmask(parseInt(unparsedNetmask)); + netmask = IPv6Network.generateNetmask(parseInt(unparsedNetmask)); else - netmask = generateNetmask(128); + netmask = IPv6Network.generateNetmask(128); - var address; + let address; // Separate based on the double-colon, if present - var doubleColon = unparsedAddress.indexOf('::'); + const doubleColon = unparsedAddress.indexOf('::'); // If no double colon, just split into groups if (doubleColon === -1) - address = splitAddress(unparsedAddress); + address = IPv6Network.splitAddress(unparsedAddress); // Otherwise, split either side of the double colon and pad with zeroes else { // Parse either side of the double colon - var leftAddress = splitAddress(unparsedAddress.substring(0, doubleColon)); - var rightAddress = splitAddress(unparsedAddress.substring(doubleColon + 2)); + const leftAddress = IPv6Network.splitAddress(unparsedAddress.substring(0, doubleColon)); + const rightAddress = IPv6Network.splitAddress(unparsedAddress.substring(doubleColon + 2)); // Pad with zeroes up to address length - var remaining = 8 - leftAddress.length - rightAddress.length; + let remaining = 8 - leftAddress.length - rightAddress.length; while (remaining > 0) { leftAddress.push(0); remaining--; @@ -213,15 +201,12 @@ angular.module('list').factory('IPv6Network', [ address = leftAddress.concat(rightAddress); } - + // Validate length of address if (address.length !== 8) return null; return new IPv6Network(address, netmask); - }; - - return IPv6Network; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/SortOrder.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/SortOrder.ts new file mode 100644 index 0000000000..9a8102b985 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/list/types/SortOrder.ts @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Maintains a sorting predicate as required by the {@link SortService}. + * The order of properties sorted by the predicate can be altered while + * otherwise maintaining the sort order. + * TODO: Rewrite documentation + */ +export class SortOrder { + + /** + * The current sorting predicate. + */ + predicate: string[]; + + /** + * The name of the highest-precedence sorting property. + */ + primary: string; + + /** + * Whether the highest-precedence sorting property is sorted in + * descending order. + */ + descending: boolean; + + /** + * Creates a new SortOrder. + + * @param predicate + * The properties to sort by, in order of precedence. + */ + constructor(predicate: string[]) { + + this.predicate = predicate; + this.primary = predicate[0]; + this.descending = false; + + // Handle initially-descending primary properties + if (this.primary.charAt(0) === '-') { + this.primary = this.primary.substring(1); + this.descending = true; + } + } + + /** + * Reorders the currently-defined predicate such that the named + * property takes precedence over all others. The property will be + * sorted in ascending order unless otherwise specified. + * + * @param name + * The name of the property to reorder by. + * + * @param descending + * Whether the property should be sorted in descending order. By + * default, all properties are sorted in ascending order. + */ + reorder(name: string, descending = false): void { + + // Build ascending and descending predicate components + const ascendingName = name; + const descendingName = '-' + name; + + // Remove requested property from current predicate + this.predicate = this.predicate.filter(function notRequestedProperty(current) { + return current !== ascendingName + && current !== descendingName; + }); + + // Add property to beginning of predicate + if (descending) + this.predicate.unshift(descendingName); + else + this.predicate.unshift(ascendingName); + + // Update sorted state + this.primary = name; + this.descending = descending; + + } + + /** + * Returns whether the sort order is primarily determined by the given + * property. + * + * @param property + * The name of the property to check. + * + * @returns + * true if the sort order is primarily determined by the given + * property, false otherwise. + */ + isSortedBy(property: string): boolean { + return this.primary === property; + } + + /** + * Sets the primary sorting property to the given property, if not already + * set. If already set, the ascending/descending sort order is toggled. + * + * @param property + * The name of the property to assign as the primary sorting property. + */ + togglePrimary(property: string): void { + + // Sort in ascending order by new property, if different + if (!this.isSortedBy(property)) + this.reorder(property, false); + + // Otherwise, toggle sort order + else + this.reorder(property, !this.descending); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/locale.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/locale.module.ts new file mode 100644 index 0000000000..89b2f9aa4a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/locale.module.ts @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { NgModule, inject, provideAppInitializer } from '@angular/core'; +import { + TRANSLOCO_CONFIG, + TRANSLOCO_LOADER, + TRANSLOCO_MISSING_HANDLER, + TranslocoConfig, + TranslocoMissingHandler, + TranslocoModule +} from '@ngneat/transloco'; +import { TranslocoMessageFormatModule } from '@ngneat/transloco-messageformat'; +import { HashMap } from '@ngneat/transloco/lib/types'; +import { Observable } from 'rxjs'; +import { TranslationLoaderService } from './service/translation-loader.service'; +import { TranslationService } from './service/translation.service'; + +/** + * Initializes the translation service. + * + * @returns + * An observable which completes when the translation service has been initialized. + */ +const initializeTranslationService = (translationService: TranslationService): Observable => { + return translationService.initialize(); +}; + +/** + * Provider factory which returns the TranslocoConfig. + */ +const translocoConfigFactory = (translationService: TranslationService): TranslocoConfig => { + return translationService.getTranslocoConfig(); +}; + +/** + * Property name to use in a parameter object of translate() functions to specify a default value + * that should be returned if a translation key is missing. + */ +export const VALUE_IF_MISSING_PROPERTY = 'valueIfMissing'; + +/** + * Returns a HashMap which can be passed to translate() functions of Transloco as additional + * parameter. Allows the caller to specify a default value to be returned if the requested + * translation is not available. + * + * @param value + * The value to return if the requested translation is not available. + */ +export const translationValueIfMissing = (value: any): HashMap => { + return { [VALUE_IF_MISSING_PROPERTY]: value }; +}; + +/** + * Custom missing handler. + */ +export class GuacMissingHandler implements TranslocoMissingHandler { + + /** + * Simply return the key itself if not even the fallback language contains a + * translation for the given key. + * + * This behavior can be modified by providing a parameter object with a + * property named `valueIfMissing`. If this property is not undefined, + * the value of this property will be returned instead. + * + * @see VALUE_IF_MISSING_PROPERTY + * @see translationValueIfMissing + */ + handle(key: string, config: TranslocoConfig, params?: HashMap) { + + if (params && params[VALUE_IF_MISSING_PROPERTY] !== undefined) + return params[VALUE_IF_MISSING_PROPERTY]; + + return key; + } +} + +/** + * Module for handling common localization-related tasks. + */ +@NgModule({ + exports : [TranslocoModule], + providers: [ + provideAppInitializer(() => { + const initializerFn = ((translationService: TranslationService) => + () => initializeTranslationService(translationService))(inject(TranslationService)); + return initializerFn(); + }), + { + provide : TRANSLOCO_CONFIG, + useFactory: translocoConfigFactory, + deps : [TranslationService], + }, + { + provide : TRANSLOCO_LOADER, + useClass: TranslationLoaderService + }, + { + provide : TRANSLOCO_MISSING_HANDLER, + useClass: GuacMissingHandler + } + ], + imports : [ + TranslocoMessageFormatModule.forRoot() + ] +}) +export class LocaleModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation-loader.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation-loader.service.ts new file mode 100644 index 0000000000..f88d6b04f0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation-loader.service.ts @@ -0,0 +1,172 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient, HttpContext } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Translation, TranslocoLoader } from '@ngneat/transloco'; +import { TranslocoLoaderData } from '@ngneat/transloco/lib/transloco.loader'; +import { LanguageService } from '../../rest/service/language.service'; +import { SKIP_ALL_INTERCEPTORS } from '../../util/interceptor.service'; + +/** + * Service for loading translation definition files. + */ +@Injectable({ + providedIn: 'root' +}) +export class TranslationLoaderService implements TranslocoLoader { + + /** + * Inject required services. + */ + constructor(private languageService: LanguageService, private http: HttpClient) { + } + + /** + * Custom loader function for Transloco which loads the desired + * language file dynamically via HTTP. If the language file cannot be + * found, an empty translation object is returned. + * + * @param languageKey + * The requested language key. + * + * @param data + * Optional data to be passed to the loader. + * + * @returns + * A promise which resolves to the requested translation object. + */ + getTranslation(languageKey: string, data?: TranslocoLoaderData): Promise { + + // Return promise which is resolved only after the translation file is loaded + return new Promise((resolve, reject) => { + + // Satisfy the translation request using possible variations of the given key + this.satisfyTranslation(resolve, reject, languageKey, this.getKeyVariations(languageKey)); + + }); + + } + + /** + * Satisfies a translation request for the given key by searching for the + * translation files for each key in the given array, in order. The request + * fails only if none of the files can be found. + * + * @param resolve + * The function to call if at least one translation file can be + * successfully loaded. + * + * @param reject + * The function to call if none of the translation files can be + * successfully loaded. + * + * @param requestedKey + * The originally-requested language key. + * + * @param remainingKeys + * The keys of the languages to attempt to load, in order, where the + * first key in this array is the language to try within this function + * call. The first key in the array is not necessarily the originally- + * requested language key. + */ + satisfyTranslation(resolve: (value: Translation) => void, reject: () => void, requestedKey: string, remainingKeys: string[]): void { + + // Get current language key + const currentKey = remainingKeys.shift(); + + // If no languages to try, "succeed" with an empty translation (force fallback) + if (!currentKey) { + resolve({}); + return; + } + + /** + * Continues trying possible translation files until no possibilities + * exist. + * + * @private + */ + const tryNextTranslation = () => { + this.satisfyTranslation(resolve, reject, requestedKey, remainingKeys); + }; + + // Retrieve list of supported languages + this.languageService.getLanguages() + // Attempt to retrieve translation if language is supported + .subscribe({ + next: (languages) => { + + // Skip retrieval if language is not supported + if (!(currentKey in languages)) { + tryNextTranslation(); + return; + } + + // Attempt to retrieve language + // TODO: cache: cacheService.languages, + const httpContext: HttpContext = new HttpContext(); + httpContext.set(SKIP_ALL_INTERCEPTORS, true); + this.http.get( + 'translations/' + encodeURIComponent(currentKey) + '.json', + { context: httpContext } + ).subscribe({ + next: (translationData) => { + resolve(translationData); + }, + // Retry with remaining languages if translation file could not be + // retrieved + error: tryNextTranslation + }); + + }, + + // Retry with remaining languages if translation does not exist + error: tryNextTranslation + }); + + } + + /** + * Given a valid language key, returns all possible legal variations of + * that key. Currently, this will be the given key and the given key + * without the country code. If the key has no country code, only the + * given key will be included in the returned array. + * + * @param key + * The language key to generate variations of. + * + * @returns + * All possible variations of the given language key. + */ + getKeyVariations(key: string): string[] { + + const underscore = key.indexOf('_'); + + // If no underscore, only one possibility + if (underscore === -1) + return [key]; + + // Otherwise, include the lack of country code as an option + return [key, key.substring(0, underscore)]; + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation.service.ts new file mode 100644 index 0000000000..ba7ba0d4cb --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/locale/service/translation.service.ts @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable, isDevMode } from '@angular/core'; +import { translocoConfig, TranslocoConfig } from '@ngneat/transloco'; +import { map, Observable, tap } from 'rxjs'; +import { LanguageService } from '../../rest/service/language.service'; +import { PreferenceService } from '../../settings/services/preference.service'; + +/** + * The default language to use if no preference is specified or other languages are not available. + */ +export const DEFAULT_LANGUAGE = 'en'; + +/** + * Given an arbitrary identifier, returns the corresponding translation + * table identifier. Translation table identifiers are uppercase strings, + * word components separated by single underscores. For example, the + * string "Swap red/blue" would become "SWAP_RED_BLUE". + * + * @param identifier + * The identifier to transform into a translation table identifier. + * + * @returns + * The translation table identifier. + */ +export const canonicalize = (identifier: string): string => { + return identifier.replace(/[^a-zA-Z0-9]+/g, '_').toUpperCase(); +}; + +/** + * Service for providing a configuration object for Transloco. + */ +@Injectable({ + providedIn: 'root' +}) +export class TranslationService { + + /** + * The preferred language to use when translating. + * The value is provided by the PreferenceService and is set during initialization. + */ + preferredLanguage: string = DEFAULT_LANGUAGE; + + /** + * The list of languages available for translation. + * The value is provided by the LanguageService and is set during initialization. + */ + availableLanguages: string[] = []; + + /** + * Inject required services. + */ + constructor(private languageService: LanguageService, private preferenceService: PreferenceService) { + } + + /** + * Initializes the TranslationService by retrieving the list of available languages from the REST API and the user's + * preferred language from the PreferenceService. + * + * Has to be called once inside an APP_INITIALIZER. + * + * @returns + * An observable which completes when the initialization is done. + */ + initialize(): Observable { + this.preferredLanguage = this.preferenceService.preferences.language; + + return this.languageService.getLanguages() + .pipe( + // extract language keys from the response + map(langauges => Object.keys(langauges)), + // store available languages + tap(languageKeys => this.availableLanguages = languageKeys), + // ignore the result + map(() => void (0)) + ); + } + + /** + * Returns the configuration object for Transloco. + * + * @returns + * The configuration object for Transloco. + */ + getTranslocoConfig(): TranslocoConfig { + + return translocoConfig({ + + // Provide the list of all available languages retrieved from the REST API + availableLangs: this.availableLanguages, + + // Set the default language according to the user's preferences + defaultLang: this.preferredLanguage, + + // If the file of the preferred language could not be loaded, fall back to English + fallbackLang: DEFAULT_LANGUAGE, + + missingHandler: { + // If a translation key is missing for the current language, fall back to English + useFallbackTranslation: true, + + // Do not use the missing handler if a translation value is an empty string + allowEmpty: true + }, + + // By default transloco uses {{...}} to reference other translation keys. We + // change it to @:...:@ to prevent transcloco from removing the parts of ICU messages. + interpolation: ['@:', ':@'], + + // Allow changing language at runtime + reRenderOnLangChange: true, + prodMode : !isDevMode() + }); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.html new file mode 100644 index 0000000000..d07c138fad --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.html @@ -0,0 +1,70 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.ts new file mode 100644 index 0000000000..bae5c850f0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/components/login/login.component.ts @@ -0,0 +1,271 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormControl, FormGroup } from '@angular/forms'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { catchError, filter } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { GuacFrontendEventArguments } from '../../../events/types/GuacFrontendEventArguments'; +import { RequestService } from '../../../rest/service/request.service'; +import { Error } from '../../../rest/types/Error'; +import { Field } from '../../../rest/types/Field'; +import { TranslatableMessage } from '../../../rest/types/TranslatableMessage'; + +@Component({ + selector: 'guac-login', + templateUrl: './login.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class LoginComponent implements OnInit, OnChanges { + /** + * An optional instructional message to display within the login + * dialog. + */ + @Input() helpText?: TranslatableMessage; + + /** + * The login form or set of fields. This will be displayed to the user + * to capture their credentials. + */ + @Input({ required: true }) form: Field[] | null = null; + + /** + * A form group of all field name/value pairs that have already been provided. + * If not undefined, the user will be prompted to continue their login + * attempt using only the fields which remain. + */ + @Input() values?: FormGroup = new FormGroup({}); + + /** + * The initial value for all login fields. Note that this value must + * not be null. If null, empty fields may not be submitted back to the + * server at all, causing the request to misrepresent true login state. + * + * For example, if a user receives an insufficient credentials error + * due to their password expiring, failing to provide that new password + * should result in the user submitting their username, original + * password, and empty new password. If only the username and original + * password are sent, the invalid password reset request will be + * indistinguishable from a normal login attempt. + */ + readonly DEFAULT_FIELD_VALUE: string = ''; + + /** + * A description of the error that occurred during login, if any. + */ + loginError: TranslatableMessage | null = null; + + /** + * All form values entered by the user, as parameter form group. + */ + enteredValues: FormGroup = new FormGroup({}); + + /** + * All form fields which have not yet been filled by the user. + */ + remainingFields: Field[] = []; + + /** + * Whether an authentication attempt has been submitted. This will be + * set to true once credentials have been submitted and will only be + * reset to false once the attempt has been fully processed, including + * rerouting the user to the requested page if the attempt succeeded. + */ + submitted = false; + + /** + * The field that is most relevant to the user. + */ + relevantField: Field | null = null; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private requestService: RequestService, + private guacEventService: GuacEventService, + private router: Router, + private route: ActivatedRoute, + private destroyRef: DestroyRef) { + } + + /** + * Returns whether a previous login attempt is continuing. + * + * @return + * true if a previous login attempt is continuing, false otherwise. + */ + isContinuation(): boolean { + + // The login is continuing if any parameter values are provided + for (const name in this.values?.controls) + return true; + + return false; + + } + + ngOnChanges(changes: SimpleChanges): void { + + // Ensure provided values are included within entered values, even if + // they have no corresponding input fields + if (changes['values']) { + const values = changes['values'].currentValue as FormGroup; + this.enteredValues = new FormGroup({ ...this.enteredValues.controls, ...values.controls }); + // angular.extend($scope.enteredValues, values || {}); + } + + // Update field information when form is changed + if (changes['form']) { + const fields = changes['form'].currentValue as Field[]; + + // If no fields are provided, then no fields remain + if (!fields) { + this.remainingFields = []; + return; + } + + // Filter provided fields against provided values + this.remainingFields = fields.filter((field) => { + return this.values && !(field.name in this.values); + }); + + // Set default values for all unset fields + this.remainingFields.forEach((field) => { + if (!this.enteredValues.get(field.name)) + this.enteredValues.addControl(field.name, new FormControl(this.DEFAULT_FIELD_VALUE)); + }); + + this.relevantField = this.getRelevantField(); + } + + + } + + /** + * Submits the currently-specified fields to the authentication service, + * as well as any URL parameters set for the current page, preferring + * the values from the fields, and redirecting to the main view if + * successful. + */ + login(): void { + + // Any values from URL paramters + const urlValues = this.route.snapshot.queryParams; + + // Values from the fields + const fieldValues = this.enteredValues.value; + + // All the values to be submitted in the auth attempt, preferring + // any values from fields over those in the URL + const authParams = { ...urlValues, ...fieldValues }; + + this.authenticationService.authenticate(authParams) + .pipe( + takeUntilDestroyed(this.destroyRef), + catchError(this.requestService.IGNORE) + ) + .subscribe(); + } + + /** + * Returns the field most relevant to the user given the current state + * of the login process. This will normally be the first empty field. + * + * @return + * The field most relevant, null if there is no single most relevant + * field. + */ + private getRelevantField(): Field | null { + + for (let i = 0; i < this.remainingFields.length; i++) { + const field = this.remainingFields[i]; + if (!this.enteredValues.get(field.name)) + return field; + } + + return null; + + } + + ngOnInit(): void { + // Update UI to reflect in-progress auth status (clear any previous + // errors, flag as pending) + this.guacEventService.on('guacLoginPending') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.submitted = true; + this.loginError = null; + }); + + // Retry route upon success (entered values will be cleared only + // after route change has succeeded as this can take time) + this.guacEventService.on('guacLogin') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(() => { + this.router.navigate([this.router.url], { onSameUrlNavigation: 'reload' }); + }); + + // Reset upon failure + this.guacEventService.on('guacLoginFailed') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ error }) => { + + // Initial submission is complete and has failed + this.submitted = false; + + // Clear out passwords if the credentials were rejected for any reason + if (error.type !== Error.Type.INSUFFICIENT_CREDENTIALS) { + + // Flag generic error for invalid login + if (error.type === Error.Type.INVALID_CREDENTIALS) + this.loginError = { + key: 'LOGIN.ERROR_INVALID_LOGIN' + }; + + // Display error if anything else goes wrong + else + this.loginError = error.translatableMessage || null; + + // Reset all remaining fields to default values, but + // preserve any usernames + this.remainingFields.forEach((field) => { + if (field.type !== Field.Type.USERNAME && this.enteredValues.get(field.name) !== null) + this.enteredValues.get(field.name)!.setValue(this.DEFAULT_FIELD_VALUE); + }); + } + + }); + + // Reset state after authentication and routing have succeeded + this.router.events.pipe( + takeUntilDestroyed(this.destroyRef), + filter(event => event instanceof NavigationEnd) + ).subscribe(() => { + this.enteredValues = new FormGroup({}); + this.submitted = false; + }); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/login/loginModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/login.module.ts similarity index 60% rename from guacamole/src/main/frontend/src/app/login/loginModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/login.module.ts index 891387ef4e..32f57cb9fa 100644 --- a/guacamole/src/main/frontend/src/app/login/loginModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/login.module.ts @@ -17,11 +17,31 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslocoModule } from '@ngneat/transloco'; +import { FormModule } from '../form/form.module'; +import { LoginComponent } from './components/login/login.component'; + /** * The module for the login functionality. */ -angular.module('login', [ - 'element', - 'form', - 'navigation' -]); +@NgModule({ + declarations: [ + LoginComponent + ], + imports : [ + CommonModule, + FormModule, + FormsModule, + TranslocoModule + ], + exports : [ + LoginComponent + ] +}) +export class LoginModule { + + +} diff --git a/guacamole/src/main/frontend/src/app/login/styles/animation.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/animation.css similarity index 100% rename from guacamole/src/main/frontend/src/app/login/styles/animation.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/animation.css diff --git a/guacamole/src/main/frontend/src/app/login/styles/dialog.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/dialog.css similarity index 98% rename from guacamole/src/main/frontend/src/app/login/styles/dialog.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/dialog.css index 81ab8a98f6..e5847abd6e 100644 --- a/guacamole/src/main/frontend/src/app/login/styles/dialog.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/dialog.css @@ -103,7 +103,7 @@ -moz-background-size: 3em 3em; -webkit-background-size: 3em 3em; -khtml-background-size: 3em 3em; - background-image: url("images/guac-tricolor.svg"); + background-image: url('images/guac-tricolor.svg'); } .login-ui.continuation .login-dialog { diff --git a/guacamole/src/main/frontend/src/app/login/styles/input.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/input.css similarity index 100% rename from guacamole/src/main/frontend/src/app/login/styles/input.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/input.css diff --git a/guacamole/src/main/frontend/src/app/login/styles/login.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/login.css similarity index 91% rename from guacamole/src/main/frontend/src/app/login/styles/login.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/login.css index 4cb07f18aa..d657f94957 100644 --- a/guacamole/src/main/frontend/src/app/login/styles/login.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/login/styles/login.css @@ -64,16 +64,16 @@ div.login-ui { .login-ui .login-fields .labeled-field .field-header { - display: block; - position: absolute; - left: 0; - right: 0; - overflow: hidden; + display: block; + position: absolute; + left: 0; + right: 0; + overflow: hidden; - z-index: -1; - margin: 0.5em; - font-size: 0.9em; - opacity: 0.5; + z-index: -1; + margin: 0.5em; + font-size: 0.9em; + opacity: 0.5; } diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.html new file mode 100644 index 0000000000..1e96aea045 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.html @@ -0,0 +1,33 @@ + + +
      + + +
      + + + + + + {{ item.name }} + +
      diff --git a/guacamole/src/main/frontend/src/app/home/types/ActiveConnection.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.ts similarity index 53% rename from guacamole/src/main/frontend/src/app/home/types/ActiveConnection.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.ts index 3de16c9d54..959785ca94 100644 --- a/guacamole/src/main/frontend/src/app/home/types/ActiveConnection.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-group-permission/connection-group-permission.component.ts @@ -17,36 +17,30 @@ * under the License. */ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + /** - * Provides the ActiveConnection class used by the guacRecentConnections - * directive. + * A component which displays a single connection group and allows + * manipulation of the connection group permissions. */ -angular.module('home').factory('ActiveConnection', [function defineActiveConnection() { +@Component({ + selector: 'guac-connection-group-permission', + templateUrl: './connection-group-permission.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionGroupPermissionComponent { /** - * A recently-user connection, visible to the current user, with an - * associated history entry. - * - * @constructor + * TODO */ - var ActiveConnection = function ActiveConnection(name, client) { - - /** - * The human-readable name of this connection. - * - * @type String - */ - this.name = name; - - /** - * The client associated with this active connection. - * - * @type ManagedClient - */ - this.client = client; + @Input({ required: true }) context!: ConnectionListContext; - }; - - return ActiveConnection; + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.html new file mode 100644 index 0000000000..5f4daf2700 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.html @@ -0,0 +1,52 @@ + + +
      +
      +

      + +
      + +
      + +
      +
      + + + + + + + + + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.ts new file mode 100644 index 0000000000..eabfe20922 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission-editor/connection-permission-editor.component.ts @@ -0,0 +1,562 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import { + GuacGroupListFilterComponent +} from '../../../group-list/components/guac-group-list-filter/guac-group-list-filter.component'; +import { ConnectionGroupDataSource } from '../../../group-list/types/ConnectionGroupDataSource'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { FilterService } from '../../../list/services/filter.service'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { PermissionFlagSet } from '../../../rest/types/PermissionFlagSet'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + +/** + * A component for manipulating the connection permissions granted within a + * given {@link PermissionFlagSet}, tracking the specific permissions added or + * removed within a separate pair of {@link PermissionSet} objects. + */ +@Component({ + selector: 'connection-permission-editor', + templateUrl: './connection-permission-editor.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionPermissionEditorComponent implements OnInit, OnChanges { + + /** + * The unique identifier of the data source associated with the + * permissions being manipulated. + */ + @Input({ required: true }) dataSource!: string; + + /** + * The current state of the permissions being manipulated. This + * {@link PermissionFlagSet} will be modified as changes are made + * through this permission editor. + */ + @Input({ required: true }) permissionFlags: PermissionFlagSet | null = null; + + /** + * The set of permissions that have been added, relative to the + * initial state of the permissions being manipulated. + */ + @Input({ required: true }) permissionsAdded!: PermissionSet; + + /** + * The set of permissions that have been removed, relative to the + * initial state of the permissions being manipulated. + */ + @Input({ required: true }) permissionsRemoved!: PermissionSet; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacGroupListFilterComponent, { static: true }) filter!: GuacGroupListFilterComponent; + + /** + * Filtered view of the root connection groups which satisfy the current + * search string. + */ + rootGroupsDataSource: ConnectionGroupDataSource | null = null; + + /** + * A map of data source identifiers to all root connection groups + * within those data sources, regardless of the permissions granted for + * the items within those groups. As only one data source is applicable + * to any particular permission set being edited/created, this will only + * contain a single key. If the data necessary to produce this map has + * not yet been loaded, this will be null. + */ + private allRootGroups: Record | null = null; + + /** + * A map of data source identifiers to the root connection groups within + * those data sources, excluding all items which are not explicitly + * readable according to this.permissionFlags. As only one data + * source is applicable to any particular permission set being + * edited/created, this will only contain a single key. If the data + * necessary to produce this map has not yet been loaded, this will be + * null. + */ + private readableRootGroups: Record | null = null; + + /** + * A map of data source identifiers to the root connection groups within + * those data sources for which we have ADMINISTER permission. If the data + * necessary to produce this map has not yet been loaded, this will be + * null. + */ + private administeredRootGroups: Record | null = null; + + /** + * The name of the tab within the connection permission editor which + * displays currently selected (readable) connections only. + * + * @constant + */ + private readonly CURRENT_CONNECTIONS: string = 'CURRENT_CONNECTIONS'; + + /** + * The name of the tab within the connection permission editor which + * displays all connections, regardless of whether they are readable. + * + * @constant + */ + private readonly ALL_CONNECTIONS: string = 'ALL_CONNECTIONS'; + + /** + * The names of all tabs which should be available within the + * connection permission editor, in display order. + */ + tabs: string[] = [ + this.CURRENT_CONNECTIONS, + this.ALL_CONNECTIONS + ]; + + /** + * The name of the currently selected tab. + */ + currentTab: string = this.ALL_CONNECTIONS; + + /** + * Array of all connection properties that are filterable. + */ + filteredConnectionProperties: string[] = [ + 'name', + 'protocol' + ]; + + /** + * Array of all connection group properties that are filterable. + */ + filteredConnectionGroupProperties: string[] = [ + 'name' + ]; + + /** + * Inject required services. + */ + constructor( + private connectionGroupService: ConnectionGroupService, + private dataSourceService: DataSourceService, + private requestService: RequestService, + private filterService: FilterService + ) { + } + + ngOnInit(): void { + + // Create a new data source for the root connection groups + this.rootGroupsDataSource = new ConnectionGroupDataSource(this.filterService, + {}, + this.filter.searchStringChange, + this.filteredConnectionProperties, + this.filteredConnectionGroupProperties); + + // Retrieve all connections for which we have ADMINISTER permission + this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string, permissionTypes: string[]) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes), + [this.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.ADMINISTER] + ) + .then((rootGroups) => { + + this.administeredRootGroups = rootGroups; + this.updateDefaultExpandedStates(); + this.rootGroupsDataSource?.updateSource(this.getRootGroups()); + + }, this.requestService.DIE); + + } + + ngOnChanges(changes: SimpleChanges): void { + + // Update default expanded state and the all / readable-only views + // when associated permissions change + if (changes['permissionFlags']) { + this.updateDefaultExpandedStates(); + } + + } + + /** + * Update default expanded state and the all / readable-only views. + */ + private updateDefaultExpandedStates(): void { + + if (!this.permissionFlags) + return; + + this.allRootGroups = {}; + this.readableRootGroups = {}; + + for (let dataSource in this.administeredRootGroups) { + const rootGroup = this.administeredRootGroups[dataSource]; + + // Convert all received ConnectionGroup objects into GroupListItems + const item = GroupListItem.fromConnectionGroup(dataSource, rootGroup); + this.allRootGroups[dataSource] = item; + + // Automatically expand all objects with any descendants for + // which the permission set contains READ permission + this.expandReadable(item, this.permissionFlags); + + // Create a duplicate view which contains only readable + // items + this.readableRootGroups[dataSource] = this.copyReadable(item, this.permissionFlags); + + } + + // Display only readable connections by default if at least one + // readable connection exists + this.currentTab = !!this.readableRootGroups[this.dataSource]?.children.length ? this.CURRENT_CONNECTIONS : this.ALL_CONNECTIONS; + + } + + /** + * Update the list data source when the selected tab changes. + */ + onCurrentTabChange(): void { + this.rootGroupsDataSource?.updateSource(this.getRootGroups()); + } + + /** + * Returns the root groups which should be displayed within the + * connection permission editor. + * + * @returns + * The root groups which should be displayed within the connection + * permission editor as a map of data source identifiers to the + * root connection groups within those data sources. + */ + getRootGroups(): Record { + + const rootGroups = this.currentTab === this.CURRENT_CONNECTIONS ? this.readableRootGroups : this.allRootGroups; + + if (rootGroups === null) { + return {}; + } + + return rootGroups as Record; + + } + + /** + * Returns whether the given PermissionFlagSet declares explicit READ + * permission for the connection, connection group, or sharing profile + * represented by the given GroupListItem. + * + * @param item + * The GroupListItem which should be checked against the + * PermissionFlagSet. + * + * @param flags + * The set of permissions which should be used to determine whether + * explicit READ permission is granted for the given item. + * + * @returns + * true if explicit READ permission is granted for the given item + * according to the given permission set, false otherwise. + */ + private isReadable(item: GroupListItem, flags: PermissionFlagSet): boolean { + + switch (item.type) { + + case GroupListItem.Type.CONNECTION: + return flags.connectionPermissions['READ'][item.identifier!]; + + case GroupListItem.Type.CONNECTION_GROUP: + return flags.connectionGroupPermissions['READ'][item.identifier!]; + + case GroupListItem.Type.SHARING_PROFILE: + return flags.sharingProfilePermissions['READ'][item.identifier!]; + + } + + return false; + + } + + /** + * Expands all items within the tree descending from the given + * GroupListItem which have at least one descendant for which explicit + * READ permission is granted. The expanded state of all other items is + * left untouched. + * + * @param item + * The GroupListItem which should be conditionally expanded + * depending on whether READ permission is granted for any of its + * descendants. + * + * @param flags + * The set of permissions which should be used to determine whether + * the given item and its descendants are expanded. + * + * @returns + * true if the given item has been expanded, false otherwise. + */ + private expandReadable(item: GroupListItem, flags: PermissionFlagSet): boolean { + + // If the current item is expandable and has defined children, + // determine whether it should be expanded + if (item.expandable && item.children) { + for (let child of item.children) { + + // The parent should be expanded by default if the child is + // expanded by default OR the permission set contains READ + // permission on the child + const childExpanded = this.expandReadable(child, flags) || this.isReadable(child, flags); + item.expanded ||= childExpanded; + + } + } + + return item.expanded; + + } + + /** + * Creates a deep copy of all items within the tree descending from the + * given GroupListItem which have at least one descendant for which + * explicit READ permission is granted. Items which lack explicit READ + * permission and which have no descendants having explicit READ + * permission are omitted from the copy. + * + * @param item + * The GroupListItem which should be conditionally copied + * depending on whether READ permission is granted for any of its + * descendants. + * + * @param flags + * The set of permissions which should be used to determine whether + * the given item or any of its descendants are copied. + * + * @returns + * A new GroupListItem containing a deep copy of the given item, + * omitting any items which lack explicit READ permission and whose + * descendants also lack explicit READ permission, or null if even + * the given item would not be copied. + */ + private copyReadable(item: GroupListItem, flags: PermissionFlagSet): GroupListItem { + + // Produce initial shallow copy of given item + item = new GroupListItem(item); + + // Replace children array with an array containing only readable + // children (or children with at least one readable descendant), + // flagging the current item for copying if any such children exist + if (item.children) { + + const children: GroupListItem[] = []; + for (let child of item.children) { + + // Reduce child tree to only explicitly readable items and + // their parents + child = this.copyReadable(child, flags); + + // Include child only if they are explicitly readable, they + // have explicitly readable descendants, or their parent is + // readable (and thus all children are relevant) + if ((child.children && child.children.length) + || this.isReadable(item, flags) + || this.isReadable(child, flags)) + children.push(child); + + } + + item.children = children; + + } + + return item; + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given connection permission. + * + * @param identifier + * The identifier of the connection to add READ permission for. + */ + private addConnectionPermission(identifier: string): void { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasConnectionPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addConnectionPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given connection permission. + * + * @param identifier + * The identifier of the connection to remove READ permission for. + */ + private removeConnectionPermission(identifier: string): void { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasConnectionPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addConnectionPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given connection group permission. + * + * @param identifier + * The identifier of the connection group to add READ permission + * for. + */ + private addConnectionGroupPermission(identifier: string): void { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasConnectionGroupPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionGroupPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addConnectionGroupPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given connection group permission. + * + * @param identifier + * The identifier of the connection group to remove READ permission + * for. + */ + private removeConnectionGroupPermission(identifier: string): void { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasConnectionGroupPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeConnectionGroupPermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addConnectionGroupPermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given sharing profile permission. + * + * @param identifier + * The identifier of the sharing profile to add READ permission for. + */ + private addSharingProfilePermission(identifier: string): void { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasSharingProfilePermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addSharingProfilePermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given sharing profile permission. + * + * @param identifier + * The identifier of the sharing profile to remove READ permission + * for. + */ + private removeSharingProfilePermission(identifier: string): void { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasSharingProfilePermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier)) + PermissionSet.removeSharingProfilePermission(this.permissionsAdded, PermissionSet.ObjectPermissionType.READ, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addSharingProfilePermission(this.permissionsRemoved, PermissionSet.ObjectPermissionType.READ, identifier); + + } + + /** + * Expose permission query and modification functions to group list template. + */ + groupListContext: ConnectionListContext = { + getPermissionFlags: () => { + return this.permissionFlags!; + }, + + connectionPermissionChanged: (identifier: string) => { + + // Determine current permission setting + const granted = this.permissionFlags!.connectionPermissions['READ'][identifier]; + + // Add/remove permission depending on flag state + if (granted) + this.addConnectionPermission(identifier); + else + this.removeConnectionPermission(identifier); + + }, + + connectionGroupPermissionChanged: (identifier: string) => { + + // Determine current permission setting + const granted = this.permissionFlags!.connectionGroupPermissions['READ'][identifier]; + + // Add/remove permission depending on flag state + if (granted) + this.addConnectionGroupPermission(identifier); + else + this.removeConnectionGroupPermission(identifier); + + }, + sharingProfilePermissionChanged : (identifier: string) => { + + // Determine current permission setting + const granted = this.permissionFlags!.sharingProfilePermissions['READ'][identifier]; + + // Add/remove permission depending on flag state + if (granted) + this.addSharingProfilePermission(identifier); + else + this.removeSharingProfilePermission(identifier); + + } + }; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.html new file mode 100644 index 0000000000..a88c4f0438 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.html @@ -0,0 +1,32 @@ + + +
      + + +
      + + + + + + {{ item.name }} + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.ts new file mode 100644 index 0000000000..06ecd0ee04 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/connection-permission/connection-permission.component.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + +/** + * A component which displays a single connection entry and allows + * manipulation of the connection permissions. + */ +@Component({ + selector: 'guac-connection-permission', + templateUrl: './connection-permission.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionPermissionComponent { + + /** + * TODO + */ + @Input({ required: true }) context!: ConnectionListContext; + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.html new file mode 100644 index 0000000000..881f91b9fb --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.html @@ -0,0 +1,22 @@ + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.ts new file mode 100644 index 0000000000..838e32b7da --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/data-source-tabs/data-source-tabs.component.ts @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import _ from 'lodash'; +import { canonicalize } from '../../../locale/service/translation.service'; +import { PageDefinition } from '../../../navigation/types/PageDefinition'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * Component which displays a set of tabs pointing to the same object within + * different data sources, such as user accounts which span multiple data + * sources. + */ +@Component({ + selector: 'data-source-tabs', + templateUrl: './data-source-tabs.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class DataSourceTabsComponent implements OnChanges { + + /** + * The permissions which dictate the management actions available + * to the current user. + */ + @Input() permissions: Record | null = null; + + /** + * A function which returns the URL of the object within a given + * data source. The relevant data source will be made available to + * the Angular expression defining this function as the + * "dataSource" variable. No other values will be made available, + * including values from the scope. + */ + @Input() url?: (dataSource: string) => string; + + /** + * The set of pages which each manage the same object within different + * data sources. + */ + pages: PageDefinition[] = []; + + ngOnChanges(changes: SimpleChanges): void { + if (changes['permissions']) { + const permissions = changes['permissions'].currentValue as Record; + + this.pages = []; + + const dataSources = _.keys(this.permissions).sort(); + dataSources.forEach(dataSource => { + + // Determine whether data source contains this object + const managementPermissions = permissions[dataSource]; + const exists = !!managementPermissions.identifier; + + // Data source is not relevant if the associated object does not + // exist and cannot be created + const readOnly = !managementPermissions.canSaveObject; + if (!exists && readOnly) + return; + + // Determine class name based on read-only / linked status + let className; + if (readOnly) className = 'read-only'; + else if (exists) className = 'linked'; + else className = 'unlinked'; + + // Add page entry + this.pages!.push(new PageDefinition({ + name : canonicalize('DATA_SOURCE_' + dataSource) + '.NAME', + url : this.url?.(dataSource) || '', + className: className + })); + + }); + + } + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.html new file mode 100644 index 0000000000..33b4d0ec65 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.html @@ -0,0 +1,71 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.ts new file mode 100644 index 0000000000..588d6e772e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/identifier-set-editor/identifier-set-editor.component.ts @@ -0,0 +1,332 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewChild, ViewEncapsulation } from '@angular/core'; +import _ from 'lodash'; +import { BehaviorSubject } from 'rxjs'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { FilterService } from '../../../list/services/filter.service'; +import { SortService } from '../../../list/services/sort.service'; +import { DataSource } from '../../../list/types/DataSource'; + +/** + * A component for manipulating a set of objects sharing some common relation + * and represented by an array of their identifiers. The specific objects + * added or removed are tracked within a separate pair of arrays of + * identifiers. + */ +@Component({ + selector: 'identifier-set-editor', + templateUrl: './identifier-set-editor.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class IdentifierSetEditorComponent implements OnInit, OnChanges { + + /** + * The translation key of the text which should be displayed within + * the main header of the identifier set editor. + */ + @Input({ required: true }) header!: string; + + /** + * The translation key of the text which should be displayed if no + * identifiers are currently present within the set. + */ + @Input({ required: true }) emptyPlaceholder!: string; + + /** + * The translation key of the text which should be displayed if no + * identifiers are available to be added within the set. + */ + @Input({ required: true }) unavailablePlaceholder!: string; + + /** + * All identifiers which are available to be added to or removed + * from the identifier set being edited. + */ + @Input() identifiersAvailable: string[] | null = null; + + /** + * The current state of the identifier set being manipulated. This + * array will be modified as changes are made through this + * identifier set editor. + */ + @Input() identifiers: string[] = []; + + /** + * The set of identifiers that have been added, relative to the + * initial state of the identifier set being manipulated. + */ + @Input({ required: true }) identifiersAdded!: string[]; + + /** + * The set of identifiers that have been removed, relative to the + * initial state of the identifier set being manipulated. + */ + @Input({ required: true }) identifiersRemoved!: string[]; + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * Filtered and paginated view of all identifiers which are available + * to be added to or removed from the identifier set being edited. + */ + identifiersAvailableDataSourceView: DataSource | null = null; + + /** + * Filtered view of the current state of the identifier set being + * manipulated. + */ + identifiersDataSourceView: DataSource | null = null; + + /** + * Whether the full list of available identifiers should be displayed. + * Initially, only an abbreviated list of identifiers currently present + * is shown. + */ + expanded = false; + + /** + * Map of identifiers to boolean flags indicating whether that + * identifier is currently present (true) or absent (false). If an + * identifier is absent, it may also be absent from this map. + */ + identifierFlags: Record = {}; + + /** + * Map of identifiers to boolean flags indicating whether that + * identifier is editable. If an identifier is not editable, it will be + * absent from this map. + */ + isEditable: Record = {}; + + /** + * The string currently being used to filter the list of available + * identifiers. + */ + filterString: BehaviorSubject = new BehaviorSubject(''); + + /** + * Inject required services. + */ + constructor(private filterService: FilterService, + private sortService: SortService, + private dataSourceBuilderService: DataSourceBuilderService) { + } + + ngOnInit(): void { + // Build the data source for the available identifiers. + this.identifiersAvailableDataSourceView = this.dataSourceBuilderService.getBuilder() + .source(this.identifiersAvailable || []) + .filter(this.filterString, ['']) + .paginate(this.pager.page) + .build(); + + // Build the data source for the current state of the identifier set being manipulated + this.identifiersDataSourceView = this.dataSourceBuilderService.getBuilder() + .source(this.identifiers) + .filter(this.filterString, ['']) + .build(); + } + + ngOnChanges(changes: SimpleChanges): void { + + // Keep identifierFlags up to date when identifiers array is replaced + // or initially assigned + if (changes['identifiers']) { + + // Maintain identifiers in sorted order so additions and removals + // can be made more efficiently + if (this.identifiers) + this.identifiers.sort(); + + // Convert array of identifiers into set of boolean + // presence/absence flags + const identifierFlags: Record = {}; + this.identifiers.forEach(identifier => { + identifierFlags[identifier] = true; + }); + this.identifierFlags = identifierFlags; + + // Update the corresponding data source + this.identifiersDataSourceView?.updateSource(this.identifiers); + } + + + if (changes['identifiersAvailable']) { + + // An identifier is editable iff it is available to be added or removed + // from the identifier set being edited (iff it is within the + // identifiersAvailable array) + this.isEditable = {}; + this.identifiersAvailable?.forEach(identifier => { + this.isEditable[identifier] = true; + }); + + // Update the corresponding data source + this.identifiersAvailableDataSourceView?.updateSource(this.identifiersAvailable || []); + } + } + + /** + * Adds the given identifier to the given sorted array of identifiers, + * preserving the sorted order of the array. If the identifier is + * already present, no change is made to the array. The given array + * must already be sorted in ascending order. + * + * @param arr + * The sorted array of identifiers to add the given identifier to. + * + * @param identifier + * The identifier to add to the given array. + */ + private addIdentifier(arr: string[], identifier: string): void { + + // Determine location that the identifier should be added to + // maintain sorted order + const index = _.sortedIndex(arr, identifier); + + // Do not add if already present + if (arr[index] === identifier) + return; + + // Insert identifier at determined location + arr.splice(index, 0, identifier); + + } + + /** + * Removes the given identifier from the given sorted array of + * identifiers, preserving the sorted order of the array. If the + * identifier is already absent, no change is made to the array. The + * given array must already be sorted in ascending order. + * + * @param arr + * The sorted array of identifiers to remove the given identifier + * from. + * + * @param identifier + * The identifier to remove from the given array. + * + * @returns + * true if the identifier was present in the given array and has + * been removed, false otherwise. + */ + private removeIdentifier(arr: string[], identifier: string): boolean { + + // Search for identifier in sorted array + const index = _.sortedIndexOf(arr, identifier); + + // Nothing to do if already absent + if (index === -1) + return false; + + // Remove identifier + arr.splice(index, 1); + return true; + + } + + /** + * Notifies the controller that a change has been made to the flag + * denoting presence/absence of a particular identifier within the + * identifierFlags map. The identifiers, + * identifiersAdded, and identifiersRemoved + * arrays are updated accordingly. + * + * @param identifier + * The identifier which has been added or removed through modifying + * its boolean flag within identifierFlags. + */ + identifierChanged(identifier: string): void { + + // Determine status of modified identifier + const present = this.identifierFlags[identifier]; + + // Add/remove identifier from added/removed sets depending on + // change in flag state + if (present) { + + this.addIdentifier(this.identifiers, identifier); + + if (!this.removeIdentifier(this.identifiersRemoved, identifier)) + this.addIdentifier(this.identifiersAdded, identifier); + + } else { + + this.removeIdentifier(this.identifiers, identifier); + + if (!this.removeIdentifier(this.identifiersAdded, identifier)) + this.addIdentifier(this.identifiersRemoved, identifier); + + } + + // Update the corresponding signal + this.identifiersDataSourceView?.updateSource(this.identifiers); + } + + /** + * Removes the given identifier, updating identifierFlags, + * identifiers, identifiersAdded, and + * identifiersRemoved accordingly. + * + * @param {String} identifier + * The identifier to remove. + */ + removeIdentifierFromScope(identifier: string): void { + this.identifierFlags[identifier] = false; + this.identifierChanged(identifier); + } + + /** + * Shows the full list of available identifiers. If the full list is + * already shown, this function has no effect. + */ + expand(): void { + this.expanded = true; + } + + /** + * Hides the full list of available identifiers. If the full list is + * already hidden, this function has no effect. + */ + collapse(): void { + this.expanded = false; + } + + /** + * Returns whether there are absolutely no identifiers that can be + * managed using this editor. If true, the editor is effectively + * useless, as there is nothing whatsoever to display. + * + * @returns} + * true if there are no identifiers that can be managed using this + * editor, false otherwise. + */ + isEmpty(): boolean { + return _.isEmpty(this.identifiers) + && _.isEmpty(this.identifiersAvailable); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.html new file mode 100644 index 0000000000..6d3c86facc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.html @@ -0,0 +1,23 @@ + + + + {{ item.name }} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.ts new file mode 100644 index 0000000000..0100fbd82d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser-connection-group/location-chooser-connection-group.component.ts @@ -0,0 +1,33 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; + +@Component({ + selector: 'guac-location-chooser-connection-group', + templateUrl: './location-chooser-connection-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class LocationChooserConnectionGroupComponent { + @Input({ required: true }) context!: { chooseGroup: (item: ConnectionGroup) => void }; + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.html new file mode 100644 index 0000000000..2472abc6f3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.html @@ -0,0 +1,41 @@ + + +
      + + +
      {{ chosenConnectionGroupName }}
      + + + + +
      + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.ts new file mode 100644 index 0000000000..77cb3e95f0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/location-chooser/location-chooser.component.ts @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; + +/** + * A component for choosing the location of a connection or connection group. + */ +@Component({ + selector: 'guac-location-chooser', + templateUrl: './location-chooser.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class LocationChooserComponent implements OnInit, OnChanges { + + /** + * The identifier of the data source from which the given root + * connection group was retrieved. + */ + @Input() dataSource?: string; + + /** + * The root connection group of the connection group hierarchy to + * display. + */ + @Input({ required: true }) rootGroup!: ConnectionGroup; + + /** + * The unique identifier of the currently-selected connection + * group. If not specified, the root group will be used. + */ + @Input() value?: string; + + /** + * Map of unique identifiers to their corresponding connection + * groups. + */ + private connectionGroups: Record = {}; + + /** + * Whether the group list menu is currently open. + */ + menuOpen = false; + + /** + * The human-readable name of the currently-chosen connection + * group. + */ + chosenConnectionGroupName: string | null = null; + + /** + * Map of the data source identifier to the root connection group. + */ + rootGroups: Record = {}; + + /** + * Recursively traverses the given connection group and all + * children, storing each encountered connection group within the + * connectionGroups map by its identifier. + * + * @param group + * The connection group to traverse. + */ + private mapConnectionGroups(group: ConnectionGroup): void { + + // Map given group + (this.connectionGroups)[group.identifier!] = group; + + // Map all child groups + if (group.childConnectionGroups) + group.childConnectionGroups.forEach((childGroup: ConnectionGroup) => this.mapConnectionGroups(childGroup)); + + } + + /** + * Toggle the current state of the menu listing connection groups. + * If the menu is currently open, it will be closed. If currently + * closed, it will be opened. + */ + toggleMenu(): void { + this.menuOpen = !this.menuOpen; + } + + ngOnInit(): void { + this.onDataSourceChange(); + } + + ngOnChanges(changes: SimpleChanges): void { + + // Update the root group map when data source or root group change + if (changes['dataSource'] || changes['rootGroup']) { + + // Abort if the root group is not set + if (this.dataSource && this.rootGroup) { + + // Wrap root group in map + this.rootGroups = {}; + this.rootGroups[this.dataSource] = this.rootGroup; + } + + } + + if (changes['dataSource']) { + this.onDataSourceChange(); + } + + } + + onDataSourceChange(): void { + this.connectionGroups = {}; + + if (this.rootGroup) { + + // Map all known groups + this.mapConnectionGroups(this.rootGroup); + + // If no value is specified, default to the root identifier + if (!this.value || !(this.value in this.connectionGroups)) + this.value = this.rootGroup.identifier!; + + this.chosenConnectionGroupName = this.connectionGroups[this.value].name; + } + } + + + /** + * Expose selection function to group list template. + */ + groupListContext = { + + /** + * Selects the given group item. + * + * @param item + * The chosen item. + */ + chooseGroup: (item: ConnectionGroup) => { + + // Record new parent + this.value = item.identifier; + this.chosenConnectionGroupName = item.name; + + // Close menu + this.menuOpen = false; + + } + + }; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.html new file mode 100644 index 0000000000..ca1144caa5 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.html @@ -0,0 +1,81 @@ + + + +
      +
      + +
      + + +
      +

      {{ 'MANAGE_CONNECTION_GROUP.SECTION_HEADER_EDIT_CONNECTION_GROUP' | transloco }}

      + +
      +
      + + + + + + + + + + + + + + + + + + + + + + +
      {{ 'MANAGE_CONNECTION_GROUP.FIELD_HEADER_NAME' | transloco }}
      {{ 'MANAGE_CONNECTION_GROUP.FIELD_HEADER_LOCATION' | transloco }} + +
      {{ 'MANAGE_CONNECTION_GROUP.FIELD_HEADER_TYPE' | transloco }} + +
      +
      + + +
      + +
      + + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.ts new file mode 100644 index 0000000000..210d782407 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection-group/manage-connection-group.component.ts @@ -0,0 +1,326 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { forkJoin, map, Observable, of } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { Form } from '../../../rest/types/Form'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { NonNullableProperties } from '../../../util/utility-types'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * The component for editing or creating connection groups. + */ +@Component({ + selector: 'guac-manage-connection-group', + templateUrl: './manage-connection-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManageConnectionGroupComponent implements OnInit { + + /** + * The identifier of the user group being edited. If a new user group is + * being created, this will not be defined. + */ + @Input({ alias: 'id' }) private identifier?: string; + + /** + * The unique identifier of the data source containing the connection group + * being edited. + */ + @Input({ alias: 'dataSource' }) selectedDataSource!: string; + + /** + * The identifier of the original connection group from which this + * connection group is being cloned. Only valid if this is a new + * connection group. + */ + private cloneSourceIdentifier: string | null = null; + + /** + * Available connection group types, as translation string / internal value + * pairs. + */ + types: { label: string; value: string }[] = [ + { + label: 'MANAGE_CONNECTION_GROUP.NAME_TYPE_ORGANIZATIONAL', + value: ConnectionGroup.Type.ORGANIZATIONAL + }, + { + label: 'MANAGE_CONNECTION_GROUP.NAME_TYPE_BALANCING', + value: ConnectionGroup.Type.BALANCING + } + ]; + + /** + * The root connection group of the connection group hierarchy. + */ + rootGroup: ConnectionGroup | null = null; + + /** + * The connection group being modified. + */ + connectionGroup: ConnectionGroup | null = null; + + /** + * The management-related actions that the current user may perform on the + * connection group currently being created/modified, or null if the current + * user's permissions have not yet been loaded. + */ + managementPermissions: ManagementPermissions | null = null; + + /** + * All available connection group attributes. This is only the set of + * attribute definitions, organized as logical groupings of attributes, not + * attribute values. + */ + attributes: Form[] | null = null; + + /** + * Form group for editing connection group attributes. + */ + connectionGroupAttributesFormGroup: FormGroup = new FormGroup({}); + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private permissionService: PermissionService, + private requestService: RequestService, + private schemaService: SchemaService, + private formService: FormService, + private router: Router, + private route: ActivatedRoute) { + } + + ngOnInit(): void { + this.cloneSourceIdentifier = this.route.snapshot.queryParamMap.get('clone'); + + // Query the user's permissions for the current connection group + const connectionGroupData = this.loadRequestedConnectionGroup(); + const attributes = this.schemaService.getConnectionGroupAttributes(this.selectedDataSource); + const permissions = this.permissionService.getEffectivePermissions(this.selectedDataSource, this.authenticationService.getCurrentUsername()!); + const rootGroup = this.connectionGroupService.getConnectionGroupTree(this.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]); + + + forkJoin([connectionGroupData, attributes, permissions, rootGroup]) + .subscribe({ + + next : ([_, attributes, permissions, rootGroup]) => { + + this.attributes = attributes; + this.rootGroup = rootGroup; + + this.connectionGroupAttributesFormGroup = this.formService.getFormGroup(attributes); + this.connectionGroupAttributesFormGroup.patchValue(this.connectionGroup?.attributes || {}); + this.connectionGroupAttributesFormGroup.valueChanges.subscribe((value) => { + if (this.connectionGroup) + this.connectionGroup.attributes = value; + }); + + this.managementPermissions = ManagementPermissions.fromPermissionSet( + permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionGroupPermission, + this.identifier); + + }, error: this.requestService.DIE + + }); + + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + + return this.rootGroup !== null + && this.connectionGroup !== null + && this.managementPermissions !== null + && this.attributes !== null; + + } + + /** + * Loads the data associated with the connection group having the given + * identifier, preparing the interface for making modifications to that + * existing connection group. + * + * @param dataSource + * The unique identifier of the data source containing the connection + * group to load. + * + * @param identifier + * The identifier of the connection group to load. + * + * @returns + * An observable which emits when the interface has been prepared for + * editing the given connection group. + */ + private loadExistingConnectionGroup(dataSource: string, identifier: string): Observable { + return this.connectionGroupService.getConnectionGroup( + dataSource, + identifier + ) + .pipe(map(connectionGroup => { + this.connectionGroup = connectionGroup; + })); + } + + /** + * Loads the data associated with the connection group having the given + * identifier, preparing the interface for cloning that existing + * connection group. + * + * @param dataSource + * The unique identifier of the data source containing the connection + * group to be cloned. + * + * @param identifier + * The identifier of the connection group being cloned. + * + * @returns + * An observable which emits when the interface has been prepared for + * cloning the given connection group. + */ + private loadClonedConnectionGroup(dataSource: string, identifier: string): Observable { + return this.connectionGroupService.getConnectionGroup( + dataSource, + identifier + ) + .pipe(map((connectionGroup) => { + this.connectionGroup = connectionGroup; + delete this.connectionGroup.identifier; + })); + } + + /** + * Loads skeleton connection group data, preparing the interface for + * creating a new connection group. + * + * @returns + * An observable which emits when the interface has been prepared for + * creating a new connection group. + */ + private loadSkeletonConnectionGroup(): Observable { + + // Use skeleton connection group object with specified parent + this.connectionGroup = new ConnectionGroup({ + identifier : '', + name : '', + type : '', + activeConnections: 0, + parentIdentifier : this.route.snapshot.queryParamMap.get('parent') || undefined + }); + + return of(void (0)); + + } + + /** + * Loads the data required for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing connection group, cloning an + * existing connection group, or creating an entirely new connection group. + * + * @returns + * An observable which emits when the interface has been prepared + * for performing the requested management task. + */ + private loadRequestedConnectionGroup(): Observable { + + // If we are editing an existing connection group, pull its data + if (this.identifier) + return this.loadExistingConnectionGroup(this.selectedDataSource, this.identifier); + + // If we are cloning an existing connection group, pull its data + // instead + if (this.cloneSourceIdentifier) + return this.loadClonedConnectionGroup(this.selectedDataSource, this.cloneSourceIdentifier); + + // If we are creating a new connection group, populate skeleton + // connection group data + return this.loadSkeletonConnectionGroup(); + + } + + /** + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. + */ + returnToConnectionList() { + this.router.navigate(['settings', encodeURIComponent(this.selectedDataSource), 'connections']); + } + + /** + * Cancels all pending edits, opening an edit page for a new connection + * group which is prepopulated with the data from the connection group + * currently being edited. + */ + cloneConnectionGroup() { + this.router.navigate( + ['manage', encodeURIComponent(this.selectedDataSource), 'connectionGroups'], + { queryParams: { clone: this.identifier } } + ).then(() => window.scrollTo(0, 0)); + } + + /** + * Saves the current connection group, creating a new connection group or + * updating the existing connection group, returning a promise which is + * resolved if the save operation succeeds and rejected if the save + * operation fails. + * + * @returns + * An observable which completes if the save operation succeeds and is + * fails with an {@link Error} if the save operation fails. + */ + saveConnectionGroup(): Observable { + return this.connectionGroupService.saveConnectionGroup(this.selectedDataSource, this.connectionGroup!); + } + + /** + * Deletes the current connection group, returning a promise which is + * resolved if the delete operation succeeds and rejected if the delete + * operation fails. + * + * @returns + * An observable which completes if the delete operation succeeds and is + * fails with an {@link Error} if the delete operation fails. + */ + deleteConnectionGroup(): Observable { + return this.connectionGroupService.deleteConnectionGroup(this.selectedDataSource, this.connectionGroup!); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.html new file mode 100644 index 0000000000..d0517a5afe --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.html @@ -0,0 +1,127 @@ + + +
      + + + + + +
      +

      {{ 'MANAGE_CONNECTION.SECTION_HEADER_EDIT_CONNECTION' | transloco }}

      + +
      +
      + + + + + + + + + + + + + + + + + + + + + + +
      {{ 'MANAGE_CONNECTION.FIELD_HEADER_NAME' | transloco }}
      {{ 'MANAGE_CONNECTION.FIELD_HEADER_LOCATION' | transloco }} + +
      {{ 'MANAGE_CONNECTION.FIELD_HEADER_PROTOCOL' | transloco }} + +
      +
      + + +
      + +
      + + +

      {{ 'MANAGE_CONNECTION.SECTION_HEADER_PARAMETERS' | transloco }}

      +
      + +
      + + + + + +
      + + +

      {{ 'MANAGE_CONNECTION.SECTION_HEADER_HISTORY' | transloco }}

      +
      +

      {{ 'MANAGE_CONNECTION.INFO_CONNECTION_NOT_USED' | transloco }}

      + + + + + + + + + + + + + + + + + + + +
      {{ 'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_USERNAME' | transloco }}{{ 'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_START' | transloco }}{{ 'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_DURATION' | transloco }}{{ 'MANAGE_CONNECTION.TABLE_HEADER_HISTORY_REMOTEHOST' | transloco }}
      + + {{ wrapper.entry.startDate | date:historyDateFormat }}{{ wrapper.entry.remoteHost }}
      + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.ts new file mode 100644 index 0000000000..928b18ce24 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-connection/manage-connection.component.ts @@ -0,0 +1,459 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { TranslocoService } from '@ngneat/transloco'; +import { forkJoin, map, Observable, of, Subscription } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { ConnectionService } from '../../../rest/service/connection.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { Connection } from '../../../rest/types/Connection'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { Form } from '../../../rest/types/Form'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { Protocol } from '../../../rest/types/Protocol'; +import { HistoryEntryWrapper } from '../../types/HistoryEntryWrapper'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * Component for editing or creating connections. + */ +@Component({ + selector: 'guac-manage-connection', + templateUrl: './manage-connection.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManageConnectionComponent implements OnInit { + + /** + * The unique identifier of the data source containing the connection being + * edited. + */ + @Input('dataSource') selectedDataSource!: string; + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + + /** + * The identifier of the original connection from which this connection is + * being cloned. Only valid if this is a new connection. + */ + private cloneSourceIdentifier: string | null = null; + + /** + * The identifier of the connection being edited. If a new connection is + * being created, this will not be defined. + */ + @Input('id') private identifier?: string; + + /** + * All known protocols. + */ + protocols?: Record; + + /** + * The root connection group of the connection group hierarchy. + */ + rootGroup?: ConnectionGroup; + + /** + * The connection being modified. + */ + connection?: Connection; + + /** + * The currently selected protocol for the connection being modified. + * + * @returns The protocol of the connection. + */ + get selectedProtocol(): string | undefined { + + return this.connection?.protocol; + + } + + /** + * Sets the protocol for the connection being modified and updates the + * form group for editing connection parameters. + * + * @param value The protocol to be set for the connection. + */ + set selectedProtocol(value: string) { + + this.connection!.protocol = value; + this.updateParametersFormGroup(); + + } + + /** + * The parameter name/value pairs associated with the connection being + * modified. + */ + parameters?: Record; + + /** + * The form group for editing connection parameters. + */ + parametersFormGroup: FormGroup = new FormGroup({}); + + /** + * The subscription to changes in the parameters form group. + */ + parametersFormGroupSubscription?: Subscription; + + /** + * The date format for use within the connection history. + */ + historyDateFormat?: string; + + /** + * The usage history of the connection being modified. + */ + historyEntryWrappers?: HistoryEntryWrapper[]; + + /** + * Paginated view of the usage history of the connection being modified. + */ + dataSourceView: DataSource | null = null; + + /** + * The management-related actions that the current user may perform on the + * connection currently being created/modified, or undefined if the current + * user's permissions have not yet been loaded. + */ + managementPermissions?: ManagementPermissions; + + /** + * All available connection attributes. This is only the set of attribute + * definitions, organized as logical groupings of attributes, not attribute + * values. + */ + attributes?: Form[]; + + /** + * Form group for editing connection attributes. + */ + connectionAttributesFormGroup: FormGroup = new FormGroup({}); + + /** + * Inject required services. + */ + constructor(private router: Router, + private route: ActivatedRoute, + private authenticationService: AuthenticationService, + private connectionService: ConnectionService, + private connectionGroupService: ConnectionGroupService, + private permissionService: PermissionService, + private requestService: RequestService, + private schemaService: SchemaService, + private translocoService: TranslocoService, + private formService: FormService, + private dataSourceBuilderService: DataSourceBuilderService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.cloneSourceIdentifier = this.route.snapshot.queryParamMap.get('clone'); + + // Build the data source for the user group list entries. + this.dataSourceView = this.dataSourceBuilderService.getBuilder() + .source([]) + .paginate(this.pager.page) + .build(); + + // Populate interface with requested data + forkJoin([ + this.loadRequestedConnection(), + this.schemaService.getConnectionAttributes(this.selectedDataSource), + this.permissionService.getEffectivePermissions(this.selectedDataSource, this.authenticationService.getCurrentUsername()!), + this.schemaService.getProtocols(this.selectedDataSource), + this.connectionGroupService.getConnectionGroupTree(this.selectedDataSource, ConnectionGroup.ROOT_IDENTIFIER, [PermissionSet.ObjectPermissionType.ADMINISTER]) + ]) + .subscribe({ + next : ([connectionData, attributes, permissions, protocols, rootGroup]) => { + + this.dataSourceView?.updateSource(this.historyEntryWrappers!); + + this.attributes = attributes; + this.updateAttributesFormGroup(); + + this.protocols = protocols; + this.updateParametersFormGroup(); + + this.rootGroup = rootGroup; + + this.managementPermissions = ManagementPermissions.fromPermissionSet( + permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionPermission, + this.identifier); + + }, error: this.requestService.DIE + }); + + // Get history date format + this.translocoService.selectTranslate('MANAGE_CONNECTION.FORMAT_HISTORY_START').subscribe(historyDateFormat => { + this.historyDateFormat = historyDateFormat; + }); + } + + /** + * Updates the form group for editing connection attributes. + */ + private updateAttributesFormGroup(): void { + + this.connectionAttributesFormGroup = this.formService.getFormGroup(this.attributes!); + this.connectionAttributesFormGroup.patchValue(this.connection?.attributes || {}); + this.connectionAttributesFormGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => { + this.connection!.attributes = value; + }); + + } + + /** + * Updates the form group for editing connection parameters based on the selected protocol. + */ + private updateParametersFormGroup(): void { + + this.parametersFormGroupSubscription?.unsubscribe(); + this.parametersFormGroup = this.formService.getFormGroup(this.protocols![this.selectedProtocol!].connectionForms); + this.parametersFormGroup.patchValue(this.parameters || {}); + this.parametersFormGroupSubscription = this.parametersFormGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => { + this.parameters = value; + }); + + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): this is Required { + + return this.protocols !== undefined + && this.rootGroup !== undefined + && this.connection !== undefined + && this.parameters !== undefined + && this.historyDateFormat !== undefined + && this.historyEntryWrappers !== undefined + && this.managementPermissions !== undefined + && this.attributes !== undefined + && this.dataSourceView !== null; + + } + + /** + * Loads the data required for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing connection, cloning an existing + * connection, or creating an entirely new connection. + * + * @returns + * An observable which completes when the interface has been prepared + * for performing the requested management task. + */ + private loadRequestedConnection(): Observable { + + // If we are editing an existing connection, pull its data + if (this.identifier) + return this.loadExistingConnection(this.selectedDataSource, this.identifier); + + // If we are cloning an existing connection, pull its data instead + if (this.cloneSourceIdentifier) + return this.loadClonedConnection(this.selectedDataSource, this.cloneSourceIdentifier); + + // If we are creating a new connection, populate skeleton connection data + return this.loadSkeletonConnection(); + + } + + /** + * Loads the data associated with the connection having the given + * identifier, preparing the interface for making modifications to that + * existing connection. + * + * @param dataSource + * The unique identifier of the data source containing the connection to + * load. + * + * @param identifier + * The identifier of the connection to load. + * + * @returns + * An observable which completes when the interface has been prepared for + * editing the given connection. + */ + private loadExistingConnection(dataSource: string, identifier: string): Observable { + return forkJoin([ + this.connectionService.getConnection(dataSource, identifier), + this.connectionService.getConnectionHistory(dataSource, identifier), + this.connectionService.getConnectionParameters(dataSource, identifier) + ]).pipe( + map(([connection, historyEntries, parameters]) => { + + this.connection = connection; + this.parameters = parameters; + + // Wrap all history entries for sake of display + this.historyEntryWrappers = []; + historyEntries.forEach(historyEntry => { + this.historyEntryWrappers!.push(new HistoryEntryWrapper(historyEntry)); + }); + + }) + ); + } + + /** + * Loads the data associated with the connection having the given + * identifier, preparing the interface for cloning that existing + * connection. + * + * @param dataSource + * The unique identifier of the data source containing the connection + * to be cloned. + * + * @param identifier + * The identifier of the connection being cloned. + * + * @returns + * An observable which completes when the interface has been prepared for + * cloning the given connection. + */ + private loadClonedConnection(dataSource: string, identifier: string): Observable { + return forkJoin([ + this.connectionService.getConnection(dataSource, identifier), + this.connectionService.getConnectionParameters(dataSource, identifier) + ]).pipe( + map(([connection, parameters]) => { + + this.connection = connection; + this.parameters = parameters; + + // Clear the identifier field because this connection is new + delete this.connection.identifier; + + // Cloned connections have no history + this.historyEntryWrappers = []; + + }) + ); + } + + /** + * Loads skeleton connection data, preparing the interface for creating a + * new connection. + * + * @returns + * An observable which completes when the interface has been prepared for + * creating a new connection. + */ + private loadSkeletonConnection(): Observable { + + // Use skeleton connection object with no associated permissions, + // history, or parameters + this.connection = new Connection({ + protocol : 'vnc', + parentIdentifier: this.route.snapshot.queryParamMap.get('parent') || undefined + }); + + this.historyEntryWrappers = []; + this.parameters = {}; + + return of(void (0)); + + } + + /** + * @borrows Protocol.getNamespace + */ + getNamespace = Protocol.getNamespace; + + /** + * @borrows Protocol.getName + */ + getProtocolName = Protocol.getName; + + /** + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. + */ + returnToConnectionList(): void { + this.router.navigate(['settings', encodeURIComponent(this.selectedDataSource), 'connections']); + } + + /** + * Cancels all pending edits, opening an edit page for a new connection + * which is prepopulated with the data from the connection currently being edited. + */ + cloneConnection(): void { + this.router.navigate( + ['manage', encodeURIComponent(this.selectedDataSource), 'connections'], + { queryParams: { clone: this.identifier } } + ).then(() => window.scrollTo(0, 0)); + } + + /** + * Saves the current connection, creating a new connection or updating the + * existing connection, returning an observable which completes if the save + * operation succeeds and rejected if the save operation fails. + * + * @returns + * An observable which completes if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. + */ + saveConnection(): Observable { + + this.connection!.parameters = this.parameters || undefined; + + // Save the connection + return this.connectionService.saveConnection(this.selectedDataSource, this.connection!); + + } + + /** + * Deletes the current connection, returning an observable which completes if + * the delete operation succeeds and rejected if the delete operation fails. + * + * @returns + * An observable which completes if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. + */ + deleteConnection(): Observable { + return this.connectionService.deleteConnection(this.selectedDataSource, this.connection!); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.html new file mode 100644 index 0000000000..570ab8b142 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.html @@ -0,0 +1,69 @@ + + +
      + + + + +
      +

      {{ 'MANAGE_SHARING_PROFILE.SECTION_HEADER_EDIT_SHARING_PROFILE' | transloco }}

      + +
      +
      + + + + + + + + + +
      {{ 'MANAGE_SHARING_PROFILE.FIELD_HEADER_NAME' | transloco }}
      {{ 'MANAGE_SHARING_PROFILE.FIELD_HEADER_PRIMARY_CONNECTION' | transloco }}{{ primaryConnection.name }}
      +
      + + +
      + +
      + + +

      {{ 'MANAGE_SHARING_PROFILE.SECTION_HEADER_PARAMETERS' | transloco }}

      +
      + +
      + + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.ts new file mode 100644 index 0000000000..affc39752a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-sharing-profile/manage-sharing-profile.component.ts @@ -0,0 +1,393 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { forkJoin, map, Observable, switchMap, tap } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { ConnectionService } from '../../../rest/service/connection.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { SharingProfileService } from '../../../rest/service/sharing-profile.service'; +import { Connection } from '../../../rest/types/Connection'; +import { Form } from '../../../rest/types/Form'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { Protocol } from '../../../rest/types/Protocol'; +import { SharingProfile } from '../../../rest/types/SharingProfile'; +import { NonNullableAllProperties } from '../../../util/utility-types'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * Component for editing or creating sharing profiles. + */ +@Component({ + selector: 'guac-manage-sharing-profile', + templateUrl: './manage-sharing-profile.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManageSharingProfileComponent implements OnInit { + + /** + * The unique identifier of the data source containing the sharing profile + * being edited. + */ + @Input('dataSource') selectedDataSource!: string; + + /** + * The identifier of the original sharing profile from which this sharing + * profile is being cloned. Only valid if this is a new sharing profile. + */ + private cloneSourceIdentifier: string | null = null; + + /** + * The identifier of the sharing profile being edited. If a new sharing + * profile is being created, this will not be defined. + */ + @Input('id') private identifier?: string; + + /** + * Map of protocol name to corresponding Protocol object. + */ + protocols: Record | null = null; + + /** + * The sharing profile being modified. + */ + sharingProfile: SharingProfile | null = null; + + /** + * The connection associated with the sharing profile being modified. + */ + primaryConnection: Connection | null = null; + + /** + * The parameter name/value pairs associated with the sharing profile being + * modified. + */ + parameters: Record | null = null; + + /** + * The form group for editing sharing profile parameters. + */ + parametersFormGroup: FormGroup = new FormGroup({}); + + /** + * The management-related actions that the current user may perform on the + * sharing profile currently being created/modified, or null if the current + * user's permissions have not yet been loaded. + */ + managementPermissions: ManagementPermissions | null = null; + + /** + * All available sharing profile attributes. This is only the set of + * attribute definitions, organized as logical groupings of attributes, not + * attribute values. + */ + attributes?: Form[]; + + /** + * Form group for editing sharing profile attributes. + */ + sharingProfileAttributesFormGroup: FormGroup = new FormGroup({}); + + /** + * Inject required services. + */ + constructor(private router: Router, + private route: ActivatedRoute, + private authenticationService: AuthenticationService, + private connectionService: ConnectionService, + private permissionService: PermissionService, + private requestService: RequestService, + private schemaService: SchemaService, + private sharingProfileService: SharingProfileService, + private formService: FormService, + private destroyRef: DestroyRef) { + } + + /** + * Query the user's permissions for the current sharing profile. + */ + ngOnInit(): void { + + this.cloneSourceIdentifier = this.route.snapshot.queryParamMap.get('clone'); + + forkJoin([ + this.loadRequestedSharingProfile(), + this.schemaService.getSharingProfileAttributes(this.selectedDataSource), + this.schemaService.getProtocols(this.selectedDataSource), + this.permissionService.getEffectivePermissions(this.selectedDataSource, this.authenticationService.getCurrentUsername()!) + ]).subscribe({ + next : ([_, attributes, protocols, permissions]) => { + + this.attributes = attributes; + this.protocols = protocols; + + // Create form group for editing sharing profile attributes + this.sharingProfileAttributesFormGroup = this.formService.getFormGroup(attributes); + this.sharingProfileAttributesFormGroup.patchValue(this.sharingProfile?.attributes || {}); + this.sharingProfileAttributesFormGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => { + this.sharingProfile!.attributes = value; + }); + + // Create form group for editing sharing profile parameters + this.parametersFormGroup = this.formService.getFormGroup(this.protocols[this.primaryConnection!.protocol].sharingProfileForms); + this.parametersFormGroup.patchValue(this.parameters || {}); + this.parametersFormGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((value) => { + this.parameters = value; + }); + + this.managementPermissions = ManagementPermissions.fromPermissionSet( + permissions, + PermissionSet.SystemPermissionType.CREATE_CONNECTION, + PermissionSet.hasConnectionPermission, + this.identifier); + + }, + error: this.requestService.DIE + }); + + } + + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): this is NonNullableAllProperties { + + return this.protocols !== null + && this.sharingProfile !== null + && this.primaryConnection !== null + && this.parameters !== null + && this.managementPermissions !== null + && this.attributes !== null; + + } + + /** + * Loads the data required for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing sharing profile, cloning an + * existing sharing profile, or creating an entirely new sharing profile. + * + * @returns + * An observable which completes when the interface has been prepared + * for performing the requested management task. + */ + loadRequestedSharingProfile(): Observable { + + // If we are editing an existing sharing profile, pull its data + if (this.identifier) + return this.loadExistingSharingProfile(this.selectedDataSource, this.identifier); + + // If we are cloning an existing sharing profile, pull its data instead + if (this.cloneSourceIdentifier) + return this.loadClonedSharingProfile(this.selectedDataSource, this.cloneSourceIdentifier); + + // If we are creating a new sharing profile, populate skeleton sharing + // profile data + return this.loadSkeletonSharingProfile(this.selectedDataSource); + + } + + /** + * Loads the data associated with the sharing profile having the given + * identifier, preparing the interface for making modifications to that + * existing sharing profile. + * + * @param dataSource + * The unique identifier of the data source containing the sharing + * profile to load. + * + * @param identifier + * The identifier of the sharing profile to load. + * + * @returns + * An observable which completes when the interface has been prepared for + * editing the given sharing profile. + */ + private loadExistingSharingProfile(dataSource: string, identifier: string): Observable { + return forkJoin([ + this.sharingProfileService.getSharingProfile(dataSource, identifier), + this.sharingProfileService.getSharingProfileParameters(dataSource, identifier) + ]).pipe( + // Store sharing profile and parameters + tap(([sharingProfile, parameters]) => { + this.sharingProfile = sharingProfile; + this.parameters = parameters; + }), + + // Load connection object for associated primary connection + switchMap(([sharingProfile]) => { + return this.connectionService.getConnection( + dataSource, + sharingProfile.primaryConnectionIdentifier! + ); + }), + + // Store connection object and map observable to void + map(connection => { + this.primaryConnection = connection; + }) + ); + } + + /** + * Loads the data associated with the sharing profile having the given + * identifier, preparing the interface for cloning that existing + * sharing profile. + * + * @param dataSource + * The unique identifier of the data source containing the sharing + * profile to be cloned. + * + * @param identifier + * The identifier of the sharing profile being cloned. + * + * @returns + * An observable which completes when the interface has been prepared for + * cloning the given sharing profile. + */ + private loadClonedSharingProfile(dataSource: string, identifier: string): Observable { + return forkJoin([ + this.sharingProfileService.getSharingProfile(dataSource, identifier), + this.sharingProfileService.getSharingProfileParameters(dataSource, identifier) + ]).pipe( + tap(([sharingProfile, parameters]) => { + this.sharingProfile = sharingProfile; + this.parameters = parameters; + + // Clear the identifier field because this sharing profile is new + delete this.sharingProfile.identifier; + }), + + // Load connection object for associated primary connection + switchMap(([sharingProfile]) => { + return this.connectionService.getConnection( + dataSource, + sharingProfile.primaryConnectionIdentifier! + ); + }), + + // Store connection object and map observable to void + map(connection => { + this.primaryConnection = connection; + }) + ); + } + + /** + * Loads skeleton sharing profile data, preparing the interface for + * creating a new sharing profile. + * + * @param dataSource + * The unique identifier of the data source containing the sharing + * profile to be created. + * + * @returns + * An observable which completes when the interface has been prepared for + * creating a new sharing profile. + */ + private loadSkeletonSharingProfile(dataSource: string): Observable { + + // Use skeleton sharing profile object with no associated parameters + this.sharingProfile = new SharingProfile({ + primaryConnectionIdentifier: this.route.snapshot.queryParamMap.get('parent')! + }); + this.parameters = {}; + + // Load connection object for associated primary connection + return this.connectionService.getConnection( + dataSource, + this.sharingProfile.primaryConnectionIdentifier! + ).pipe( + // Store connection object and map observable to void + map(connection => { + this.primaryConnection = connection; + }) + ); + } + + /** + * @borrows Protocol.getNamespace + */ + getNamespace = Protocol.getNamespace; + + /** + * Cancels all pending edits, returning to the main list of connections + * within the selected data source. + */ + returnToConnectionList(): void { + this.router.navigate(['settings', encodeURIComponent(this.selectedDataSource), 'connections']); + } + + /** + * Cancels all pending edits, opening an edit page for a new sharing profile + * which is prepopulated with the data from the sharing profile currently + * being edited. + */ + cloneSharingProfile(): void { + this.router.navigate( + ['manage', encodeURIComponent(this.selectedDataSource), 'sharingProfiles'], + { queryParams: { clone: this.identifier } } + ).then(() => window.scrollTo(0, 0)); + } + + /** + * Saves the current sharing profile, creating a new sharing profile or + * updating the existing sharing profile, returning a promise which is + * resolved if the save operation succeeds and rejected if the save + * operation fails. + * + * @returns + * An observable which completes if the save operation succeeds and is + * rejected with an {@link Error} if the save operation fails. + */ + saveSharingProfile(): Observable { + + this.sharingProfile!.parameters = this.parameters || undefined; + + // Save the sharing profile + return this.sharingProfileService.saveSharingProfile(this.selectedDataSource, this.sharingProfile!); + + } + + /** + * Deletes the current sharing profile, returning a promise which is + * resolved if the delete operation succeeds and rejected if the delete + * operation fails. + * + * @returns + * An observable which completes if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. + */ + deleteSharingProfile(): Observable { + return this.sharingProfileService.deleteSharingProfile(this.selectedDataSource, this.sharingProfile!); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.html new file mode 100644 index 0000000000..b9d92204ba --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.html @@ -0,0 +1,119 @@ + + + +
      +
      + +
      + + +
      +

      {{ 'MANAGE_USER_GROUP.SECTION_HEADER_EDIT_USER_GROUP' | transloco }}

      + +
      + + +
      + + +
      + + + + + + + + + +
      {{ 'MANAGE_USER_GROUP.FIELD_HEADER_USER_GROUP_NAME' | transloco }} + + {{ userGroup.identifier }} +
      {{ 'MANAGE_USER_GROUP.FIELD_HEADER_USER_GROUP_DISABLED' | transloco }}
      +
      + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.ts new file mode 100644 index 0000000000..532e5f56e8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user-group/manage-user-group.component.ts @@ -0,0 +1,568 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { catchError, forkJoin, from, map, Observable, of, switchMap } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { MembershipService } from '../../../rest/service/membership.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { UserGroupService } from '../../../rest/service/user-group.service'; +import { UserService } from '../../../rest/service/user.service'; +import { Form } from '../../../rest/types/Form'; +import { PermissionFlagSet } from '../../../rest/types/PermissionFlagSet'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { UserGroup } from '../../../rest/types/UserGroup'; +import { NonNullableProperties } from '../../../util/utility-types'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * The component for editing user groups. + */ +@Component({ + selector: 'guac-manage-user-group', + templateUrl: './manage-user-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManageUserGroupComponent implements OnInit { + + /** + * The identifier of the user group being edited. If a new user group is + * being created, this will not be defined. + */ + @Input({ alias: 'id' }) private identifier?: string; + + /** + * The unique identifier of the data source containing the user group being + * edited. + */ + @Input({ required: true }) dataSource!: string; + + /** + * The identifiers of all data sources currently available to the + * authenticated user. + */ + private dataSources: string[] = this.authenticationService.getAvailableDataSources(); + + /** + * The username of the current, authenticated user. + */ + private currentUsername: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The identifier of the original user group from which this user group is + * being cloned. Only valid if this is a new user group. + */ + private cloneSourceIdentifier: string | null = null; + + /** + * All user groups associated with the same identifier as the group being + * created or edited, as a map of data source identifier to the UserGroup + * object within that data source. + */ + userGroups: Record | null = null; + + /** + * The user group being modified. + */ + userGroup: UserGroup | null = null; + + /** + * All permissions associated with the user group being modified. + */ + permissionFlags: PermissionFlagSet | null = null; + + /** + * The set of permissions that will be added to the user group when the + * user group is saved. Permissions will only be present in this set if they + * are manually added, and not later manually removed before saving. + */ + permissionsAdded: PermissionSet = new PermissionSet(); + + /** + * The set of permissions that will be removed from the user group when the + * user group is saved. Permissions will only be present in this set if they + * are manually removed, and not later manually added before saving. + */ + permissionsRemoved: PermissionSet = new PermissionSet(); + + /** + * The identifiers of all user groups which can be manipulated (all groups + * for which the user accessing this interface has UPDATE permission), + * whether that means changing the members of those groups or changing the + * groups of which those groups are members. If this information has not + * yet been retrieved, this will be null. + */ + availableGroups: string[] | null = null; + + /** + * The identifiers of all users which can be manipulated (all users for + * which the user accessing this interface has UPDATE permission), either + * through adding those users as a member of the current group or removing + * those users from the current group. If this information has not yet been + * retrieved, this will be null. + */ + availableUsers: string[] | null = null; + + /** + * The identifiers of all user groups of which this group is a member, + * taking into account any user groups which will be added/removed when + * saved. If this information has not yet been retrieved, this will be + * null. + */ + parentGroups: string[] | null = null; + + /** + * The set of identifiers of all parent user groups to which this group + * will be added when saved. Parent groups will only be present in this set + * if they are manually added, and not later manually removed before + * saving. + */ + parentGroupsAdded: string[] = []; + + /** + * The set of identifiers of all parent user groups from which this group + * will be removed when saved. Parent groups will only be present in this + * set if they are manually removed, and not later manually added before + * saving. + */ + parentGroupsRemoved: string[] = []; + + /** + * The identifiers of all user groups which are members of this group, + * taking into account any user groups which will be added/removed when + * saved. If this information has not yet been retrieved, this will be + * null. + */ + memberGroups: string[] | null = null; + + /** + * The set of identifiers of all member user groups which will be added to + * this group when saved. Member groups will only be present in this set if + * they are manually added, and not later manually removed before saving. + */ + memberGroupsAdded: string[] = []; + + /** + * The set of identifiers of all member user groups which will be removed + * from this group when saved. Member groups will only be present in this + * set if they are manually removed, and not later manually added before + * saving. + */ + memberGroupsRemoved: string[] = []; + + /** + * The identifiers of all users which are members of this group, taking + * into account any users which will be added/removed when saved. If this + * information has not yet been retrieved, this will be null. + */ + memberUsers: string[] | null = null; + + /** + * The set of identifiers of all member users which will be added to this + * group when saved. Member users will only be present in this set if they + * are manually added, and not later manually removed before saving. + */ + memberUsersAdded: string[] = []; + + /** + * The set of identifiers of all member users which will be removed from + * this group when saved. Member users will only be present in this set if + * they are manually removed, and not later manually added before saving. + */ + memberUsersRemoved: string[] = []; + + /** + * For each applicable data source, the management-related actions that the + * current user may perform on the user group currently being created + * or modified, as a map of data source identifier to the + * {@link ManagementPermissions} object describing the actions available + * within that data source, or null if the current user's permissions have + * not yet been loaded. + */ + managementPermissions: Record | null = null; + + /** + * All available user group attributes. This is only the set of attribute + * definitions, organized as logical groupings of attributes, not attribute + * values. + */ + attributes: Form[] | null = null; + + /** + * The form group for editing user group attributes. + */ + attributesFormGroup: FormGroup = new FormGroup({}); + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private dataSourceService: DataSourceService, + private membershipService: MembershipService, + private permissionService: PermissionService, + private requestService: RequestService, + private schemaService: SchemaService, + private userGroupService: UserGroupService, + private userService: UserService, + private formService: FormService, + private router: Router, + private route: ActivatedRoute, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + this.cloneSourceIdentifier = this.route.snapshot.queryParamMap.get('clone'); + + // Populate interface with requested data + forkJoin([ + this.loadRequestedUserGroup(), + this.dataSourceService.apply((dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), this.dataSources, this.currentUsername), + this.userGroupService.getUserGroups(this.dataSource, [PermissionSet.ObjectPermissionType.UPDATE]), + this.userService.getUsers(this.dataSource, [PermissionSet.ObjectPermissionType.UPDATE]), + this.schemaService.getUserGroupAttributes(this.dataSource) + ]) + .subscribe({ + next : ([userGroupData, permissions, userGroups, users, attributes]) => { + + this.attributes = attributes; + + this.attributesFormGroup = this.formService.getFormGroup(this.attributes); + this.attributesFormGroup.valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + this.userGroup!.attributes = value; + }); + + this.managementPermissions = {}; + this.dataSources.forEach(dataSource => { + + // Determine whether data source contains this user group + const exists = (dataSource in (this.userGroups || {})); + + // Add the identifiers of all modifiable user groups + this.availableGroups = []; + for (const groupIdentifier in userGroups) { + const userGroup = userGroups[groupIdentifier]; + this.availableGroups.push(userGroup.identifier!); + } + + // Add the identifiers of all modifiable users + this.availableUsers = []; + for (const username in users) { + const user = users[username]; + this.availableUsers.push(user.username); + } + + // Calculate management actions available for this specific group + this.managementPermissions![dataSource] = ManagementPermissions.fromPermissionSet( + permissions[dataSource], + PermissionSet.SystemPermissionType.CREATE_USER_GROUP, + PermissionSet.hasUserGroupPermission, + exists ? this.identifier : undefined); + + }); + + }, error: this.requestService.WARN + }); + + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user group interface to + * be useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + + return this.userGroups !== null + && this.userGroup !== null + && this.permissionFlags !== null + && this.managementPermissions !== null + && this.availableGroups !== null + && this.availableUsers !== null + && this.parentGroups !== null + && this.memberGroups !== null + && this.memberUsers !== null + && this.attributes !== null; + + } + + /** + * Returns whether the current user can edit the identifier of the user + * group being edited. + * + * @returns + * true if the current user can edit the identifier of the user group + * being edited, false otherwise. + */ + canEditIdentifier(): boolean { + return !this.identifier; + } + + /** + * Loads the data required for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing user group, cloning an existing + * user group, or creating an entirely new user group. + * + * @returns + * An observable which completes when the interface has been prepared + * for performing the requested management task. + */ + loadRequestedUserGroup(): Observable { + + // Pull user group data and permissions if we are editing an existing + // user group + if (this.identifier) + return this.loadExistingUserGroup(this.dataSource, this.identifier); + + // If we are cloning an existing user group, pull its data instead + if (this.cloneSourceIdentifier) + return this.loadClonedUserGroup(this.dataSource, this.cloneSourceIdentifier); + + // If we are creating a new user group, populate skeleton user group data + return this.loadSkeletonUserGroup(); + + } + + /** + * Loads the data associated with the user group having the given + * identifier, preparing the interface for making modifications to that + * existing user group. + * + * @param dataSource + * The unique identifier of the data source containing the user group + * to load. + * + * @param identifier + * The unique identifier of the user group to load. + * + * @returns + * An observable which completes when the interface has been prepared for + * editing the given user group. + */ + private loadExistingUserGroup(dataSource: string, identifier: string): Observable { + const userGroups = from(this.dataSourceService.apply( + (dataSource: string, identifier: string) => this.userGroupService.getUserGroup(dataSource, identifier), + this.dataSources, + identifier)); + + // Use empty permission set if group cannot be found + const permissions = + this.permissionService.getPermissions(this.dataSource, this.identifier!, true) + .pipe(catchError(this.requestService.defaultValue(new PermissionSet()))); + + // Assume no parent groups if group cannot be found + const parentGroups = + this.membershipService.getUserGroups(this.dataSource, this.identifier!, true) + .pipe(catchError(this.requestService.defaultValue([]))); + + // Assume no member groups if group cannot be found + const memberGroups = + this.membershipService.getMemberUserGroups(this.dataSource, this.identifier!) + .pipe(catchError(this.requestService.defaultValue([]))); + + // Assume no member users if group cannot be found + const memberUsers = + this.membershipService.getMemberUsers(this.dataSource, this.identifier!) + .pipe(catchError(this.requestService.defaultValue([]))); + + return forkJoin([userGroups, permissions, parentGroups, memberGroups, memberUsers]) + .pipe( + map(([userGroups, permissions, parentGroups, memberGroups, memberUsers]) => { + + this.userGroups = userGroups; + this.parentGroups = parentGroups; + this.memberGroups = memberGroups; + this.memberUsers = memberUsers; + + // Create skeleton user group if user group does not exist + this.userGroup = userGroups[dataSource] || new UserGroup({ + 'identifier': identifier + }); + + this.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + + }) + ); + } + + /** + * Loads the data associated with the user group having the given + * identifier, preparing the interface for cloning that existing user + * group. + * + * @param dataSource + * The unique identifier of the data source containing the user group to + * be cloned. + * + * @param identifier + * The unique identifier of the user group being cloned. + * + * @returns {Promise} + * An observable which completes when the interface has been prepared for + * cloning the given user group. + */ + private loadClonedUserGroup(dataSource: string, identifier: string): Observable { + return forkJoin([ + this.dataSourceService.apply((dataSource: string, identifier: string) => + this.userGroupService.getUserGroup(dataSource, identifier), [dataSource], identifier), + this.permissionService.getPermissions(dataSource, identifier, true), + this.membershipService.getUserGroups(dataSource, identifier, true), + this.membershipService.getMemberUserGroups(dataSource, identifier), + this.membershipService.getMemberUsers(dataSource, identifier) + ]) + .pipe( + map(([userGroups, permissions, parentGroups, memberGroups, memberUsers]) => { + this.userGroups = {}; + this.userGroup = userGroups[dataSource]; + this.parentGroups = parentGroups; + this.parentGroupsAdded = parentGroups; + this.memberGroups = memberGroups; + this.memberGroupsAdded = memberGroups; + this.memberUsers = memberUsers; + this.memberUsersAdded = memberUsers; + + this.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + this.permissionsAdded = permissions; + + }) + ); + } + + /** + * Loads skeleton user group data, preparing the interface for creating a + * new user group. + * + * @returns + * An observable which completes when the interface has been prepared for + * creating a new user group. + */ + private loadSkeletonUserGroup(): Observable { + + // No user groups exist regardless of data source if the user group is + // being created + this.userGroups = {}; + + // Use skeleton user group object with no associated permissions + this.userGroup = new UserGroup(); + this.parentGroups = []; + this.memberGroups = []; + this.memberUsers = []; + this.permissionFlags = new PermissionFlagSet(); + + return of(void (0)); + + } + + /** + * Returns the URL for the page which manages the user group currently + * being edited under the given data source. The given data source need not + * be the same as the data source currently selected. + * + * @param dataSource + * The unique identifier of the data source that the URL is being + * generated for. + * + * @returns + * The URL for the page which manages the user group currently being + * edited under the given data source. + */ + getUserGroupURL(dataSource: string): string { + return '/manage/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(this.identifier || ''); + } + + /** + * Cancels all pending edits, returning to the main list of user groups. + */ + returnToUserGroupList(): void { + this.router.navigate(['/settings/userGroups']); + } + + /** + * Cancels all pending edits, opening an edit page for a new user group + * which is prepopulated with the data from the user currently being edited. + */ + cloneUserGroup(): void { + this.router.navigate( + ['manage', encodeURIComponent(this.dataSource), 'userGroups'], + { queryParams: { clone: this.identifier } } + ).then(() => window.scrollTo(0, 0)); + } + + /** + * Saves the current user group, creating a new user group or updating the + * existing user group depending on context, returning a promise which is + * resolved if the save operation succeeds and rejected if the save + * operation fails. + * + * @returns + * An observable which completes if the save operation succeeds and is + * fails with an {@link Error} if the save operation fails. + */ + saveUserGroup(): Observable { + + // Save or create the user group, depending on whether the user group exists + let saveUserGroup$: Observable; + if (this.dataSource in (this.userGroups || {})) + saveUserGroup$ = this.userGroupService.saveUserGroup(this.dataSource, this.userGroup!); + else + saveUserGroup$ = this.userGroupService.createUserGroup(this.dataSource, this.userGroup!); + + return saveUserGroup$ + .pipe( + switchMap(() => { + + return forkJoin([ + this.permissionService.patchPermissions(this.dataSource, this.userGroup!.identifier!, this.permissionsAdded, this.permissionsRemoved, true), + this.membershipService.patchUserGroups(this.dataSource, this.userGroup!.identifier!, this.parentGroupsAdded, this.parentGroupsRemoved, true), + this.membershipService.patchMemberUserGroups(this.dataSource, this.userGroup!.identifier!, this.memberGroupsAdded, this.memberGroupsRemoved), + this.membershipService.patchMemberUsers(this.dataSource, this.userGroup!.identifier!, this.memberUsersAdded, this.memberUsersRemoved) + ]) + .pipe(map(() => void (0))); + }) + ); + + } + + /** + * Deletes the current user group, returning a promise which is resolved if + * the delete operation succeeds and rejected if the delete operation + * fails. + * + * @returns + * An observable which completes if the delete operation succeeds and fails + * with an {@link Error} if the delete operation fails. + */ + deleteUserGroup(): Observable { + return this.userGroupService.deleteUserGroup(this.dataSource, this.userGroup!); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.html new file mode 100644 index 0000000000..ede4bc123a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.html @@ -0,0 +1,118 @@ + + +
      + + + + +
      + + +
      +

      {{ 'MANAGE_USER.SECTION_HEADER_EDIT_USER' | transloco }}

      + +
      + + + + +
      +

      {{ 'MANAGE_USER.INFO_READ_ONLY' | transloco }}

      +
      + + +
      + + +
      + + + + + + + + + + + + + + + + + +
      {{ 'MANAGE_USER.FIELD_HEADER_USERNAME' | transloco }} + + {{ user.username }} +
      {{ 'MANAGE_USER.FIELD_HEADER_PASSWORD' | transloco }}
      {{ 'MANAGE_USER.FIELD_HEADER_PASSWORD_AGAIN' | transloco }}
      {{ 'MANAGE_USER.FIELD_HEADER_USER_DISABLED' | transloco }}
      +
      + + +
      + +
      + + + + + + + + + + + + + + + + + +
      + +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.ts new file mode 100644 index 0000000000..b1b86866d6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/manage-user/manage-user.component.ts @@ -0,0 +1,581 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient } from '@angular/common/http'; +import { Component, DestroyRef, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; +import { catchError, forkJoin, map, Observable, of, switchMap, throwError } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { MembershipService } from '../../../rest/service/membership.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { UserGroupService } from '../../../rest/service/user-group.service'; +import { UserService } from '../../../rest/service/user.service'; +import { Error } from '../../../rest/types/Error'; +import { Form } from '../../../rest/types/Form'; +import { PermissionFlagSet } from '../../../rest/types/PermissionFlagSet'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { User } from '../../../rest/types/User'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +@Component({ + selector: 'guac-manage-user', + templateUrl: './manage-user.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManageUserComponent implements OnInit { + + + /** + * The unique identifier of the data source containing the user being + * edited. + */ + @Input() dataSource!: string; + + /** + * The username of the user being edited. If a new user is + * being created, this will not be defined. + */ + @Input('id') username?: string; + + /** + * The identifiers of all data sources currently available to the + * authenticated user. + */ + private dataSources: string[] = []; + + /** + * The username of the current, authenticated user. + */ + private currentUsername: string | null = null; + + /** + * The username of the original user from which this user is + * being cloned. Only valid if this is a new user. + */ + cloneSourceUsername: string | null = null; + + /** + * The string value representing the user currently being edited within the + * permission flag set. Note that his may not match the user's actual + * username - it is a marker that is (1) guaranteed to be associated with + * the current user's permissions in the permission set and (2) guaranteed + * not to collide with any user that does not represent the current user + * within the permission set. + */ + selfUsername = ''; + + /** + * All user accounts associated with the same username as the account being + * created or edited, as a map of data source identifier to the User object + * within that data source. + */ + users: Record | null = null; + + /** + * The user being modified. + */ + user: User | null = null; + + /** + * The form group describing the attributes of the user being modified. + */ + userAttributes: FormGroup = new FormGroup({}); + + /** + * All permissions associated with the user being modified. + */ + permissionFlags: PermissionFlagSet | null = null; + + /** + * The set of permissions that will be added to the user when the user is + * saved. Permissions will only be present in this set if they are + * manually added, and not later manually removed before saving. + */ + permissionsAdded: PermissionSet = new PermissionSet(); + + /** + * The set of permissions that will be removed from the user when the user + * is saved. Permissions will only be present in this set if they are + * manually removed, and not later manually added before saving. + */ + permissionsRemoved: PermissionSet = new PermissionSet(); + + /** + * The identifiers of all user groups which can be manipulated (all groups + * for which the user accessing this interface has UPDATE permission), + * either through adding the current user as a member or removing the + * current user from that group. If this information has not yet been + * retrieved, this will be null. + */ + availableGroups: string[] | null = null; + + /** + * The identifiers of all user groups of which the user is a member, + * taking into account any user groups which will be added/removed when + * saved. If this information has not yet been retrieved, this will be + * null. + */ + parentGroups: string[] | null = null; + + /** + * The set of identifiers of all parent user groups to which the user will + * be added when saved. Parent groups will only be present in this set if + * they are manually added, and not later manually removed before saving. + */ + parentGroupsAdded: string[] = []; + + /** + * The set of identifiers of all parent user groups from which the user + * will be removed when saved. Parent groups will only be present in this + * set if they are manually removed, and not later manually added before + * saving. + */ + parentGroupsRemoved: string[] = []; + + /** + * For each applicable data source, the management-related actions that the + * current user may perform on the user account currently being created + * or modified, as a map of data source identifier to the + * {@link ManagementPermissions} object describing the actions available + * within that data source, or null if the current user's permissions have + * not yet been loaded. + */ + managementPermissions: Record | null = null; + + /** + * All available user attributes. This is only the set of attribute + * definitions, organized as logical groupings of attributes, not attribute + * values. + */ + attributes: Form[] | null = null; + + /** + * The password match for the user. + */ + passwordMatch?: string = undefined; + + constructor(private router: Router, + private route: ActivatedRoute, + private http: HttpClient, + private authenticationService: AuthenticationService, + private dataSourceService: DataSourceService, + private membershipService: MembershipService, + private permissionService: PermissionService, + private requestService: RequestService, + private schemaService: SchemaService, + private userGroupService: UserGroupService, + private userService: UserService, + private formService: FormService, + private destroyRef: DestroyRef + ) { + } + + ngOnInit(): void { + this.dataSources = this.authenticationService.getAvailableDataSources(); + this.currentUsername = this.authenticationService.getCurrentUsername(); + this.cloneSourceUsername = this.route.snapshot.queryParamMap.get('clone'); + + // Populate interface with requested data + const userData = this.loadRequestedUser(); + const permissions = this.dataSourceService.apply( + (ds: string, username: string) => this.permissionService.getEffectivePermissions(ds, username), + this.dataSources, + this.currentUsername + ); + const userGroups = this.userGroupService.getUserGroups(this.dataSource, [PermissionSet.ObjectPermissionType.UPDATE]); + const attributes = this.schemaService.getUserAttributes(this.dataSource); + + forkJoin([userData, permissions, userGroups, attributes]) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + next : ([userData, permissions, userGroups, attributes]) => { + + this.attributes = attributes; + + // Populate form with user attributes + this.createUserAttributesFormGroup(); + + this.managementPermissions = {}; + + // add account page + this.dataSources.forEach(dataSource => { + + // Determine whether data source contains this user + const exists = (dataSource in (this.users || {})); + + // Add the identifiers of all modifiable user groups + const availableGroups: string[] = []; + for (const groupIdentifier in userGroups) { + const userGroup = userGroups[groupIdentifier]; + availableGroups.push(userGroup.identifier!); + } + this.availableGroups = availableGroups; + + // Calculate management actions available for this specific account + if (this.managementPermissions) + this.managementPermissions[dataSource] = ManagementPermissions.fromPermissionSet( + permissions[dataSource], + PermissionSet.SystemPermissionType.CREATE_USER, + PermissionSet.hasUserPermission, + exists ? this.username : undefined); + + }); + + }, error: this.requestService.DIE + }); + } + + /** + * Creates the form group that is passed to the guac-form component to allow the + * editing of the user's attributes. This form group is populated with the user's + * current attribute values. The user's attributes are updated when the form group + * values change. + */ + private createUserAttributesFormGroup() { + if (!this.attributes) + return; + + // Get a form group which allows editing of the user's attributes + this.userAttributes = this.formService.getFormGroup(this.attributes); + + // Populate the form group with the user's current attribute values + this.userAttributes.patchValue(this.user!.attributes); + + // Update the user's attributes when the form group is updated + this.userAttributes.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + if (this.user) + this.user.attributes = value; + }); + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): boolean { + + return this.users !== null + && this.permissionFlags !== null + && this.managementPermissions !== null + && this.availableGroups !== null + && this.parentGroups !== null + && this.attributes !== null; + + } + + /** + * Returns whether the current user can edit the username of the user being + * edited within the given data source. + * + * @param dataSource + * The identifier of the data source to check. If omitted, this will + * default to the currently-selected data source. + * + * @returns + * true if the current user can edit the username of the user being + * edited, false otherwise. + */ + canEditUsername(dataSource?: string): boolean { + return !this.username; + } + + /** + * Loads the data associated with the user having the given username, + * preparing the interface for making modifications to that existing user. + * + * @param dataSource + * The unique identifier of the data source containing the user to + * load. + * + * @param username + * The username of the user to load. + * + * @returns + * An observable that completes when the interface has been prepared for + * editing the given user. + */ + private loadExistingUser(dataSource: string, username: string): Observable { + + const users = this.dataSourceService.apply( + (ds: string, username: string) => this.userService.getUser(ds, username), + this.dataSources, + username + ); + + // Use empty permission set if user cannot be found + const permissions = this.permissionService.getPermissions(dataSource, username) + .pipe( + catchError(this.requestService.defaultValue(new PermissionSet())) + ); + + // Assume no parent groups if user cannot be found + const parentGroups = this.membershipService.getUserGroups(dataSource, username) + .pipe( + catchError(this.requestService.defaultValue([])) + ); + + + return forkJoin([users, permissions, parentGroups]) + .pipe( + map(([users, permissions, parentGroups]) => { + + this.users = users; + this.parentGroups = parentGroups; + + // Create skeleton user if uster does not exist + this.user = users[dataSource] || new User({ + 'username': username + }); + + // The current user will be associated with username of the existing + // user in the retrieved permission set + this.selfUsername = username; + this.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + + }) + ); + } + + /** + * Loads the data associated with the user having the given username, + * preparing the interface for cloning that existing user. + * + * @param dataSource + * The unique identifier of the data source containing the user to + * be cloned. + * + * @param username + * The username of the user being cloned. + * + * @returns + * An observable that completes when the interface has been prepared for + * cloning the given user. + */ + private loadClonedUser(dataSource: string, username: string): Observable { + + const users = this.dataSourceService.apply( + (ds: string, username: string) => this.userService.getUser(ds, username), + [dataSource], + username + ); + + const permissions = this.permissionService.getPermissions(dataSource, username); + const parentGroups = this.membershipService.getUserGroups(dataSource, username); + + + return forkJoin([users, permissions, parentGroups]) + .pipe( + map(([users, permissions, parentGroups]) => { + + this.users = {}; + this.user = users[dataSource]; + this.parentGroups = parentGroups; + this.parentGroupsAdded = parentGroups; + + // The current user will be associated with cloneSourceUsername in the + // retrieved permission set + this.selfUsername = username; + this.permissionFlags = PermissionFlagSet.fromPermissionSet(permissions); + this.permissionsAdded = permissions; + + }) + ); + } + + /** + * Loads skeleton user data, preparing the interface for creating a new + * user. + * + * @returns + * An observable that completes when the interface has been prepared for + * creating a new user. + */ + private loadSkeletonUser(): Observable { + + // No users exist regardless of data source if there is no username + this.users = {}; + + // Use skeleton user object with no associated permissions + this.user = new User(); + this.parentGroups = []; + this.permissionFlags = new PermissionFlagSet(); + + // As no permissions are yet associated with the user, it is safe to + // use any non-empty username as a placeholder for self-referential + // permissions + this.selfUsername = 'SELF'; + + return of(void (0)); + + } + + /** + * Loads the data required for performing the management task requested + * through the route parameters given at load time, automatically preparing + * the interface for editing an existing user, cloning an existing user, or + * creating an entirely new user. + * + * @returns + * An observable that completes when the interface has been prepared + * for performing the requested management task. + */ + loadRequestedUser(): Observable { + + // Pull user data and permissions if we are editing an existing user + if (this.username) + return this.loadExistingUser(this.dataSource, this.username); + + // If we are cloning an existing user, pull his/her data instead + if (this.cloneSourceUsername) + return this.loadClonedUser(this.dataSource, this.cloneSourceUsername); + + // If we are creating a new user, populate skeleton user data + return this.loadSkeletonUser(); + + } + + /** + * Returns the URL for the page which manages the user account currently + * being edited under the given data source. The given data source need not + * be the same as the data source currently selected. + * + * @param dataSource + * The unique identifier of the data source that the URL is being + * generated for. + * + * @returns + * The URL for the page which manages the user account currently being + * edited under the given data source. + */ + getUserURL(dataSource: string): string { + return '/manage/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(this.username || ''); + } + + /** + * Cancels all pending edits, returning to the main list of users. + */ + returnToUserList(): void { + this.router.navigate(['/settings/users']); + } + + /** + * Cancels all pending edits, opening an edit page for a new user + * which is prepopulated with the data from the user currently being edited. + */ + cloneUser(): void { + this.router.navigate( + ['manage', encodeURIComponent(this.dataSource), 'users'], + { queryParams: { clone: this.username } } + ).then(() => window.scrollTo(0, 0)); + } + + /** + * Saves the current user, creating a new user or updating the existing + * user depending on context, returning an observable that completes if the + * save operation succeeds and rejected if the save operation fails. + * + * @returns + * An observable that completes if the save operation succeeds and fails + * with an {@link Error} if the save operation fails. + */ + saveUser(): Observable { + + // Verify passwords match + if (this.passwordMatch !== this.user?.password) { + return throwError(() => new Error({ + translatableMessage: { + key: 'MANAGE_USER.ERROR_PASSWORD_MISMATCH' + } + })); + } + + // Save or create the user, depending on whether the user exists + let saveUserPromise; + if (this.dataSource in (this.users || {})) + saveUserPromise = this.userService.saveUser(this.dataSource, this.user!); + else + saveUserPromise = this.userService.createUser(this.dataSource, this.user!); + + return saveUserPromise.pipe( + switchMap(() => { + + // Move permission flags if username differs from marker + if (this.selfUsername !== this.user!.username) { + + // Rename added permission + if (this.permissionsAdded.userPermissions[this.selfUsername]) { + this.permissionsAdded.userPermissions[this.user!.username] = this.permissionsAdded.userPermissions[this.selfUsername]; + delete this.permissionsAdded.userPermissions[this.selfUsername]; + } + + // Rename removed permission + if (this.permissionsRemoved.userPermissions[this.selfUsername]) { + this.permissionsRemoved.userPermissions[this.user!.username] = this.permissionsRemoved.userPermissions[this.selfUsername]; + delete this.permissionsRemoved.userPermissions[this.selfUsername]; + } + + } + + // Upon success, save any changed permissions/groups + return forkJoin([ + this.permissionService.patchPermissions(this.dataSource, this.user!.username, this.permissionsAdded, this.permissionsRemoved), + this.membershipService.patchUserGroups(this.dataSource, this.user!.username, this.parentGroupsAdded, this.parentGroupsRemoved) + ]); + + }), + + // Map [void, void] from forkJoin to a simple void + map(() => void (0)) + ); + + } + + /** + * Deletes the current user, returning an observable that completes if the + * delete operation succeeds and rejected if the delete operation fails. + * + * @returns + * An observable that completes if the delete operation succeeds and is + * rejected with an {@link Error} if the delete operation fails. + */ + deleteUser(): Observable { + + if (this.user !== null) { + return this.userService.deleteUser(this.dataSource, this.user); + } + + return of(void (0)); + } + + modelOnly(): boolean { + return !this.managementPermissions?.[this.dataSource].canChangeAllAttributes; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.html new file mode 100644 index 0000000000..668341b707 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.html @@ -0,0 +1,31 @@ + + +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.ts new file mode 100644 index 0000000000..70717b140f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/management-buttons/management-buttons.component.ts @@ -0,0 +1,193 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { Observable } from 'rxjs'; +import { MenuAction } from '../../../navigation/types/MenuAction'; +import { GuacNotificationService } from '../../../notification/services/guac-notification.service'; +import { ManagementPermissions } from '../../types/ManagementPermissions'; + +/** + * Component which displays a set of object management buttons (save, delete, + * clone, etc.) representing the actions available to the current user in + * context of the object being edited/created. + */ +@Component({ + selector: 'management-buttons', + templateUrl: './management-buttons.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ManagementButtonsComponent implements OnInit { + + /** + * The translation namespace associated with all applicable + * translation strings. This directive requires at least the + * following translation strings within the given namespace: + * + * - ACTION_CANCEL + * - ACTION_CLONE + * - ACTION_DELETE + * - ACTION_SAVE + * - DIALOG_HEADER_CONFIRM_DELETE + * - TEXT_CONFIRM_DELETE + */ + @Input({ required: true }) namespace!: string; + + /** + * The permissions which dictate the management actions available + * to the current user. + */ + @Input({ required: true }) permissions!: ManagementPermissions; + + /** + * The function to invoke to save the arbitrary object being edited + * if the current user has permission to do so. The provided + * function MUST return an observable which completes if the save + * operation succeeds and fail with an {@link Error} if the + * save operation fails. + */ + @Input({ required: true }) save!: () => Observable; + + /** + * The function to invoke when the current user chooses to clone + * the object being edited. The provided function MUST perform the + * actions necessary to produce an interface which will clone the + * object. + */ + @Input({ required: true }) clone!: () => void; + + /** + * The function to invoke to delete the arbitrary object being edited + * if the current user has permission to do so. The provided + * function MUST return a promise which is resolved if the delete + * operation succeeds and is rejected with an {@link Error} if the + * delete operation fails. + */ + @Input({ required: true }) delete!: () => Observable; + + /** + * The function to invoke when the current user chooses to cancel + * the edit in progress, or when a save/delete operation has + * succeeded. The provided function MUST perform the actions + * necessary to return the user to a reasonable starting point. + */ + @Input({ required: true }) return!: () => void; + + /** + * An action to be provided along with the object sent to showStatus which + * immediately deletes the current connection. + */ + private DELETE_ACTION!: MenuAction; + + /** + * An action to be provided along with the object sent to showStatus which + * closes the currently-shown status dialog. + */ + private CANCEL_ACTION!: MenuAction; + + /** + * Inject required services. + */ + constructor(private notificationService: GuacNotificationService) { + } + + ngOnInit(): void { + + this.DELETE_ACTION = { + name : this.namespace + '.ACTION_DELETE', + className: 'danger', + callback : () => { + this.deleteObjectImmediately(); + this.notificationService.showStatus(false); + } + }; + + this.CANCEL_ACTION = { + name : this.namespace + '.ACTION_CANCEL', + callback: () => { + this.notificationService.showStatus(false); + } + }; + + } + + /** + * Invokes the provided return function to navigate the user back to + * the page they started from. + */ + navigateBack(): void { + this.return(); + } + + /** + * Invokes the provided delete function, immediately deleting the + * current object without prompting the user for confirmation. If + * deletion is successful, the user is navigated back to the page they + * started from. If the deletion fails, an error notification is + * displayed. + */ + private deleteObjectImmediately(): void { + this.delete().subscribe({ + next : () => this.navigateBack(), + error: this.notificationService.SHOW_REQUEST_ERROR + }); + } + + /** + * Cancels all pending edits, invoking the provided clone function to + * open an edit page for a new object which is prepopulated with the + * data from the current object. + */ + cloneObject(): void { + this.clone(); + } + + /** + * Invokes the provided save function to save the current object. If + * saving is successful, the user is navigated back to the page they + * started from. If saving fails, an error notification is displayed. + */ + saveObject(): void { + this.save().subscribe({ + next : () => this.navigateBack(), + error: this.notificationService.SHOW_REQUEST_ERROR + }); + } + + /** + * Deletes the current object, prompting the user first to confirm that + * deletion is desired. If the user confirms that deletion is desired, + * the object is deleted through invoking the provided delete function. + * The user is automatically navigated back to the page they started + * from or given an error notification depending on whether deletion + * succeeds. + */ + deleteObject(): void { + + // Confirm deletion request + this.notificationService.showStatus({ + title : this.namespace + '.DIALOG_HEADER_CONFIRM_DELETE', + text : { key: this.namespace + '.TEXT_CONFIRM_DELETE' }, + actions: [this.DELETE_ACTION, this.CANCEL_ACTION] + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.html new file mode 100644 index 0000000000..52311503ab --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.html @@ -0,0 +1,33 @@ + + + diff --git a/guacamole/src/main/frontend/src/app/home/types/RecentConnection.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/home/types/RecentConnection.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.ts index 149d71f932..be9424ed54 100644 --- a/guacamole/src/main/frontend/src/app/home/types/RecentConnection.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/sharing-profile-permission/sharing-profile-permission.component.ts @@ -17,36 +17,30 @@ * under the License. */ +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { ConnectionListContext } from '../../types/ConnectionListContext'; + /** - * Provides the RecentConnection class used by the guacRecentConnections - * directive. + * A component which displays a sharing profile for a specific + * connection and allows manipulation of the sharing profile permissions. */ -angular.module('home').factory('RecentConnection', [function defineRecentConnection() { +@Component({ + selector: 'guac-sharing-profile-permission', + templateUrl: './sharing-profile-permission.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class SharingProfilePermissionComponent { /** - * A recently-user connection, visible to the current user, with an - * associated history entry. - * - * @constructor + * TODO */ - var RecentConnection = function RecentConnection(name, entry) { - - /** - * The human-readable name of this connection. - * - * @type String - */ - this.name = name; - - /** - * The history entry associated with this recent connection. - * - * @type HistoryEntry - */ - this.entry = entry; + @Input({ required: true }) context!: ConnectionListContext; - }; - - return RecentConnection; + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.html new file mode 100644 index 0000000000..cd6b5ccc18 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.html @@ -0,0 +1,37 @@ + + +
      +

      {{ 'MANAGE_USER.SECTION_HEADER_PERMISSIONS' | transloco }}

      +
      + + + + + + + + + +
      {{ systemPermissionType.label | transloco }}
      {{ 'MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD' | transloco }}
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.ts new file mode 100644 index 0000000000..df3b6f5b22 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/components/system-permission-editor/system-permission-editor.component.ts @@ -0,0 +1,308 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { PermissionFlagSet } from '../../../rest/types/PermissionFlagSet'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; + +/** + * A directive for manipulating the system permissions granted within a given + * {@link PermissionFlagSet}, tracking the specific permissions added or + * removed within a separate pair of {@link PermissionSet} objects. Optionally, + * the permission for a particular user to update themselves (change their own + * password/attributes) may also be manipulated. + */ +@Component({ + selector: 'system-permission-editor', + templateUrl: './system-permission-editor.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class SystemPermissionEditorComponent implements OnInit { + + /** + * The unique identifier of the data source associated with the + * permissions being manipulated. + */ + @Input({ required: true }) dataSource!: string; + + /** + * The username of the user whose self-update permission (whether + * the user has permission to update their own user account) should + * be additionally controlled by this editor. If no such user + * permissions should be controlled, this should be left undefined. + */ + @Input() username?: string; + + /** + * The current state of the permissions being manipulated. This + * {@link PermissionFlagSet} will be modified as changes are made + * through this permission editor. + */ + @Input({ required: true }) permissionFlags!: PermissionFlagSet; + + /** + * The set of permissions that have been added, relative to the + * initial state of the permissions being manipulated. + */ + @Input({ required: true }) permissionsAdded!: PermissionSet; + + /** + * The set of permissions that have been removed, relative to the + * initial state of the permissions being manipulated. + */ + @Input({ required: true }) permissionsRemoved!: PermissionSet; + + /** + * The identifiers of all data sources currently available to the + * authenticated user. + */ + private dataSources: string[] = []; + + /** + * The username of the current, authenticated user. + */ + private currentUsername: string | null = null; + + /** + * The permissions granted to the currently-authenticated user. + */ + private permissions: Record | null = null; + + + /** + * Available system permission types, as translation string / internal + * value pairs. + */ + systemPermissionTypes: { label: string; value: PermissionSet.SystemPermissionType }[] = [ + { + label: 'MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM', + value: PermissionSet.SystemPermissionType.ADMINISTER + }, + { + label: 'MANAGE_USER.FIELD_HEADER_AUDIT_SYSTEM', + value: PermissionSet.SystemPermissionType.AUDIT + }, + { + label: 'MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS', + value: PermissionSet.SystemPermissionType.CREATE_USER + }, + { + label: 'MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS', + value: PermissionSet.SystemPermissionType.CREATE_USER_GROUP + }, + { + label: 'MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS', + value: PermissionSet.SystemPermissionType.CREATE_CONNECTION + }, + { + label: 'MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS', + value: PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP + }, + { + label: 'MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES', + value: PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE + } + ]; + + + constructor(private authenticationService: AuthenticationService, + private dataSourceService: DataSourceService, + private permissionService: PermissionService, + private requestService: RequestService) { + } + + ngOnInit(): void { + this.dataSources = this.authenticationService.getAvailableDataSources(); + this.currentUsername = this.authenticationService.getCurrentUsername(); + + // Query the permissions granted to the currently-authenticated user + this.dataSourceService.apply( + (dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), + this.dataSources, + this.currentUsername + ) + .then(permissions => { + this.permissions = permissions; + }, this.requestService.PROMISE_DIE); + + + } + + /** + * Returns whether the current user has permission to change the system + * permissions granted to users. + * + * @returns + * true if the current user can grant or revoke system permissions + * to the permission set being edited, false otherwise. + */ + canChangeSystemPermissions(): boolean { + + // Do not check if permissions are not yet loaded + if (!this.permissions) + return false; + + // Only the administrator can modify system permissions + return PermissionSet.hasSystemPermission(this.permissions[this.dataSource], + PermissionSet.SystemPermissionType.ADMINISTER); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given system permission. + * + * @param type + * The system permission to add, as defined by + * PermissionSet.SystemPermissionType. + */ + addSystemPermission(type: PermissionSet.SystemPermissionType): void { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasSystemPermission(this.permissionsRemoved, type)) + PermissionSet.removeSystemPermission(this.permissionsRemoved, type); + + // Otherwise, explicitly add the permission + else + PermissionSet.addSystemPermission(this.permissionsAdded, type); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given system permission. + * + * @param type + * The system permission to remove, as defined by + * PermissionSet.SystemPermissionType. + */ + removeSystemPermission(type: PermissionSet.SystemPermissionType): void { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasSystemPermission(this.permissionsAdded, type)) + PermissionSet.removeSystemPermission(this.permissionsAdded, type); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addSystemPermission(this.permissionsRemoved, type); + + } + + /** + * Notifies the controller that a change has been made to the given + * system permission for the permission set being edited. + * + * @param type + * The system permission that was changed, as defined by + * PermissionSet.SystemPermissionType. + */ + systemPermissionChanged(type: PermissionSet.SystemPermissionType): void { + + // Determine current permission setting + const granted = this.permissionFlags.systemPermissions[type]; + + // Add/remove permission depending on flag state + if (granted) + this.addSystemPermission(type); + else + this.removeSystemPermission(type); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the addition of the given user permission. + * + * @param type + * The user permission to add, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param identifier + * The identifier of the user affected by the permission being added. + */ + addUserPermission(type: PermissionSet.ObjectPermissionType, identifier: string): void { + + // If permission was previously removed, simply un-remove it + if (PermissionSet.hasUserPermission(this.permissionsRemoved, type, identifier)) + PermissionSet.removeUserPermission(this.permissionsRemoved, type, identifier); + + // Otherwise, explicitly add the permission + else + PermissionSet.addUserPermission(this.permissionsAdded, type, identifier); + + } + + /** + * Updates the permissionsAdded and permissionsRemoved permission sets + * to reflect the removal of the given user permission. + * + * @param type + * The user permission to remove, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param identifier + * The identifier of the user affected by the permission being + * removed. + */ + removeUserPermission(type: PermissionSet.ObjectPermissionType, identifier: string): void { + + // If permission was previously added, simply un-add it + if (PermissionSet.hasUserPermission(this.permissionsAdded, type, identifier)) + PermissionSet.removeUserPermission(this.permissionsAdded, type, identifier); + + // Otherwise, explicitly remove the permission + else + PermissionSet.addUserPermission(this.permissionsRemoved, type, identifier); + + } + + /** + * Notifies the controller that a change has been made to the given user + * permission for the permission set being edited. + * + * @param type + * The user permission that was changed, as defined by + * PermissionSet.ObjectPermissionType. + * + * @param identifier + * The identifier of the user affected by the changed permission. + */ + userPermissionChanged(type: PermissionSet.ObjectPermissionType, identifier: string): void { + + // Determine current permission setting + const granted = this.permissionFlags.userPermissions[type][identifier]; + + // Add/remove permission depending on flag state + if (granted) + this.addUserPermission(type, identifier); + else + this.removeUserPermission(type, identifier); + + } + + /** + * Make the ObjectPermissionType enum available to the template. + */ + protected readonly ObjectPermissionType = PermissionSet.ObjectPermissionType; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/manage.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/manage.module.ts new file mode 100644 index 0000000000..3b7a8e61db --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/manage.module.ts @@ -0,0 +1,100 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslocoModule } from '@ngneat/transloco'; +import { FormModule } from '../form/form.module'; +import { GroupListModule } from '../group-list/group-list.module'; +import { ListModule } from '../list/list.module'; +import { NavigationModule } from '../navigation/navigation.module'; +import { OrderByPipe } from '../util/order-by.pipe'; +import { + ConnectionGroupPermissionComponent +} from './components/connection-group-permission/connection-group-permission.component'; +import { + ConnectionPermissionEditorComponent +} from './components/connection-permission-editor/connection-permission-editor.component'; +import { ConnectionPermissionComponent } from './components/connection-permission/connection-permission.component'; +import { DataSourceTabsComponent } from './components/data-source-tabs/data-source-tabs.component'; +import { IdentifierSetEditorComponent } from './components/identifier-set-editor/identifier-set-editor.component'; +import { + LocationChooserConnectionGroupComponent +} from './components/location-chooser-connection-group/location-chooser-connection-group.component'; +import { LocationChooserComponent } from './components/location-chooser/location-chooser.component'; +import { ManageConnectionGroupComponent } from './components/manage-connection-group/manage-connection-group.component'; +import { ManageConnectionComponent } from './components/manage-connection/manage-connection.component'; +import { ManageSharingProfileComponent } from './components/manage-sharing-profile/manage-sharing-profile.component'; +import { ManageUserGroupComponent } from './components/manage-user-group/manage-user-group.component'; +import { ManageUserComponent } from './components/manage-user/manage-user.component'; +import { ManagementButtonsComponent } from './components/management-buttons/management-buttons.component'; +import { + SharingProfilePermissionComponent +} from './components/sharing-profile-permission/sharing-profile-permission.component'; +import { + SystemPermissionEditorComponent +} from './components/system-permission-editor/system-permission-editor.component'; + +/** + * The module for the administration functionality. + */ +@NgModule({ + declarations: [ + ManageUserComponent, + DataSourceTabsComponent, + SystemPermissionEditorComponent, + IdentifierSetEditorComponent, + ManagementButtonsComponent, + ManageConnectionComponent, + LocationChooserComponent, + LocationChooserConnectionGroupComponent, + ManageUserGroupComponent, + ManageConnectionGroupComponent, + ConnectionPermissionEditorComponent, + ConnectionPermissionComponent, + SharingProfilePermissionComponent, + ConnectionGroupPermissionComponent, + ManageSharingProfileComponent + ], + imports : [ + CommonModule, + FormModule, + TranslocoModule, + FormsModule, + NavigationModule, + ListModule, + NgOptimizedImage, + GroupListModule, + OrderByPipe + ], + exports : [ + ManageUserComponent, + DataSourceTabsComponent, + SystemPermissionEditorComponent, + IdentifierSetEditorComponent, + ManagementButtonsComponent, + ManageConnectionComponent, + LocationChooserComponent, + LocationChooserConnectionGroupComponent, + ManageConnectionGroupComponent + ] +}) +export class ManageModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/services/user-page.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/services/user-page.service.ts new file mode 100644 index 0000000000..f19f57a0fc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/services/user-page.service.ts @@ -0,0 +1,493 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import _ from 'lodash'; +import { catchError, forkJoin, from, map, Observable } from 'rxjs'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { canonicalize } from '../../locale/service/translation.service'; +import { ClientIdentifierService } from '../../navigation/service/client-identifier.service'; +import { ClientIdentifier } from '../../navigation/types/ClientIdentifier'; +import { PageDefinition } from '../../navigation/types/PageDefinition'; +import { ConnectionGroupService } from '../../rest/service/connection-group.service'; +import { DataSourceService } from '../../rest/service/data-source-service.service'; +import { PermissionService } from '../../rest/service/permission.service'; +import { RequestService } from '../../rest/service/request.service'; +import { ConnectionGroup } from '../../rest/types/ConnectionGroup'; +import { PermissionSet } from '../../rest/types/PermissionSet'; + +/** + * A service for generating all the important pages a user can visit. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserPageService { + + /** + * The home page to assign to a user if they can navigate to more than one + * page. + */ + private readonly SYSTEM_HOME_PAGE: PageDefinition = new PageDefinition({ + name: 'USER_MENU.ACTION_NAVIGATE_HOME', + url : '/' + }); + + /** + * Inject required services. + */ + constructor( + private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private dataSourceService: DataSourceService, + private permissionService: PermissionService, + private requestService: RequestService, + private clientIdentifierService: ClientIdentifierService + ) { + } + + /** + * Returns an appropriate home page for the current user. + * + * @param rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. + * + * @param permissions + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. + * + * @returns + * The user's home page. + */ + private generateHomePage(rootGroups: Record, permissions: Record): PageDefinition { + + const settingsPages = this.generateSettingsPages(permissions); + + // If user has access to settings pages, return home page and skip + // evaluation for automatic connections. The Preferences page is + // a Settings page and is always visible, and the Session management + // page is also available to all users so that they can kill their + // own session. We look for more than those two pages to determine + // if we should go to the home page. + if (settingsPages.length > 2) + return this.SYSTEM_HOME_PAGE; + + // If exactly one connection or balancing group is available, use + // that as the home page + const clientPages = this.getClientPages(rootGroups); + return (clientPages.length === 1) ? clientPages[0] : this.SYSTEM_HOME_PAGE; + + } + + /** + * Adds to the given array all pages that the current user may use to + * access connections or balancing groups that are descendants of the given + * connection group. + * + * @param clientPages + * The array that pages should be added to. + * + * @param dataSource + * The data source containing the given connection group. + * + * @param connectionGroup + * The connection group ancestor of the connection or balancing group + * descendants whose pages should be added to the given array. + */ + private addClientPages(clientPages: PageDefinition[], dataSource: string, connectionGroup: ConnectionGroup): void { + + // Add pages for all child connections + connectionGroup.childConnections?.forEach((connection) => { + clientPages.push(new PageDefinition({ + name: connection.name!, + url : '/client/' + this.clientIdentifierService.getString({ + dataSource: dataSource, + type : ClientIdentifier.Types.CONNECTION, + id : connection.identifier + }) + })); + }); + + // Add pages for all child balancing groups, as well as the connectable + // descendants of all balancing groups of any type + connectionGroup.childConnectionGroups?.forEach((connectionGroup) => { + if (connectionGroup.type === ConnectionGroup.Type.BALANCING) { + clientPages.push(new PageDefinition({ + name: connectionGroup.name, + url : '/client/' + this.clientIdentifierService.getString({ + dataSource: dataSource, + type : ClientIdentifier.Types.CONNECTION_GROUP, + id : connectionGroup.identifier + }) + })); + } + + this.addClientPages(clientPages, dataSource, connectionGroup); + + }); + + } + + /** + * Returns a full list of all pages that the current user may use to access + * a connection or balancing group, regardless of the depth of those + * connections/groups within the connection hierarchy. + * + * @param rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. + * + * @returns + * A list of all pages that the current user may use to access a + * connection or balancing group. + */ + getClientPages(rootGroups: Record): PageDefinition[] { + + const clientPages: PageDefinition[] = []; + + // Determine whether a connection or balancing group should serve as + // the home page + for (const dataSource in rootGroups) { + this.addClientPages(clientPages, dataSource, rootGroups[dataSource]); + } + + return clientPages; + + } + + /** + * Returns an observable which emits an appropriate home page for the + * current user. + * + * @returns + * An observable which emits the user's default home page. + */ + getHomePage(): Observable { + + // Resolve promise using home page derived from root connection groups + const rootGroups = this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID), + this.authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ); + const permissionsSets = this.dataSourceService.apply( + (dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), + this.authenticationService.getAvailableDataSources(), + this.authenticationService.getCurrentUsername() + ); + + return forkJoin([rootGroups, permissionsSets]) + .pipe( + map(([rootGroups, permissionsSets]) => { + return this.generateHomePage(rootGroups, permissionsSets); + }), + catchError(this.requestService.DIE) + ); + + } + + /** + * Returns all settings pages that the current user can visit. This can + * include any of the various manage pages. + * + * @param permissionSets + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. + * + * @returns + * An array of all settings pages that the current user can visit. + */ + private generateSettingsPages(permissionSets: Record): PageDefinition[] { + + const pages: PageDefinition[] = []; + + const canManageUsers: string[] = []; + const canManageUserGroups: string[] = []; + const canManageConnections: string[] = []; + const canViewConnectionRecords: string[] = []; + + // Inspect the contents of each provided permission set + this.authenticationService.getAvailableDataSources().forEach((dataSource) => { + + // Get permissions for current data source, skipping if non-existent + let permissions: PermissionSet = permissionSets[dataSource]; + if (!permissions) + return; + + // Do not modify original object + permissions = _.cloneDeep(permissions); + + // Ignore permission to update root group + PermissionSet.removeConnectionGroupPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + ConnectionGroup.ROOT_IDENTIFIER); + + // Ignore permission to update self + PermissionSet.removeUserPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, + this.authenticationService.getCurrentUsername()!); + + // Determine whether the current user needs access to the user management UI + if ( + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER) + + // Permission to update users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + + // Permission to delete users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + + // Permission to administer users + || PermissionSet.hasUserPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) { + canManageUsers.push(dataSource); + } + + // Determine whether the current user needs access to the group management UI + if ( + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_USER_GROUP) + + // Permission to update user groups + || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + + // Permission to delete user groups + || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + + // Permission to administer user groups + || PermissionSet.hasUserGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) { + canManageUserGroups.push(dataSource); + } + + // Determine whether the current user needs access to the connection management UI + if ( + // System permissions + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP) + + // Permission to update connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE) + + // Permission to delete connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.DELETE) + + // Permission to administer connections or connection groups + || PermissionSet.hasConnectionPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + || PermissionSet.hasConnectionGroupPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER) + ) { + canManageConnections.push(dataSource); + } + + // Determine whether the current user needs access to view connection history + if ( + // A user must be a system administrator or auditor to view connection records + PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissions, PermissionSet.SystemPermissionType.AUDIT) + ) { + canViewConnectionRecords.push(dataSource); + } + + }); + + // Add link to Session management (always accessible) + pages.push(new PageDefinition({ + name: 'USER_MENU.ACTION_MANAGE_SESSIONS', + url : '/settings/sessions' + })); + + // If user can view connection records, add links for connection history pages + canViewConnectionRecords.forEach((dataSource) => { + pages.push(new PageDefinition({ + name: [ + 'USER_MENU.ACTION_VIEW_HISTORY', + canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' + ], + url : '/settings/' + encodeURIComponent(dataSource) + '/history' + })); + }); + + // If user can manage users, add link to user management page + if (canManageUsers.length) { + pages.push(new PageDefinition({ + name: 'USER_MENU.ACTION_MANAGE_USERS', + url : '/settings/users' + })); + } + + // If user can manage user groups, add link to group management page + if (canManageUserGroups.length) { + pages.push(new PageDefinition({ + name: 'USER_MENU.ACTION_MANAGE_USER_GROUPS', + url : '/settings/userGroups' + })); + } + + // If user can manage connections, add links for connection management pages + canManageConnections.forEach((dataSource) => { + pages.push(new PageDefinition({ + name: [ + 'USER_MENU.ACTION_MANAGE_CONNECTIONS', + canonicalize('DATA_SOURCE_' + dataSource) + '.NAME' + ], + url : '/settings/' + encodeURIComponent(dataSource) + '/connections' + })); + }); + + // Add link to user preferences (always accessible) + pages.push(new PageDefinition({ + name: 'USER_MENU.ACTION_MANAGE_PREFERENCES', + url : '/settings/preferences' + })); + + return pages; + } + + /** + * Returns an observable which emits an array of all settings pages that + * the current user can visit. This can include any of the various manage + * pages. The promise will not be rejected. + * + * @returns + * An observable which emits an array of all settings pages that the + * current user can visit. + */ + getSettingsPages(): Observable { + + // Retrieve current permissions + return from(this.dataSourceService.apply( + (dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), + this.authenticationService.getAvailableDataSources(), + this.authenticationService.getCurrentUsername() + )) + + // Resolve promise using settings pages derived from permissions + .pipe(map(permissions => { + return this.generateSettingsPages(permissions); + }), + catchError(this.requestService.DIE) + ); + + } + + /** + * Returns all the main pages that the current user can visit. This can + * include the home page, manage pages, etc. In the case that there are no + * applicable pages of this sort, it may return a client page. + * + * @param rootGroups + * A map of all root connection groups visible to the current user, + * where each key is the identifier of the corresponding data source. + * + * @param permissions + * A map of all permissions granted to the current user, where each + * key is the identifier of the corresponding data source. + * + * @returns + * An array of all main pages that the current user can visit. + */ + generateMainPages(rootGroups: Record, permissions: Record): PageDefinition[] { + + const pages: PageDefinition[] = []; + + // Get home page and settings pages + const homePage = this.generateHomePage(rootGroups, permissions); + const settingsPages = this.generateSettingsPages(permissions); + + // Only include the home page in the list of main pages if the user + // can navigate elsewhere. + if (homePage === this.SYSTEM_HOME_PAGE || settingsPages.length) + pages.push(homePage); + + // Add generic link to the first-available settings page + if (settingsPages.length) { + pages.push(new PageDefinition({ + name: 'USER_MENU.ACTION_MANAGE_SETTINGS', + url : settingsPages[0].url + })); + } + + return pages; + } + + /** + * Returns a promise which resolves to an array of all main pages that the + * current user can visit. This can include the home page, manage pages, + * etc. In the case that there are no applicable pages of this sort, it may + * return a client page. The promise will not be rejected. + * + * @returns + * A promise which resolves to an array of all main pages that the + * current user can visit. + */ + getMainPages(): Observable { + + const promise: Promise = new Promise((resolve, reject) => { + + let rootGroups: Record | null = null; + let permissions: Record | null = null; + + /** + * Resolves the main pages retrieval promise, if possible. If + * insufficient data is available, this function does nothing. + */ + const resolveMainPages = () => { + if (rootGroups && permissions) + resolve(this.generateMainPages(rootGroups, permissions)); + }; + + // Retrieve root group, resolving main pages if possible + this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID), + this.authenticationService.getAvailableDataSources(), + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(function rootConnectionGroupsRetrieved(retrievedRootGroups) { + rootGroups = retrievedRootGroups; + resolveMainPages(); + }, this.requestService.PROMISE_DIE); + + + // Retrieve current permissions + this.dataSourceService.apply( + (dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), + this.authenticationService.getAvailableDataSources(), + this.authenticationService.getCurrentUsername() + ) + + // Resolving main pages if possible + .then(function permissionsRetrieved(retrievedPermissions) { + permissions = retrievedPermissions; + resolveMainPages(); + }, this.requestService.PROMISE_DIE); + + }); + + return from(promise); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/manage/styles/attributes.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/attributes.css similarity index 96% rename from guacamole/src/main/frontend/src/app/manage/styles/attributes.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/attributes.css index 2b5bc92fc1..b4cbd4080c 100644 --- a/guacamole/src/main/frontend/src/app/manage/styles/attributes.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/attributes.css @@ -30,10 +30,14 @@ margin: 1em; } -.attributes .form .fields .labeled-field { +.attributes .form .fields guac-form-field { display: table-row; } +.attributes .form .fields .labeled-field { + display: contents; +} + .attributes .form .fields .field-header, .attributes .form .fields .form-field { display: table-cell; diff --git a/guacamole/src/main/frontend/src/app/manage/styles/connection-parameter.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/connection-parameter.css similarity index 95% rename from guacamole/src/main/frontend/src/app/manage/styles/connection-parameter.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/connection-parameter.css index c5645fb36f..ac7a8742b1 100644 --- a/guacamole/src/main/frontend/src/app/manage/styles/connection-parameter.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/connection-parameter.css @@ -32,10 +32,14 @@ width: 100%; } -.connection-parameters .form .fields .labeled-field { +.connection-parameters .form .fields guac-form-field { display: table-row; } +.connection-parameters .form .fields .labeled-field { + display: contents; +} + .connection-parameters .form .fields .field-header, .connection-parameters .form .fields .form-field { display: table-cell; diff --git a/guacamole/src/main/frontend/src/app/manage/styles/forms.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/forms.css similarity index 100% rename from guacamole/src/main/frontend/src/app/manage/styles/forms.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/forms.css diff --git a/guacamole/src/main/frontend/src/app/manage/styles/locationChooser.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/locationChooser.css similarity index 100% rename from guacamole/src/main/frontend/src/app/manage/styles/locationChooser.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/locationChooser.css diff --git a/guacamole/src/main/frontend/src/app/manage/styles/manage-user-group.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user-group.css similarity index 99% rename from guacamole/src/main/frontend/src/app/manage/styles/manage-user-group.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user-group.css index 4c5adc9f73..57b01722e6 100644 --- a/guacamole/src/main/frontend/src/app/manage/styles/manage-user-group.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user-group.css @@ -68,4 +68,4 @@ text-align: center; padding: 1em; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/manage/styles/manage-user.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user.css similarity index 99% rename from guacamole/src/main/frontend/src/app/manage/styles/manage-user.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user.css index 0afaf4e675..fe55b2eaac 100644 --- a/guacamole/src/main/frontend/src/app/manage/styles/manage-user.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/manage-user.css @@ -68,4 +68,4 @@ text-align: center; padding: 1em; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/manage/styles/related-objects.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/related-objects.css similarity index 93% rename from guacamole/src/main/frontend/src/app/manage/styles/related-objects.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/related-objects.css index ddc85b1de1..77284b8c94 100644 --- a/guacamole/src/main/frontend/src/app/manage/styles/related-objects.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/styles/related-objects.css @@ -58,8 +58,8 @@ margin: 0 0.25em; } -.related-objects .abbreviated-related-objects img.expand, -.related-objects .abbreviated-related-objects img.collapse { +.related-objects .abbreviated-related-objects img.expand:not([hidden]), +.related-objects .abbreviated-related-objects img.collapse:not([hidden]) { display: table-cell; max-height: 1.5em; max-width: 1.5em; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ConnectionListContext.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ConnectionListContext.ts new file mode 100644 index 0000000000..1fd0f6172a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ConnectionListContext.ts @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { PermissionFlagSet } from '../../rest/types/PermissionFlagSet'; + +/** + * Expose permission query and modification functions to group list template. + */ +export interface ConnectionListContext { + + /** + * Returns the PermissionFlagSet that contains the current state of + * granted permissions. + * + * @returns + * The PermissionFlagSet describing the current state of granted + * permissions for the permission set being edited. + */ + getPermissionFlags(): PermissionFlagSet; + + /** + * Notifies the controller that a change has been made to the given + * connection permission for the permission set being edited. This + * only applies to READ permissions. + * + * @param identifier + * The identifier of the connection affected by the changed + * permission. + */ + connectionPermissionChanged(identifier: string): void; + + /** + * Notifies the controller that a change has been made to the given + * connection group permission for the permission set being edited. + * This only applies to READ permissions. + * + * @param identifier + * The identifier of the connection group affected by the + * changed permission. + */ + connectionGroupPermissionChanged(identifier: string): void; + + /** + * Notifies the controller that a change has been made to the given + * sharing profile permission for the permission set being edited. + * This only applies to READ permissions. + * + * @param identifier + * The identifier of the sharing profile affected by the changed + * permission. + */ + sharingProfilePermissionChanged(identifier: string): void; + +} diff --git a/guacamole/src/main/frontend/src/app/manage/types/HistoryEntryWrapper.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/HistoryEntryWrapper.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/manage/types/HistoryEntryWrapper.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/HistoryEntryWrapper.ts index a45c2e5e75..3d42341fc3 100644 --- a/guacamole/src/main/frontend/src/app/manage/types/HistoryEntryWrapper.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/HistoryEntryWrapper.ts @@ -17,50 +17,43 @@ * under the License. */ +import { ConnectionHistoryEntry } from '../../rest/types/ConnectionHistoryEntry'; + /** - * A service for defining the HistoryEntryWrapper class. + * Wrapper for ConnectionHistoryEntry which adds display-specific + * properties, such as the connection duration. */ -angular.module('manage').factory('HistoryEntryWrapper', ['$injector', - function defineHistoryEntryWrapper($injector) { +export class HistoryEntryWrapper { - // Required types - var ConnectionHistoryEntry = $injector.get('ConnectionHistoryEntry'); + /** + * The wrapped ConnectionHistoryEntry. + */ + entry: ConnectionHistoryEntry; /** - * Wrapper for ConnectionHistoryEntry which adds display-specific - * properties, such as the connection duration. - * - * @constructor - * @param {ConnectionHistoryEntry} historyEntry - * The history entry to wrap. + * An object providing value and unit properties, denoting the duration + * and its corresponding units. */ - var HistoryEntryWrapper = function HistoryEntryWrapper(historyEntry) { + duration: ConnectionHistoryEntry.Duration | null = null; - /** - * The wrapped ConnectionHistoryEntry. - * - * @type ConnectionHistoryEntry - */ - this.entry = historyEntry; + /** + * The string to display as the duration of this history entry. If a + * duration is available, its value and unit will be exposed to any + * given translation string as the VALUE and UNIT substitution + * variables respectively. + */ + durationText = 'MANAGE_CONNECTION.TEXT_HISTORY_DURATION'; - /** - * An object providing value and unit properties, denoting the duration - * and its corresponding units. - * - * @type ConnectionHistoryEntry.Duration - */ + /** + * Creates a new HistoryEntryWrapper. + * + * @param historyEntry + * The history entry to wrap. + */ + constructor(historyEntry: ConnectionHistoryEntry) { + this.entry = historyEntry; this.duration = null; - /** - * The string to display as the duration of this history entry. If a - * duration is available, its value and unit will be exposed to any - * given translation string as the VALUE and UNIT substitution - * variables respectively. - * - * @type String - */ - this.durationText = 'MANAGE_CONNECTION.TEXT_HISTORY_DURATION'; - // Notify if connection is active right now if (historyEntry.active) this.durationText = 'MANAGE_CONNECTION.INFO_CONNECTION_ACTIVE_NOW'; @@ -72,9 +65,5 @@ angular.module('manage').factory('HistoryEntryWrapper', ['$injector', // Set the duration if the necessary information is present if (historyEntry.endDate && historyEntry.startDate) this.duration = new ConnectionHistoryEntry.Duration(historyEntry.endDate - historyEntry.startDate); - - }; - - return HistoryEntryWrapper; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUser.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUser.ts new file mode 100644 index 0000000000..d333fb20eb --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUser.ts @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { User } from '../../rest/types/User'; +import { Optional } from '../../util/utility-types'; + +/** + * Template type for creating new ManageableUser instances. + * The property "disabled" is optional, and the function "isDisabled" is + * omitted. + */ +export type ManageableUserTemplate = Optional, 'disabled'>; + +/** + * A pairing of an {@link User} with the identifier of its corresponding + * data source. + */ +export class ManageableUser { + + /** + * The unique identifier of the data source containing this user. + */ + dataSource: string; + + /** + * The {@link User} object represented by this ManageableUser and + * contained within the associated data source. + */ + user: User; + + /** + * True if the underlying user account is disabled, otherwise false. + */ + disabled: boolean; + + /** + * Creates a new ManageableUser. This constructor initializes the properties of the + * new ManageableUser with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManageableUser. + */ + constructor(template: ManageableUserTemplate) { + this.dataSource = template.dataSource; + this.user = template.user; + this.disabled = !!template.disabled; + } + + /** + * Return true if the underlying user account is disabled, otherwise + * return false. + * + * @returns + * True if the underlying user account is disabled, otherwise false. + */ + isDisabled(): boolean { + return this.disabled; + } + +} diff --git a/guacamole/src/main/frontend/src/app/manage/types/ManageableUserGroup.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUserGroup.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/manage/types/ManageableUserGroup.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUserGroup.ts index 6853fa0fcd..b9f4cc6006 100644 --- a/guacamole/src/main/frontend/src/app/manage/types/ManageableUserGroup.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManageableUserGroup.ts @@ -17,37 +17,36 @@ * under the License. */ +import { UserGroup } from '../../rest/types/UserGroup'; + /** - * A service for defining the ManageableUserGroup class. + * A pairing of an {@link UserGroup} with the identifier of its corresponding + * data source. */ -angular.module('manage').factory('ManageableUserGroup', [function defineManageableUserGroup() { +export class ManageableUserGroup { /** - * A pairing of an @link{UserGroup} with the identifier of its corresponding - * data source. - * - * @constructor - * @param {Object|ManageableUserGroup} template + * The unique identifier of the data source containing this user. */ - var ManageableUserGroup = function ManageableUserGroup(template) { - - /** - * The unique identifier of the data source containing this user. - * - * @type String - */ - this.dataSource = template.dataSource; + dataSource: string; - /** - * The @link{UserGroup} object represented by this ManageableUserGroup - * and contained within the associated data source. - * - * @type UserGroup - */ - this.userGroup = template.userGroup; - - }; + /** + * The @link{UserGroup} object represented by this ManageableUserGroup + * and contained within the associated data source. + */ + userGroup: UserGroup; - return ManageableUserGroup; -}]); + /** + * Creates a new ManageableUserGroup. This constructor initializes the properties of the + * new ManageableUserGroup with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ManageableUserGroup. + */ + constructor(template: ManageableUserGroup) { + this.dataSource = template.dataSource; + this.userGroup = template.userGroup; + } +} diff --git a/guacamole/src/main/frontend/src/app/manage/types/ManagementPermissions.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManagementPermissions.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/manage/types/ManagementPermissions.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManagementPermissions.ts index 5d02a76f0a..e1a8f66790 100644 --- a/guacamole/src/main/frontend/src/app/manage/types/ManagementPermissions.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/manage/types/ManagementPermissions.ts @@ -17,166 +17,156 @@ * under the License. */ +import { PermissionSet } from '../../rest/types/PermissionSet'; + /** - * A service for defining the ManagementPermissions class. + * Higher-level representation of the management-related permissions + * available to the current user on a particular, arbitrary object. */ -angular.module('manage').factory('ManagementPermissions', ['$injector', - function defineManagementPermissions($injector) { +export class ManagementPermissions { + + /** + * The identifier of the associated object, or null if the object does + * not yet exist. + */ + identifier: string | null; + + /** + * Whether the user can save the associated object. This could be + * updating an existing object, or creating a new object. + */ + canSaveObject: boolean; + + /** + * Whether the user can clone the associated object. + */ + canCloneObject: boolean; + + /** + * Whether the user can delete the associated object. + */ + canDeleteObject: boolean; - // Required types - var PermissionSet = $injector.get('PermissionSet'); + /** + * Whether the user can change attributes which are currently + * associated with the object. + */ + canChangeAttributes: boolean; /** - * Higher-level representation of the management-related permissions - * available to the current user on a particular, arbitrary object. + * Whether the user can change absolutely all attributes associated + * with the object, including those which are not already present. + */ + canChangeAllAttributes: boolean; + + /** + * Whether the user can change permissions which are assigned to the + * associated object, if the object is capable of being assigned + * permissions. + */ + canChangePermissions: boolean; + + /** + * Creates a new ManagementPermissions. This constructor initializes the properties of the + * new ManagementPermissions with the corresponding properties of the given template. * - * @constructor - * @param {ManagementPermissions|Object} template + * @param template * An object whose properties should be copied into the new * ManagementPermissions object. */ - var ManagementPermissions = function ManagementPermissions(template) { - - /** - * The identifier of the associated object, or null if the object does - * not yet exist. - * - * @type String - */ + constructor(template: ManagementPermissions) { this.identifier = template.identifier || null; - - /** - * Whether the user can save the associated object. This could be - * updating an existing object, or creating a new object. - * - * @type Boolean - */ this.canSaveObject = template.canSaveObject; - - /** - * Whether the user can clone the associated object. - * - * @type Boolean - */ this.canCloneObject = template.canCloneObject; - - /** - * Whether the user can delete the associated object. - * - * @type Boolean - */ this.canDeleteObject = template.canDeleteObject; - - /** - * Whether the user can change attributes which are currently - * associated with the object. - * - * @type Boolean - */ this.canChangeAttributes = template.canChangeAttributes; - - /** - * Whether the user can change absolutely all attributes associated - * with the object, including those which are not already present. - * - * @type Boolean - */ this.canChangeAllAttributes = template.canChangeAllAttributes; - - /** - * Whether the user can change permissions which are assigned to the - * associated object, if the object is capable of being assigned - * permissions. - * - * @type Boolean - */ this.canChangePermissions = template.canChangePermissions; - - }; + } /** * Creates a new {@link ManagementPermissions} which defines the high-level * actions the current user may take for the given object. * - * @param {PermissionSet} permissions + * @param permissions * The effective permissions granted to the current user within the * data source associated with the object being managed. * - * @param {String} createPermission + * @param createPermission * The system permission required to create objects of the same type as * the object being managed, as defined by - * {@link PermissionSet.SystemPermissionTypes}. + * {@link PermissionSet.SystemPermissionType}. * - * @param {Function} hasObjectPermission + * @param hasObjectPermission * The function to invoke to test whether a {@link PermissionSet} * contains a particular object permission. The parameters accepted * by this function must be identical to those accepted by * {@link PermissionSet.hasUserPermission()}, * {@link PermissionSet.hasConnectionPermission()}, etc. * - * @param {String} [identifier] + * @param identifier * The identifier of the object being managed. If the object does not * yet exist, this parameter should be omitted or set to null. * - * @returns {ManagementPermissions} + * @returns * A new {@link ManagementPermissions} which defines the high-level * actions the current user may take for the given object. */ - ManagementPermissions.fromPermissionSet = function fromPermissionSet( - permissions, createPermission, hasObjectPermission, identifier) { + static fromPermissionSet(permissions: PermissionSet, createPermission: PermissionSet.SystemPermissionType, + hasObjectPermission: (permissions: PermissionSet, permission: PermissionSet.ObjectPermissionType, identifier?: string) => boolean, + identifier?: string): ManagementPermissions { - var isAdmin = PermissionSet.hasSystemPermission(permissions, - PermissionSet.SystemPermissionType.ADMINISTER); + const nullableIdentifier = identifier === undefined ? null : identifier; - var canCreate = PermissionSet.hasSystemPermission(permissions, createPermission); - var canAdminister = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER, identifier); - var canUpdate = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); - var canDelete = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier); + const isAdmin = PermissionSet.hasSystemPermission(permissions, + PermissionSet.SystemPermissionType.ADMINISTER); - var exists = !!identifier; + const canCreate = PermissionSet.hasSystemPermission(permissions, createPermission); + const canAdminister = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.ADMINISTER, identifier); + const canUpdate = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier); + const canDelete = hasObjectPermission(permissions, PermissionSet.ObjectPermissionType.DELETE, identifier); + + const exists = !!nullableIdentifier; return new ManagementPermissions({ - identifier : identifier, + identifier: nullableIdentifier, // A user can save (create or update) an object if they are a // system-level administrator, OR the object does not yet exist and // the user has explicit permission to create such objects, OR the // object does already exist and the user has explicit UPDATE // permission on the object - canSaveObject : isAdmin || (!exists && canCreate) || canUpdate, + canSaveObject: isAdmin || (!exists && canCreate) || canUpdate, // A user can clone an object only if the object exists, and // only if they are a system-level administrator OR they have // explicit permission to create such objects - canCloneObject : exists && (isAdmin || canCreate), + canCloneObject: exists && (isAdmin || canCreate), // A user can delete an object only if the object exists, and // only if they are a system-level administrator OR they have // explicit DELETE permission on the object - canDeleteObject : exists && (isAdmin || canDelete), + canDeleteObject: exists && (isAdmin || canDelete), // Attributes in general (with or without existing values) can only // be changed if the object is being created, OR the user is a // system-level administrator, OR the user has explicit UPDATE // permission on the object - canChangeAttributes : !exists || isAdmin || canUpdate, + canChangeAttributes: !exists || isAdmin || canUpdate, // A user can change the attributes of an object which are not // explicitly defined on that object when the object is being // created - canChangeAllAttributes : !exists, + canChangeAllAttributes: !exists, // A user can change the system permissions related to an object // if they are a system-level admin, OR they are creating the // object, OR they have explicit ADMINISTER permission on the // existing object - canChangePermissions : isAdmin || !exists || canAdminister + canChangePermissions: isAdmin || !exists || canAdminister }); - }; - - return ManagementPermissions; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.html new file mode 100644 index 0000000000..9433343840 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.html @@ -0,0 +1,28 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.ts new file mode 100644 index 0000000000..999a574e08 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-menu/guac-menu.component.ts @@ -0,0 +1,86 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DOCUMENT } from '@angular/common'; +import { Component, ElementRef, Inject, Input, OnInit, ViewEncapsulation } from '@angular/core'; + +@Component({ + selector: 'guac-menu', + templateUrl: './guac-menu.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacMenuComponent implements OnInit { + + /** + * The string which should be rendered as the menu title. + */ + @Input() menuTitle = ''; + + /** + * Whether the menu should remain open while the user interacts + * with the contents of the menu. By default, the menu will close + * if the user clicks within the menu contents. + */ + @Input() interactive = false; + + /** + * The outermost element of the guacMenu directive. + */ + element?: Element; + + /** + * Whether the contents of the menu are currently shown. + */ + menuShown = false; + + constructor(@Inject(DOCUMENT) private document: Document, private elementRef: ElementRef) { + this.element = elementRef.nativeElement; + } + + ngOnInit(): void { + + // Close menu when user clicks anywhere outside this specific menu + this.document.body.addEventListener('click', (e: MouseEvent) => { + + if (e.target !== this.element && !this.element?.contains(e.target as HTMLElement)) + this.menuShown = false; + + }, false); + + } + + + /** + * Toggles visibility of the menu contents. + */ + toggleMenu(): void { + this.menuShown = !this.menuShown; + } + + /** + * Prevent clicks within menu contents from toggling menu visibility + * if the menu contents are intended to be interactive. + */ + clickInsideMenuContents(e: Event): void { + if (this.interactive) + e.stopPropagation(); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.html new file mode 100644 index 0000000000..fff3300b02 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.html @@ -0,0 +1,42 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.spec.ts new file mode 100644 index 0000000000..8f0bb31d3b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.spec.ts @@ -0,0 +1,93 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { SimpleChange } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { LocaleModule } from '../../../locale/locale.module'; +import { GuacPageListComponent } from './guac-page-list.component'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('PageListComponent', () => { + let component: GuacPageListComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [GuacPageListComponent], + imports: [RouterTestingModule, + LocaleModule], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}); + fixture = TestBed.createComponent(GuacPageListComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should display a list of links to pages', () => { + + const selector = By.css('a'); + + component.pages = [ + { name: 'Page 1', url: '/page1' }, + { name: 'Page 2', url: '/page2' }, + { name: 'Page 3', url: '/page3' } + ]; + + component.ngOnChanges({ pages: new SimpleChange([], component.pages, false) }); + fixture.detectChanges(); + + const elements = fixture.debugElement.queryAll(selector); + expect(elements.length).toBe(3); + }); + + it('should add a "current" class to the link of the current page', () => { + + const selector = By.css('a'); + + component.pages = [ + { name: 'Page 0', url: '/' }, + { name: 'Page 1', url: '/page1' }, + ]; + + component.ngOnChanges({ pages: new SimpleChange([], component.pages, false) }); + fixture.detectChanges(); + + const elements = fixture.debugElement.queryAll(selector); + + const page0 = elements[0].nativeElement; + const page1 = elements[1].nativeElement; + + expect(page0).toHaveClass('current'); + expect(page1).not.toHaveClass('current'); + + component.currentURL = '/page1'; + fixture.detectChanges(); + + expect(page0).not.toHaveClass('current'); + expect(page1).toHaveClass('current'); + }); + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.ts new file mode 100644 index 0000000000..c2d7bf988a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-page-list/guac-page-list.component.ts @@ -0,0 +1,262 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, OnInit, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { filter } from 'rxjs'; +import { isArray } from '../../../util/is-array'; +import { PageDefinition } from '../../types/PageDefinition'; + +/** + * A directive which provides a list of links to specific pages. + */ +@Component({ + selector: 'guac-page-list', + templateUrl: './guac-page-list.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacPageListComponent implements OnInit, OnChanges { + + /** + * The array of pages to display. + */ + @Input() pages: PageDefinition[] = []; + + /** + * The URL of the currently-displayed page. + */ + currentURL = ''; + + /** + * The names associated with the current page, if the current page + * is known. The value of this property corresponds to the value of + * PageDefinition.name. Though PageDefinition.name may be a String, + * this will always be an Array. + */ + currentPageName: string[] = []; + + /** + * Array of each level of the page list, where a level is defined + * by a mapping of names (translation strings) to the + * PageDefinitions corresponding to those names. + */ + levels: Record[] = []; + + /** + * Inject required services. + */ + constructor(private router: Router, private route: ActivatedRoute) { + } + + ngOnInit(): void { + + // Set the currentURL initially + this.currentURL = this.router.url; + + // Updates the currentURL when the route changes. + this.router.events + .pipe(filter((event): event is NavigationEnd => event instanceof NavigationEnd)) + .subscribe((event) => { + this.currentURL = event.url; + }); + } + + + /** + * Returns the names associated with the given page, in + * hierarchical order. If the page is only associated with a single + * name, and that name is not stored as an array, it will be still + * be returned as an array containing a single item. + * + * @param page + * The page to return the names of. + * + * @return + * An array of all names associated with the given page, in + * hierarchical order. + */ + getPageNames(page: PageDefinition): string[] { + + // If already an array, simply return the name + if (isArray(page.name)) + return page.name; + + // Otherwise, transform into array + return [page.name]; + + } + + /** + * Adds the given PageDefinition to the overall set of pages + * displayed by this guacPageList, automatically updating the + * available levels and the contents of those levels. + * + * @param page + * The PageDefinition to add. + * + * @param weight + * The sorting weight to use for the page if it does not + * already have an associated weight. + */ + addPage(page: PageDefinition, weight = 0): void { + + // Pull all names for page + const names = this.getPageNames(page); + + // Copy the hierarchy of this page into the displayed levels + // as far as is relevant for the currently-displayed page + for (let i = 0; i < names.length; i++) { + + // Create current level, if it doesn't yet exist + let pages = this.levels[i]; + if (!pages) + pages = this.levels[i] = {}; + + // Get the name at the current level + const name = names[i]; + + // Determine whether this page definition is part of the + // hierarchy containing the current page + const isCurrentPage = (this.currentPageName[i] === name); + + // Store new page if it doesn't yet exist at this level + if (!pages[name]) { + pages[name] = new PageDefinition({ + name : name, + url : isCurrentPage ? this.currentURL : page.url, + className: page.className, + weight : page.weight || (weight + i) + }); + } + + // If the name at this level no longer matches the + // hierarchy of the current page, do not go any deeper + if (this.currentPageName[i] !== name) + break; + + } + + } + + /** + * Navigate to the given page. + * + * @param page + * The page to navigate to. + */ + navigateToPage(page: PageDefinition): void { + this.router.navigate([page.url]); + } + + /** + * Tests whether the given page is the page currently being viewed. + * + * @param page + * The page to test. + * + * @returns + * true if the given page is the current page, false otherwise. + */ + isCurrentPage(page: PageDefinition): boolean { + return this.currentURL === page.url; + } + + /** + * Given an arbitrary map of PageDefinitions, returns an array of + * those PageDefinitions, sorted by weight. + * + * @param level + * A map of PageDefinitions with arbitrary keys. The value of + * each key is ignored. + * + * @returns + * An array of all PageDefinitions in the given map, sorted by + * weight. + */ + getPages(level: Record): PageDefinition[] { + + const pages: PageDefinition[] = []; + + // Convert contents of level to a flat array of pages + for (const key in level) { + const page = level[key]; + pages.push(page); + + } + + // Sort page array by weight + pages.sort(function comparePages(a, b) { + return (a.weight || 0) - (b.weight || 0); + }); + + return pages; + + } + + ngOnChanges(changes: SimpleChanges): void { + // Update page levels whenever pages changes + if (changes['pages']) { + const pages: PageDefinition[] = changes['pages'].currentValue; + + // Determine current page name + this.currentPageName = []; + for (const page of pages) { + + // If page is current page, store its names + if (this.isCurrentPage(page)) + this.currentPageName = this.getPageNames(page); + } + + + // Reset contents of levels + this.levels = []; + + // Add all page definitions + pages.forEach((page) => this.addPage(page)); + + // Filter to only relevant levels + this.levels = this.levels.filter(function isRelevant(level) { + + // Determine relevancy by counting the number of pages + let pageCount = 0; + for (const name in level) { + + // Level is relevant if it has two or more pages + if (++pageCount === 2) + return true; + + } + + // Otherwise, the level is not relevant + return false; + + }); + } + } + + trackByIndex(index: number): number { + return index; + } + + /** + * Make isArray() available to the template to render page.name accordingly. + */ + protected readonly isArray = isArray; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.html new file mode 100644 index 0000000000..b266f738ed --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.html @@ -0,0 +1,29 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.spec.ts new file mode 100644 index 0000000000..e88e369858 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.spec.ts @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { TranslocoTestingModule } from '@ngneat/transloco'; +import { LocaleModule } from '../../../locale/locale.module'; + +import { GuacSectionTabsComponent } from './guac-section-tabs.component'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('SectionTabsComponent', () => { + let component: GuacSectionTabsComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [GuacSectionTabsComponent], + imports: [LocaleModule, + TranslocoTestingModule.forRoot({ + langs: { + en: { + 'TAB_1': 'TAB_1', + 'TAB_2': 'TAB_2', + 'TAB_3': 'TAB_3' + } + } + })], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}); + fixture = TestBed.createComponent(GuacSectionTabsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should produce the correct section header translation string', () => { + component.namespace = 'namespace'; + const sectionHeader = component.getSectionHeader('Tab 1'); + expect(sectionHeader).toBe('NAMESPACE.SECTION_HEADER_TAB_1'); + }); + + it('should emit the current tab name when a tab is selected', () => { + component.tabs = ['Tab 1', 'Tab 2', 'Tab 3']; + component.current = 'Tab 1'; + spyOn(component.currentChange, 'emit'); + component.selectTab('Tab 2'); + expect(component.current).toBe('Tab 2'); + expect(component.currentChange.emit).toHaveBeenCalledWith('Tab 2'); + }); + + it('should render the tabs', () => { + component.tabs = ['Tab 1', 'Tab 2', 'Tab 3']; + fixture.detectChanges(); + const tabElements = fixture.debugElement.queryAll(By.css('a')); + expect(tabElements.length).toBe(3); + expect(tabElements[0].nativeElement.textContent).toContain('TAB_1'); + expect(tabElements[1].nativeElement.textContent).toContain('TAB_2'); + expect(tabElements[2].nativeElement.textContent).toContain('TAB_3'); + }); + + it('should apply the "current" CSS class to the current tab', () => { + component.tabs = ['Tab 1', 'Tab 2', 'Tab 3']; + component.current = 'Tab 2'; + fixture.detectChanges(); + const tabElements = fixture.debugElement.queryAll(By.css('a')); + expect(tabElements.length).toBe(3); + expect(tabElements[0].nativeElement).not.toHaveClass('current'); + expect(tabElements[1].nativeElement).toHaveClass('current'); + expect(tabElements[2].nativeElement).not.toHaveClass('current'); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.ts new file mode 100644 index 0000000000..d80606848c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-section-tabs/guac-section-tabs.component.ts @@ -0,0 +1,139 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core'; +import { canonicalize } from '../../../locale/service/translation.service'; + +/** + * Component which displays a set of tabs dividing a section of a page into + * logical subsections or views. The currently selected tab is communicated + * through assignment to the variable bound to the current + * attribute. No navigation occurs as a result of selecting a tab. + */ +@Component({ + selector: 'guac-section-tabs', + templateUrl: './guac-section-tabs.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSectionTabsComponent { + + /** + * The translation namespace to use when producing translation + * strings for each tab. Tab translation strings will be of the + * form: + * + * NAMESPACE.SECTION_HEADER_NAME + * + * where NAMESPACE is the namespace provided to this + * attribute and NAME is one of the names within the + * array provided to the tabs attribute and + * transformed via canonicalize(). + */ + @Input() namespace?: string; + + /** + * The name of the currently selected tab. This name MUST be one of + * the names present in the array given via the tabs + * attribute. This directive will not automatically choose an + * initially selected tab, and a default value should be manually + * assigned to current to ensure a tab is initially + * selected. + */ + @Input() current?: string; + + /** + * The name of the currently selected tab. This name MUST be one of + * the names present in the array given via the tabs + * attribute. This directive will not automatically choose an + * initially selected tab, and a default value should be manually + * assigned to current to ensure a tab is initially + * selected. + * + * When the current tab changes, this output property emits the new + * tab name as a string. + */ + @Output() currentChange: EventEmitter = new EventEmitter(); + + /** + * The unique names of all tabs which should be made available, in + * display order. These names will be assigned to the variable + * bound to the current attribute when the current + * tab changes. + */ + @Input() tabs?: string[]; + + /** + * Produces the translation string for the section header representing + * the tab having the given name. The translation string will be of the + * form: + * + * NAMESPACE.SECTION_HEADER_NAME + * + * where NAMESPACE is the namespace provided to the + * directive and NAME is the given name transformed + * via canonicalize(). + * + * @param name + * The name of the tab. + * + * @returns + * The translation string which produces the translated header + * of the tab having the given name. + */ + getSectionHeader(name: string): string { + + // If no name, then no header + if (!name) + return ''; + + return canonicalize(this.namespace || 'MISSING_NAMESPACE') + + '.SECTION_HEADER_' + canonicalize(name); + + } + + /** + * Selects the tab having the given name. The name of the currently + * selected tab will be communicated outside the directive through + * this.current. + * + * @param name + * The name of the tab to select. + */ + selectTab(name: string): void { + this.current = name; + this.currentChange.emit(this.current); + } + + /** + * Returns whether the tab having the given name is currently + * selected. A tab is currently selected if its name is stored within + * this.current, as assigned externally or by selectTab(). + * + * @param name + * The name of the tab to test. + * + * @returns + * true if the tab having the given name is currently selected, + * false otherwise. + */ + isSelected(name: string): boolean { + return this.current === name; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.html new file mode 100644 index 0000000000..5c51f14b08 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.html @@ -0,0 +1,52 @@ + + +
      + + + +
      + +
      {{ role }}
      +
      {{ organization }}
      +
      + + + + + + + + + + +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.ts new file mode 100644 index 0000000000..3ca7ac9553 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/components/guac-user-menu/guac-user-menu.component.ts @@ -0,0 +1,156 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnInit, ViewEncapsulation } from '@angular/core'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { UserPageService } from '../../../manage/services/user-page.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { UserService } from '../../../rest/service/user.service'; +import { User } from '../../../rest/types/User'; +import { MenuAction } from '../../types/MenuAction'; +import { PageDefinition } from '../../types/PageDefinition'; + +/** + * A directive which provides a user-oriented menu containing options for + * navigation and configuration. + */ +@Component({ + selector: 'guac-user-menu', + templateUrl: './guac-user-menu.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacUserMenuComponent implements OnInit { + + /** + * Optional array of actions which are specific to this particular + * location, as these actions may not be appropriate for other + * locations which contain the user menu. + */ + @Input() localActions?: MenuAction[]; + + /** + * The username of the current user. + */ + username: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The user's full name. If not yet available, or if not defined, + * this will be null. + */ + fullName: string | null = null; + + /** + * A URL pointing to relevant user information such as the user's + * email address. If not yet available, or if no such URL can be + * determined, this will be null. + */ + userURL: string | null = null; + + /** + * The organization, company, group, etc. that the user belongs to. + * If not yet available, or if not defined, this will be null. + */ + organization: string | null = null; + + /** + * The role that the user has at the organization, company, group, + * etc. they belong to. If not yet available, or if not defined, + * this will be null. + */ + role: string | null = null; + + /** + * The available main pages for the current user. + */ + pages: PageDefinition[] = []; + + /** + * Action which logs out the current user, redirecting them to back + * to the login screen after logout completes. + */ + private readonly LOGOUT_ACTION: MenuAction = { + name : 'USER_MENU.ACTION_LOGOUT', + className: 'logout', + callback : () => this.logout() + }; + + /** + * All available actions for the current user. + */ + actions: MenuAction[] = [this.LOGOUT_ACTION]; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private requestService: RequestService, + private userService: UserService, + private userPageService: UserPageService + ) { + } + + ngOnInit(): void { + // Display user profile attributes if available + this.userService.getUser(this.authenticationService.getDataSource()!, this.username!) + .subscribe({ + next: user => { + // Pull basic profile information + this.fullName = user.attributes[User.Attributes.FULL_NAME]; + this.organization = user.attributes[User.Attributes.ORGANIZATION]; + this.role = user.attributes[User.Attributes.ORGANIZATIONAL_ROLE]; + + // Link to email address if available + const email = user.attributes[User.Attributes.EMAIL_ADDRESS]; + this.userURL = email ? 'mailto:' + email : null; + }, + + error: this.requestService.IGNORE + }); + + // Retrieve the main pages from the user page service + this.userPageService.getMainPages() + .subscribe(pages => { + this.pages = pages; + }); + } + + /** + * Returns whether the current user has authenticated anonymously. + * + * @returns + * true if the current user has authenticated anonymously, false + * otherwise. + */ + isAnonymous(): boolean { + return this.authenticationService.isAnonymous(); + } + + /** + * Logs out the current user, redirecting them to back to the root + * after logout completes. + */ + logout(): void { + this.authenticationService.logout().subscribe({ + error: this.requestService.IGNORE + }); + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/navigation.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/navigation.module.ts new file mode 100644 index 0000000000..855761f29d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/navigation.module.ts @@ -0,0 +1,52 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterLink } from '@angular/router'; +import { TranslocoModule } from '@ngneat/transloco'; +import { GuacMenuComponent } from './components/guac-menu/guac-menu.component'; +import { GuacPageListComponent } from './components/guac-page-list/guac-page-list.component'; +import { GuacSectionTabsComponent } from './components/guac-section-tabs/guac-section-tabs.component'; +import { GuacUserMenuComponent } from './components/guac-user-menu/guac-user-menu.component'; + +/** + * Module for generating and implementing user navigation options. + */ +@NgModule({ + declarations: [ + GuacMenuComponent, + GuacPageListComponent, + GuacSectionTabsComponent, + GuacUserMenuComponent + ], + exports : [ + GuacMenuComponent, + GuacPageListComponent, + GuacSectionTabsComponent, + GuacUserMenuComponent + ], + imports : [ + CommonModule, + TranslocoModule, + RouterLink, + ] +}) +export class NavigationModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/service/client-identifier.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/service/client-identifier.service.ts new file mode 100644 index 0000000000..13f41526d9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/service/client-identifier.service.ts @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { ClientIdentifier } from '../types/ClientIdentifier'; + +/** + * Encodes the given value as base64url, a variant of base64 defined by + * RFC 4648: https://datatracker.ietf.org/doc/html/rfc4648#section-5. + * + * The "base64url" variant is identical to standard base64 except that it + * uses "-" instead of "+", "_" instead of "/", and padding with "=" is + * optional. + * + * @param value + * The string value to encode. + * + * @returns + * The provided string value encoded as unpadded base64url. + */ +const base64urlEncode = (value: string): string => { + + // Translate padded standard base64 to unpadded base64url + return window.btoa(value).replace(/[+/=]/g, + (str) => ({ + '+': '-', + '/': '_', + '=': '' + })[str] as string + ); + +}; + +/** + * Decodes the given base64url or base64 string. The input string may + * contain "=" padding characters, but this is not required. + * + * @param value + * The base64url or base64 value to decode. + * + * @returns + * The result of decoding the provided base64url or base64 string. + */ +const base64urlDecode = (value: string): string => { + + // Add any missing padding (standard base64 requires input strings to + // be multiples of 4 in length, padded using '=') + value += ([ + '', + '===', + '==', + '=' + ])[value.length % 4]; + + // Translate padded base64url to padded standard base64 + return window.atob(value.replace(/[-_]/g, + (str) => ({ + '-': '+', + '_': '/' + })[str] as string + )); +}; + +@Injectable({ + providedIn: 'root' +}) +export class ClientIdentifierService { + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService + ) { + } + + /** + * Converts the given ClientIdentifier or ClientIdentifier-like object to + * a String representation. Any object having the same properties as + * ClientIdentifier may be used, but only those properties will be taken + * into account when producing the resulting String. + * + * @param id + * The ClientIdentifier or ClientIdentifier-like object to convert to + * a String representation. + * + * @returns + * A deterministic String representation of the given ClientIdentifier + * or ClientIdentifier-like object. + */ + getString(id: ClientIdentifier): string { + return ClientIdentifierService.getString(id); + } + + /** + * Converts the given ClientIdentifier or ClientIdentifier-like object to + * a String representation. Any object having the same properties as + * ClientIdentifier may be used, but only those properties will be taken + * into account when producing the resulting String. + * + * @param id + * The ClientIdentifier or ClientIdentifier-like object to convert to + * a String representation. + * + * @returns + * A deterministic String representation of the given ClientIdentifier + * or ClientIdentifier-like object. + */ + static getString(id: ClientIdentifier): string { + return base64urlEncode([ + id.id, + id.type, + id.dataSource + ].join('\0')); + } + + /** + * Converts the given String into the corresponding ClientIdentifier. If + * the provided String is not a valid identifier, it will be interpreted + * as the identifier of a connection within the data source that + * authenticated the current user. + * + * @param str + * The String to convert to a ClientIdentifier. + * + * @returns + * The ClientIdentifier represented by the given String. + */ + fromString(str: string): ClientIdentifier { + + try { + const values = base64urlDecode(str).split('\0'); + return new ClientIdentifier({ + id : values[0], + type : values[1], + dataSource: values[2] + }); + } + + // If the provided string is invalid, transform into a reasonable guess + catch (e) { + return new ClientIdentifier({ + id : str, + type : ClientIdentifier.Types.CONNECTION, + dataSource: this.authenticationService.getDataSource() || 'default' + }); + } + + } + + +} diff --git a/guacamole/src/main/frontend/src/app/navigation/styles/menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/menu.css similarity index 99% rename from guacamole/src/main/frontend/src/app/navigation/styles/menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/menu.css index 608650ea8a..a5638c64d4 100644 --- a/guacamole/src/main/frontend/src/app/navigation/styles/menu.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/menu.css @@ -28,7 +28,7 @@ display: -moz-box; -moz-box-align: center; -moz-box-orient: horizontal; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: center; @@ -78,7 +78,7 @@ -webkit-box-flex: 0; -webkit-flex: 0 0 auto; flex: 0 0 auto; - + } .menu-dropdown .menu-indicator { @@ -112,7 +112,7 @@ border-bottom: 1px solid rgba(0,0,0,0.125); z-index: 5; - + } .menu-dropdown .menu-contents ul { diff --git a/guacamole/src/main/frontend/src/app/navigation/styles/tabs.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/tabs.css similarity index 100% rename from guacamole/src/main/frontend/src/app/navigation/styles/tabs.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/tabs.css diff --git a/guacamole/src/main/frontend/src/app/navigation/styles/user-menu.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/user-menu.css similarity index 98% rename from guacamole/src/main/frontend/src/app/navigation/styles/user-menu.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/user-menu.css index 0af3f82c65..337b797af4 100644 --- a/guacamole/src/main/frontend/src/app/navigation/styles/user-menu.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/styles/user-menu.css @@ -17,6 +17,11 @@ * under the License. */ +guac-menu, +guac-user-menu { + display: contents; +} + .user-menu { /* IE10 */ @@ -28,7 +33,7 @@ display: -moz-box; -moz-box-align: stretch; -moz-box-orient: horizontal; - + /* Ancient WebKit */ display: -webkit-box; -webkit-box-align: stretch; @@ -96,4 +101,4 @@ .user-menu .menu-dropdown .menu-contents .profile .organization, .user-menu .menu-dropdown .menu-contents .profile .organizational-role { font-size: 0.8em; -} \ No newline at end of file +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/ClientIdentifier.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/ClientIdentifier.ts new file mode 100644 index 0000000000..b596483c18 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/ClientIdentifier.ts @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Uniquely identifies a particular connection or connection + * group within Guacamole. This object can be converted to/from a string to + * generate a guaranteed-unique, deterministic identifier for client URLs. + */ +export class ClientIdentifier { + + /** + * The identifier of the data source associated with the object to + * which the client will connect. This identifier will be the + * identifier of an AuthenticationProvider within the Guacamole web + * application. + */ + dataSource: string; + + /** + * The type of object to which the client will connect. Possible values + * are defined within ClientIdentifier.Types. + */ + type: string; + + /** + * The unique identifier of the object to which the client will + * connect. + */ + id?: string; + + /** + * Creates a new ClientIdentifier. This constructor initializes the properties of the + * new ClientIdentifier with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ClientIdentifier. + */ + constructor(template: ClientIdentifier) { + this.dataSource = template.dataSource; + this.type = template.type; + this.id = template.id; + } + +} + +export namespace ClientIdentifier { + + /** + * All possible ClientIdentifier types. + */ + export enum Types { + /** + * The type string for a Guacamole connection. + */ + CONNECTION = 'c', + + /** + * The type string for a Guacamole connection group. + */ + CONNECTION_GROUP = 'g', + + /** + * The type string for an active Guacamole connection. + */ + ACTIVE_CONNECTION = 'a' + } +} + diff --git a/guacamole/src/main/frontend/src/app/navigation/types/MenuAction.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/MenuAction.ts similarity index 51% rename from guacamole/src/main/frontend/src/app/navigation/types/MenuAction.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/MenuAction.ts index a63bae543f..5b019391ad 100644 --- a/guacamole/src/main/frontend/src/app/navigation/types/MenuAction.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/MenuAction.ts @@ -18,57 +18,42 @@ */ /** - * Provides the MenuAction class definition. + * Pairs an arbitrary callback with + * an action name. The name of this action will ultimately be presented to + * the user when the user when this action's associated menu is open. */ -angular.module('navigation').factory('MenuAction', [function defineMenuAction() { +export class MenuAction { /** - * Creates a new MenuAction, which pairs an arbitrary callback with - * an action name. The name of this action will ultimately be presented to - * the user when the user when this action's associated menu is open. + * The CSS class associated with this action. + */ + className?: string; + + /** + * The name of this action. + */ + name: string; + + /** + * The callback to call when this action is performed. + */ + callback: Function; + + /** + * Creates a new MenuAction. * - * @constructor - * @param {String} name + * @param name * The name of this action. * - * @param {Function} callback + * @param callback * The callback to call when the user elects to perform this action. - * - * @param {String} className + * + * @param className * The CSS class to associate with this action, if any. */ - var MenuAction = function MenuAction(name, callback, className) { - - /** - * Reference to this MenuAction. - * - * @type MenuAction - */ - var action = this; - - /** - * The CSS class associated with this action. - * - * @type String - */ - this.className = className; - - /** - * The name of this action. - * - * @type String - */ + constructor(name: string, callback: Function, className?: string) { this.name = name; - - /** - * The callback to call when this action is performed. - * - * @type Function - */ this.callback = callback; - - }; - - return MenuAction; - -}]); + this.className = className; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/PageDefinition.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/PageDefinition.ts new file mode 100644 index 0000000000..ee225c5f60 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/navigation/types/PageDefinition.ts @@ -0,0 +1,68 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Pairs the URL of a page with + * an arbitrary, human-readable name. + */ +export class PageDefinition { + + /** + * The name of the page, which should be a translation table key. + * Alternatively, this may also be a list of names, where the final + * name represents the page and earlier names represent categorization. + * Those categorical names may be rendered hierarchically as a system + * of menus, tabs, etc. + */ + name: string | string[]; + + /** + * The URL of the page. + */ + url: string; + + /** + * The CSS class name to associate with this page, if any. This will be + * an empty string by default. + */ + className?: string; + + /** + * A numeric value denoting the relative sort order when compared to + * other sibling PageDefinitions. If unspecified, sort order is + * determined by the system using the PageDefinition. + */ + weight?: number; + + + /** + * Creates a new PageDefinition object. + * + * @param template + * The object whose properties should be copied within the new + * PageDefinition. + */ + constructor(template: PageDefinition) { + this.name = template.name; + this.url = template.url; + this.className = template.className || ''; + this.weight = template.weight; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.html new file mode 100644 index 0000000000..b81e521d6b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.html @@ -0,0 +1,22 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.spec.ts new file mode 100644 index 0000000000..2a986d7acd --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { GuacModalComponent } from './guac-modal.component'; + +describe('ModalComponent', () => { + let component: GuacModalComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [GuacModalComponent] + }); + fixture = TestBed.createComponent(GuacModalComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/frontend/src/app/client/clientModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.ts similarity index 72% rename from guacamole/src/main/frontend/src/app/client/clientModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.ts index 73d3220212..1f0c490617 100644 --- a/guacamole/src/main/frontend/src/app/client/clientModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-modal/guac-modal.component.ts @@ -17,18 +17,17 @@ * under the License. */ +import { Component, ViewEncapsulation } from '@angular/core'; + /** - * The module for code used to connect to a connection or balancing group. + * A component for displaying arbitrary modal content. */ -angular.module('client', [ - 'auth', - 'clipboard', - 'element', - 'history', - 'navigation', - 'notification', - 'osk', - 'rest', - 'textInput', - 'touch' -]); +@Component({ + selector: 'guac-modal', + templateUrl: './guac-modal.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacModalComponent { + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.html new file mode 100644 index 0000000000..cd02432cb7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.html @@ -0,0 +1,68 @@ + + +
      + + +
      +
      {{ notification.title | transloco }}
      +
      + +
      + + +

      + + +
      + +
      + + +
      +
      +
      +
      + + +

      + +
      + + +
      + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.ts new file mode 100644 index 0000000000..78ccd0b71a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/components/guac-notification/guac-notification.component.ts @@ -0,0 +1,135 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { + Component, + DestroyRef, + DoCheck, + Input, + OnChanges, + OnDestroy, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { FormService } from '../../../form/service/form.service'; +import { Notification } from '../../types/Notification'; + +/** + * A directive for displaying notifications. + */ +@Component({ + selector: 'guac-notification', + templateUrl: './guac-notification.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacNotificationComponent implements OnChanges, DoCheck, OnDestroy { + + /** + * The notification to display. + */ + @Input({ required: true }) notification!: Notification | any; + + /** + * The percentage of the operation that has been completed, if known. + */ + progressPercent: number | null = null; + + /** + * The time remaining in the countdown, if any. + */ + timeRemaining: number | null = null; + + /** + * The interval used to update the countdown, if any. + */ + private interval: number | null = null; + + /** + * TODO + */ + notificationFormGroup: FormGroup = new FormGroup({}); + + constructor(private formService: FormService, + private destroyRef: DestroyRef) { + } + + /** + * Custom change detection logic for the component. + */ + ngDoCheck(): void { + + // Update progress bar if end known + if (this.notification?.progress?.ratio) { + this.progressPercent = this.notification.progress.ratio * 100; + } + + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes['notification']) { + const forms = this.formService.asFormArray(this.notification.forms); + this.notificationFormGroup = this.formService.getFormGroup(forms); + this.notificationFormGroup.patchValue(this.notification.formModel || {}); + this.notificationFormGroup.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + this.notification.formModel = value; + }); + + const countdown = this.notification.countdown; + + // Clean up any existing interval + if (this.interval) + window.clearInterval(this.interval); + + // Update and handle countdown, if provided + if (countdown) { + + this.timeRemaining = countdown.remaining; + + this.interval = window.setInterval(() => { + + // Update time remaining + this.timeRemaining!--; + + // Call countdown callback when time remaining expires + if (this.timeRemaining === 0 && countdown.callback) + countdown.callback(); + + }, 1000, this.timeRemaining); + + } + + } + + } + + /** + * Clean up interval upon destruction. + */ + ngOnDestroy(): void { + if (this.interval) + window.clearInterval(this.interval); + } + + +} diff --git a/guacamole/src/main/frontend/src/app/notification/notificationModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/notification.module.ts similarity index 53% rename from guacamole/src/main/frontend/src/app/notification/notificationModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/notification.module.ts index 15a2a4628b..7934578026 100644 --- a/guacamole/src/main/frontend/src/app/notification/notificationModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/notification.module.ts @@ -17,10 +17,32 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslocoModule } from '@ngneat/transloco'; +import { FormModule } from '../form/form.module'; +import { GuacModalComponent } from './components/guac-modal/guac-modal.component'; +import { GuacNotificationComponent } from './components/guac-notification/guac-notification.component'; + /** * The module for code used to display arbitrary notifications. */ -angular.module('notification', [ - 'rest', - 'storage' -]); +@NgModule({ + declarations: [ + GuacModalComponent, + GuacNotificationComponent + ], + imports : [ + CommonModule, + TranslocoModule, + FormModule, + FormsModule + ], + exports : [ + GuacModalComponent, + GuacNotificationComponent + ] +}) +export class NotificationModule { +} diff --git a/guacamole/src/main/frontend/src/app/notification/services/guacNotification.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/services/guac-notification.service.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/notification/services/guacNotification.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/services/guac-notification.service.ts index 22eeed6304..bb616d173a 100644 --- a/guacamole/src/main/frontend/src/app/notification/services/guacNotification.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/services/guac-notification.service.ts @@ -17,60 +17,72 @@ * under the License. */ +import { Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { filter, Observable, throwError } from 'rxjs'; +import { RequestService } from '../../rest/service/request.service'; +import { SessionStorageEntry, SessionStorageFactory } from '../../storage/session-storage-factory.service'; +import { Notification } from '../types/Notification'; +import { NotificationAction } from '../types/NotificationAction'; + /** * Service for displaying notifications and modal status dialogs. */ -angular.module('notification').factory('guacNotification', ['$injector', - function guacNotification($injector) { - - // Required services - var $rootScope = $injector.get('$rootScope'); - var requestService = $injector.get('requestService'); - var sessionStorageFactory = $injector.get('sessionStorageFactory'); - - var service = {}; +@Injectable({ + providedIn: 'root' +}) +export class GuacNotificationService { /** * Getter/setter which retrieves or sets the current status notification, * which may simply be false if no status is currently shown. - * - * @type Function */ - var storedStatus = sessionStorageFactory.create(false); + private storedStatus: SessionStorageEntry + = this.sessionStorageFactory.create(false); + + /** + * Inject required services. + */ + constructor(private router: Router, + private requestService: RequestService, + private sessionStorageFactory: SessionStorageFactory) { + + // Hide status upon navigation + this.router.events + .pipe(filter(event => event instanceof NavigationEnd)) + .subscribe(() => this.showStatus(false)); + + } /** * An action to be provided along with the object sent to showStatus which * closes the currently-shown status dialog. - * - * @type NotificationAction */ - service.ACKNOWLEDGE_ACTION = { - name : 'APP.ACTION_ACKNOWLEDGE', - callback : function acknowledgeCallback() { - service.showStatus(false); + readonly ACKNOWLEDGE_ACTION: NotificationAction = { + name : 'APP.ACTION_ACKNOWLEDGE', + callback: () => { + this.showStatus(false); } }; /** * Retrieves the current status notification, which may simply be false if * no status is currently shown. - * - * @type Notification|Boolean */ - service.getStatus = function getStatus() { - return storedStatus(); - }; + getStatus(): Notification | boolean | Object { + return this.storedStatus(); + } /** * Shows or hides the given notification as a modal status. If a status * notification is currently shown, no further statuses will be shown * until the current status is hidden. * - * @param {Notification|Boolean|Object} status + * @param status * The status notification to show. * * @example - * + * * // To show a status message with actions * guacNotification.showStatus({ * 'title' : 'Disconnected', @@ -84,40 +96,32 @@ angular.module('notification').factory('guacNotification', ['$injector', * } * }] * }); - * + * * // To hide the status message * guacNotification.showStatus(false); */ - service.showStatus = function showStatus(status) { - if (!storedStatus() || !status) - storedStatus(status); - }; + showStatus(status: Notification | boolean | Object): void { + if (!this.storedStatus() || !status) + this.storedStatus(status); + } /** - * Promise error callback which displays a modal notification for all - * rejections due to REST errors. The message displayed to the user within + * Observable error callback which displays a modal notification for all + * failures due to REST errors. The message displayed to the user within * the notification is provided by the contents of the @link{Error} object - * within the REST response. All other rejections, such as those due to + * within the REST response. All other errors, such as those due to * JavaScript errors, are logged to the browser console without displaying * any notification. - * - * @constant - * @type Function */ - service.SHOW_REQUEST_ERROR = requestService.createErrorCallback(function showRequestError(error) { - service.showStatus({ - className : 'error', - title : 'APP.DIALOG_HEADER_ERROR', - text : error.translatableMessage, - actions : [ service.ACKNOWLEDGE_ACTION ] + readonly SHOW_REQUEST_ERROR: (error: any) => Observable = this.requestService.createErrorCallback(error => { + this.showStatus({ + className: 'error', + title : 'APP.DIALOG_HEADER_ERROR', + text : error.translatableMessage, + actions : [this.ACKNOWLEDGE_ACTION] }); - }); - // Hide status upon navigation - $rootScope.$on('$routeChangeSuccess', function() { - service.showStatus(false); + return throwError(() => error); }); - return service; - -}]); +} diff --git a/guacamole/src/main/frontend/src/app/notification/styles/modal.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/styles/modal.css similarity index 100% rename from guacamole/src/main/frontend/src/app/notification/styles/modal.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/styles/modal.css diff --git a/guacamole/src/main/frontend/src/app/notification/styles/notification.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/styles/notification.css similarity index 95% rename from guacamole/src/main/frontend/src/app/notification/styles/notification.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/styles/notification.css index f5b99fe0b8..29b0620a27 100644 --- a/guacamole/src/main/frontend/src/app/notification/styles/notification.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/styles/notification.css @@ -67,8 +67,8 @@ height: 100%; width: 0; box-shadow: inset 1px 1px 0 rgba(255, 255, 255, 0.5), - inset -1px -1px 0 rgba( 0, 0, 0, 0.1), - 1px 1px 0 gray; + inset -1px -1px 0 rgba( 0, 0, 0, 0.1), + 1px 1px 0 gray; } .notification .progress { @@ -91,11 +91,11 @@ -webkit-animation-iteration-count: infinite; padding: 0.25em; - + border: 1px solid gray; position: relative; - + } .notification .progress .text { @@ -111,10 +111,14 @@ width: 100%; } -.notification .parameters .fields .labeled-field { +.notification .parameters .fields guac-form-field { display: table-row; } +.notification .parameters .fields .labeled-field { + display: contents; +} + .notification .parameters .fields .field-header, .notification .parameters .fields .form-field { text-align: left; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/Notification.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/Notification.ts new file mode 100644 index 0000000000..62edc8b7d6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/Notification.ts @@ -0,0 +1,113 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Field } from '../../rest/types/Field'; +import { Form } from '../../rest/types/Form'; +import { TranslatableMessage } from '../../rest/types/TranslatableMessage'; +import { NotificationAction } from './NotificationAction'; +import { NotificationCountdown } from './NotificationCountdown'; +import { NotificationProgress } from './NotificationProgress'; + +/** + * Provides the Notification class. + */ +export class Notification { + + /** + * The CSS class to associate with the notification, if any. + */ + className?: string; + + /** + * The title of the notification. + */ + title?: string; + + /** + * The body text of the notification. + */ + text?: TranslatableMessage; + + /** + * The translation namespace of the translation strings that will + * be generated for all fields within the notification. This namespace + * is absolutely required if form fields will be included in the + * notification. + */ + formNamespace?: string; + + /** + * Optional form content to display. This may be a form, an array of + * forms, or a simple array of fields. + */ + forms?: Form[] | Form | Field[] | Field; + + /** + * The object which will receive all field values. Each field value + * will be assigned to the property of this object having the same + * name. + */ + formModel?: Record; + + /** + * The function to invoke when the form is submitted, if form fields + * are present within the notification. + */ + formSubmitCallback?: Function; + + /** + * An array of all actions available to the user in response to this + * notification. + */ + actions: NotificationAction[]; + + /** + * The current progress state of the ongoing action associated with this + * notification. + */ + progress?: NotificationProgress; + + /** + * The countdown and corresponding default action which applies to + * this notification, if any. + */ + countdown?: NotificationCountdown; + + /** + * Creates a new Notification, initializing the properties of that + * Notification with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * Notification. + */ + constructor(template: Partial = {}) { + this.className = template.className; + this.title = template.title; + this.text = template.text; + this.formNamespace = template.formNamespace; + this.forms = template.forms; + this.formModel = template.formModel; + this.formSubmitCallback = template.formSubmitCallback; + this.actions = template.actions || []; + this.progress = template.progress; + this.countdown = template.countdown; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationAction.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationAction.ts new file mode 100644 index 0000000000..6d2618f8ee --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationAction.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Provides the NotificationAction interface, which pairs an arbitrary callback with + * an action name. The name of this action will ultimately be presented to + * the user when the user is prompted to choose among available actions. + */ +export interface NotificationAction { + /** + * The name of this action. + */ + name: string; + + /** + * The callback to call when this action is performed. + */ + callback: () => void; + + /** + * The CSS class associated with this action. + */ + className?: string; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationCountdown.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationCountdown.ts new file mode 100644 index 0000000000..822ffb80cc --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationCountdown.ts @@ -0,0 +1,44 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Provides the NotificationCountdown class, which describes an action that + * should be performed after a specific number of seconds has elapsed. + */ +export class NotificationCountdown { + + /** + * Creates a new NotificationCountdown. + * + * @param text + * The body text of the notification countdown. For the sake of i18n, + * the variable REMAINING should be applied within the translation + * string for formatting plurals, etc. + * + * @param remaining + * The number of seconds remaining in the countdown. After this number + * of seconds elapses, the callback associated with this + * NotificationCountdown will be called. + * + * @param callback + * The callback to call when the countdown elapses. + */ + constructor(public text: string, public remaining: number, public callback?: () => void) { + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationProgress.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationProgress.ts new file mode 100644 index 0000000000..c78b18f2da --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/notification/types/NotificationProgress.ts @@ -0,0 +1,50 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Provides the NotificationProgress class, which describes the current status + * of an operation, and how much of that operation remains to be performed. + */ +export class NotificationProgress { + + /** + * Creates a new NotificationProgress. + * + * @param text + * The text describing the operation progress. For the sake of i18n, + * the variable VALUE should be applied within the translation + * string for formatting plurals, etc., while UNIT should be used + * for the progress unit, if any. + * + * @param value + * The current state of operation progress, as an arbitrary number + * which increases as the operation continues. + * + * @param unit + * The unit of the arbitrary value, if that value has an associated + * unit. + * + * @param ratio + * If known, the current status of the operation as a value between 0 + * and 1 inclusive, where 0 is not yet started, and 1 is complete. + */ + constructor(public text: string, public value: number, public unit?: string, public ratio?: number) { + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.html new file mode 100644 index 0000000000..01a7ea19ad --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.html @@ -0,0 +1,22 @@ + + +
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.ts new file mode 100644 index 0000000000..f4711e55b0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-display/player-display.component.ts @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { + AfterViewInit, + Component, + ElementRef, + Input, + OnChanges, + SimpleChanges, + ViewChild, + ViewEncapsulation +} from '@angular/core'; + +/** + * Component which contains a given Guacamole.Display, automatically scaling + * the display to fit available space. + */ +@Component({ + selector: 'guac-player-display', + templateUrl: './player-display.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class PlayerDisplayComponent implements AfterViewInit, OnChanges { + + /** + * The Guacamole.Display instance which should be displayed within the + * directive. + */ + @Input() display?: Guacamole.Display; + + /** + * The root element of this instance of the guacPlayerDisplay + * directive. + */ + @ViewChild('guacPlayerDisplay') element!: ElementRef; + + /** + * The element which serves as a container for the root element of the + * Guacamole.Display assigned to $scope.display. + */ + @ViewChild('guacPlayerDisplayContainer') container!: ElementRef; + + ngAfterViewInit(): void { + this.addDisplayToContainer(this.display); + } + + /** + * Rescales the Guacamole.Display currently assigned to this.display + * such that it exactly fits within this component's available space. + * If no display is currently assigned or the assigned display is not + * at least 1x1 pixels in size, this function has no effect. + */ + fitDisplay() { + + // Ignore if no display is yet present + if (!this.display) + return; + + const displayWidth = this.display.getWidth(); + const displayHeight = this.display.getHeight(); + + // Ignore if the provided display is not at least 1x1 pixels + if (!displayWidth || !displayHeight) + return; + + // Fit display within available space + this.display.scale(Math.min(this.element.nativeElement.offsetWidth / displayWidth, + this.element.nativeElement.offsetHeight / displayHeight)); + + } + + ngOnChanges(changes: SimpleChanges): void { + + if (changes['display']) { + // Automatically add/remove the Guacamole.Display as this.display is + // updated + const display = changes['display'].currentValue as Guacamole.Display; + const oldDisplay = changes['display'].previousValue as Guacamole.Display; + + // Clear out old display, if any + if (oldDisplay) { + this.container.nativeElement.innerHTML = ''; + // @ts-ignore + oldDisplay.onresize = null; + } + + this.addDisplayToContainer(display); + } + } + + /** + * If a new display is provided, add it to the container, keeping + * its scale in sync with changes to available space and display + * size. + * + * @private + */ + private addDisplayToContainer(display: Guacamole.Display | undefined) { + if (display && this.container?.nativeElement) { + this.container.nativeElement.appendChild(display.getElement()); + display.onresize = this.fitDisplay; + this.fitDisplay(); + } + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.html new file mode 100644 index 0000000000..f3207afe4b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.html @@ -0,0 +1,47 @@ + + +
      + +
      +
      + +
      + +
      + +
      + +
      +
      +
      {{ formatTime(batch.events[0].timestamp) }}
      +
      + {{ event.text }} +
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.ts new file mode 100644 index 0000000000..b7ae482485 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player-text-view/player-text-view.component.ts @@ -0,0 +1,132 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnChanges, signal, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import fuzzysort from 'fuzzysort'; +import { TextBatch } from '../services/key-event-display.service'; +import { PlayerTimeService } from '../services/player-time.service'; + +/** + * Component which plays back session recordings. + */ +@Component({ + selector: 'guac-player-text-view', + templateUrl: './player-text-view.component.html', + encapsulation: ViewEncapsulation.None, + host: { + '[class.fullscreen]': 'fullscreenKeyLog()' + }, + standalone: false +}) +export class PlayerTextViewComponent implements OnChanges { + + /** + * All the batches of text extracted from this recording. + */ + @Input({ required: true }) textBatches!: TextBatch[]; + + /** + * A callback that accepts a timestamp, and seeks the recording to + * that provided timestamp. + */ + @Input({ required: true }) seek!: (timestamp: number) => void; + + /** + * The current position within the recording. + */ + @Input({ required: true }) currentPosition!: number; + + /** + * The phrase to search within the text batches in order to produce the + * filtered list for display. + */ + searchPhrase = ''; + + /** + * The text batches that match the current search phrase, or all + * batches if no search phrase is set. + */ + filteredBatches: TextBatch[] = this.textBatches; + + /** + * Whether or not the key log viewer should be full-screen. False by + * default unless explicitly enabled by user interaction. + */ + fullscreenKeyLog = signal(false); + + /** + * @borrows PlayerTimeService.formatTime + */ + formatTime = this.playerTimeService.formatTime; + + /** + * Inject required services. + */ + constructor(private readonly playerTimeService: PlayerTimeService) { + } + + /** + * Toggle whether the key log viewer should take up the whole screen. + */ + toggleKeyLogFullscreen() { + this.fullscreenKeyLog.update(fullscreen => !fullscreen); + } + + /** + * Filter the provided text batches using the provided search phrase to + * generate the list of filtered batches, or set to all provided + * batches if no search phrase is provided. + * + * @param searchPhrase + * The phrase to search the text batches for. If no phrase is + * provided, the list of batches will not be filtered. + */ + private applyFilter(searchPhrase: string): void { + + // If there's search phrase entered, search the text within the + // batches for it + if (searchPhrase) + this.filteredBatches = fuzzysort.go( + searchPhrase, this.textBatches, { key: 'simpleValue' }) + .map(result => result.obj); + + // Otherwise, do not filter the batches + else + this.filteredBatches = this.textBatches; + + } + + /** + * React to changes to the search phrase and the text batches. + */ + ngOnChanges(changes: SimpleChanges): void { + + // Reapply the current filter to the updated text batches + if (changes['textBatches']) { + this.applyFilter(this.searchPhrase); + } + + // Reapply the filter whenever the search phrase is updated + if (changes['searchPhrase']) { + this.applyFilter(changes['searchPhrase'].currentValue); + } + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.html new file mode 100644 index 0000000000..c36dc063f6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.html @@ -0,0 +1,105 @@ + + +
      + + + + + + + +
      + + +
      + + + + +
      + + + + + + + + +
      + {{ 'PLAYER.INFO_FRAME_EVENTS_LEGEND' | transloco }} + {{ 'PLAYER.INFO_KEY_EVENTS_LEGEND' | transloco }} +
      +
      + +
      + + + + + + + + + + {{ formatTime(playbackPosition) }} / {{ formatTime(recording.getDuration()) }} + + + + {{ 'PLAYER.ACTION_SHOW_KEY_LOG' | transloco }} + + + + {{ 'PLAYER.INFO_NO_KEY_LOG' | transloco }} + +
      + +
      + + +
      + +

      {{ operationMessage | transloco }}

      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.ts new file mode 100644 index 0000000000..7ff34229b7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.component.ts @@ -0,0 +1,552 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { + Component, + HostListener, + Input, + OnChanges, + OnDestroy, + signal, + SimpleChanges, + ViewEncapsulation +} from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import _ from 'lodash'; +import { GuacFrontendEventArguments } from '../events/types/GuacFrontendEventArguments'; +import { KeyEventDisplayService, TextBatch } from './services/key-event-display.service'; +import { PlayerHeatmapService } from './services/player-heatmap.service'; +import { PlayerTimeService } from './services/player-time.service'; + +/** + * The number of milliseconds after the last detected mouse activity after + * which the associated CSS class should be removed. + */ +const MOUSE_CLEANUP_DELAY = 4000; + +/** + * The number of milliseconds after the last detected mouse activity before + * the cleanup timer to remove the associated CSS class should be scheduled. + */ +const MOUSE_DEBOUNCE_DELAY = 250; + +/** + * The number of milliseconds, after the debounce delay, before the mouse + * activity cleanup timer should run. + */ +const MOUSE_CLEANUP_TIMER_DELAY = MOUSE_CLEANUP_DELAY - MOUSE_DEBOUNCE_DELAY; + +/** + * Component which plays back session recordings. This directive emits the + * following events based on state changes within the current recording: + * + * "guacPlayerLoading": + * A new recording has been selected and is now loading. + * + * "guacPlayerError": + * The current recording cannot be loaded or played due to an error. + * The recording may be unreadable (lack of permissions) or corrupt + * (protocol error). + * + * "guacPlayerProgress" + * Additional data has been loaded for the current recording and the + * recording's duration has changed. The new duration in milliseconds + * and the number of bytes loaded so far are passed to the event. + * + * "guacPlayerLoaded" + * The current recording has finished loading. + * + * "guacPlayerPlay" + * Playback of the current recording has started or has been resumed. + * + * "guacPlayerPause" + * Playback of the current recording has been paused. + * + * "guacPlayerSeek" + * The playback position of the current recording has changed. The new + * position within the recording is passed to the event as the number + * of milliseconds since the start of the recording. + */ +@Component({ + selector: 'guac-player', + templateUrl: './player.component.html', + encapsulation: ViewEncapsulation.None, + host: { + '[class.recent-mouse-movement]': 'recentMouseMovement()' + }, + standalone: false +}) +export class PlayerComponent implements OnChanges, OnDestroy { + + /** + * A Blob containing the Guacamole session recording to load. + */ + @Input('guacSrc') src?: Blob | Guacamole.Tunnel; + + /** + * Guacamole.SessionRecording instance to be used to playback the + * session recording given via $scope.src. If the recording has not + * yet been loaded, this will be null. + */ + recording: Guacamole.SessionRecording | null = null; + + /** + * The current playback position, in milliseconds. If a seek request is + * in progress, this will be the desired playback position of the + * pending request. + */ + playbackPosition = 0; + + /** + * The key of the translation string that describes the operation + * currently running in the background, or null if no such operation is + * running. + */ + operationMessage: string | null = null; + + /** + * The current progress toward completion of the operation running in + * the background, where 0 represents no progress and 1 represents full + * completion. If no such operation is running, this value has no + * meaning. + */ + operationProgress = 0; + + /** + * The position within the recording of the current seek operation, in + * milliseconds. If a seek request is not in progress, this will be + * undefined. + */ + seekPosition: number | null = null; + + /** + * Any batches of text typed during the recording. + */ + textBatches: TextBatch[] = []; + + /** + * Whether or not the key log viewer should be displayed. False by + * default unless explicitly enabled by user interaction. + */ + showKeyLog = false; + + /** + * The height, in pixels, of the SVG heatmap paths. Note that this is not + * necessarily the actual rendered height, just the initial size of the + * SVG path before any styling is applied. + */ + HEATMAP_HEIGHT = 100; + + /** + * The width, in pixels, of the SVG heatmap paths. Note that this is not + * necessarily the actual rendered width, just the initial size of the + * SVG path before any styling is applied. + */ + HEATMAP_WIDTH = 1000; + + /** + * The maximum number of key events per millisecond to display in the + * key event heatmap. Any key event rates exceeding this value will be + * capped at this rate to ensure that unsually large spikes don't make + * swamp the rest of the data. + * + * Note: This is 6 keys per second (events include both presses and + * releases) - equivalent to ~88 words per minute typed. + */ + private readonly KEY_EVENT_RATE_CAP = 12 / 1000; + + /** + * The maximum number of frames per millisecond to display in the + * frame heatmap. Any frame rates exceeding this value will be + * capped at this rate to ensure that unsually large spikes don't make + * swamp the rest of the data. + */ + private readonly FRAME_RATE_CAP = 10 / 1000; + + /** + * An SVG path describing a smoothed curve that visualizes the relative + * number of frames rendered throughout the recording - i.e. a heatmap + * of screen updates. + */ + frameHeatmap = ''; + + /** + * An SVG path describing a smoothed curve that visualizes the relative + * number of key events recorded throughout the recording - i.e. a + * heatmap of key events. + */ + keyHeatmap = ''; + + /** + * Whether a seek request is currently in progress. A seek request is + * in progress if the user is attempting to change the current playback + * position (the user is manipulating the playback position slider). + */ + private pendingSeekRequest = false; + + /** + * Whether playback should be resumed (play() should be invoked on the + * recording) once the current seek request is complete. This value + * only has meaning if a seek request is pending. + */ + private resumeAfterSeekRequest = false; + + /** + * A scheduled timer to clean up the mouse activity CSS class, or null + * if no timer is scheduled. + */ + private mouseActivityTimer: number | null = null; + + /** + * The recording-relative timestamp of each frame of the recording that + * has been processed so far. + */ + private frameTimestamps: number[] = []; + + /** + * The recording-relative timestamp of each text event that has been + * processed so far. + */ + private keyTimestamps: number[] = []; + + /** + * Whether the mouse has moved recently. + */ + recentMouseMovement = signal(false); + + /** + * The operation that should be performed when the cancel button is + * clicked. + */ + cancelOperation: () => void = () => { + }; + + /** + * Inject required services. + */ + constructor(private guacEventService: GuacEventService, + private keyEventDisplayService: KeyEventDisplayService, + private playerHeatmapService: PlayerHeatmapService, + private playerTimeService: PlayerTimeService) { + } + + /** + * Return true if any batches of key event logs are available for this + * recording, or false otherwise. + * + * @return + * True if any batches of key event logs are avaiable for this + * recording, or false otherwise. + */ + hasTextBatches(): boolean { + return this.textBatches.length >= 0; + } + + /** + * Toggle the visibility of the text key log viewer. + */ + toggleKeyLogView(): void { + this.showKeyLog = !this.showKeyLog; + } + + /** + * @borrows PlayerTimeService.formatTime + */ + formatTime = this.playerTimeService.formatTime; + + /** + * Pauses playback and decouples the position slider from current + * playback position, allowing the user to manipulate the slider + * without interference. Playback state will be resumed following a + * call to commitSeekRequest(). + */ + beginSeekRequest(): void { + + // If a recording is present, pause and save state if we haven't + // already done so + if (this.recording && !this.pendingSeekRequest) { + this.resumeAfterSeekRequest = this.recording.isPlaying(); + this.recording.pause(); + } + + // Flag seek request as in progress + this.pendingSeekRequest = true; + + } + + /** + * Restores the playback state at the time beginSeekRequest() was + * called and resumes coupling between the playback position slider and + * actual playback position. + */ + commitSeekRequest(): void { + + // If a recording is present and there is an active seek request, + // restore the playback state at the time that request began and + // begin seeking to the requested position + if (this.recording && this.pendingSeekRequest) + this.seekToPlaybackPosition(); + + // Flag seek request as completed + this.pendingSeekRequest = false; + + } + + /** + * Seek the recording to the specified position within the recording, + * in milliseconds. + * + * @param timestamp + * The position to seek to within the current record, + * in milliseconds. + */ + seekToTimestamp(timestamp: number): void { + + // Set the timestamp and seek to it + this.playbackPosition = timestamp; + this.seekToPlaybackPosition(); + + } + + /** + * Seek the recording to the current playback position value. + */ + seekToPlaybackPosition() { + + this.seekPosition = null; + this.operationMessage = 'PLAYER.INFO_SEEK_IN_PROGRESS'; + this.operationProgress = 0; + + // Cancel seek when requested, updating playback position if + // that position changed + this.cancelOperation = () => { + this.recording!.cancel(); + this.playbackPosition = this.seekPosition || this.playbackPosition; + }; + + this.resumeAfterSeekRequest && this.recording!.play(); + this.recording!.seek(this.playbackPosition, () => { + this.seekPosition = null; + this.operationMessage = null; + }); + } + + /** + * Toggles the current playback state. If playback is currently paused, + * playback is resumed. If playback is currently active, playback is + * paused. If no recording has been loaded, this function has no + * effect. + */ + togglePlayback(): void { + if (this.recording) { + if (this.recording.isPlaying()) + this.recording.pause(); + else + this.recording.play(); + } + } + + ngOnChanges(changes: SimpleChanges) { + if (changes['src']) { + + // Reset position and seek state + this.pendingSeekRequest = false; + this.playbackPosition = 0; + + // Stop loading the current recording, if any + if (this.recording) { + this.recording.pause(); + this.recording.abort(); + } + + // If no recording is provided, reset to empty + if (!this.src) + this.recording = null; + + // Otherwise, begin loading the provided recording + else { + + this.recording = new Guacamole.SessionRecording(this.src); + + // Begin downloading the recording + this.recording.connect(); + + // Notify listeners and set any heatmap paths + // when the recording is completely loaded + this.recording.onload = () => { + this.operationMessage = null; + this.guacEventService.broadcast('guacPlayerLoaded'); + + const recordingDuration = this.recording!.getDuration(); + + // Generate heat maps for rendered frames and typed text + this.frameHeatmap = ( + this.playerHeatmapService.generateHeatmapPath( + this.frameTimestamps, recordingDuration, this.FRAME_RATE_CAP, + this.HEATMAP_HEIGHT, this.HEATMAP_WIDTH)); + this.keyHeatmap = ( + this.playerHeatmapService.generateHeatmapPath( + this.keyTimestamps, recordingDuration, this.KEY_EVENT_RATE_CAP, + this.HEATMAP_HEIGHT, this.HEATMAP_WIDTH)); + }; + + // Notify listeners if an error occurs + this.recording.onerror = (message) => { + this.operationMessage = null; + this.guacEventService.broadcast('guacPlayerError', { message }); + }; + + // Notify listeners when additional recording data has been + // loaded + this.recording.onprogress = (duration, current) => { + this.operationProgress = (this.src as unknown as any)?.size ? current / (this.src as unknown as any).size : 0; + this.guacEventService.broadcast('guacPlayerProgress', { duration, current }); + + // Store the timestamp of the just-received frame + this.frameTimestamps.push(duration); + }; + + // Notify listeners when playback has started/resumed + this.recording.onplay = () => { + this.guacEventService.broadcast('guacPlayerPlay'); + }; + + // Notify listeners when playback has paused + this.recording.onpause = () => { + this.guacEventService.broadcast('guacPlayerPause'); + }; + + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + // Extract key events from the recording + this.recording.onkeyevents = (events) => { + + // Convert to a display-optimized format + this.textBatches = ( + this.keyEventDisplayService.parseEvents(events)); + + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + this.keyTimestamps = events.map(event => event.timestamp); + + }; + + // Notify listeners when current position within the recording + // has changed + this.recording.onseek = (position, current, total) => { + + // Update current playback position while playing + if (this.recording?.isPlaying()) + this.playbackPosition = position; + + // Update seek progress while seeking + else { + this.seekPosition = position; + this.operationProgress = current / total; + } + + this.guacEventService.broadcast('guacPlayerSeek', { position }); + + }; + + this.operationMessage = 'PLAYER.INFO_LOADING_RECORDING'; + this.operationProgress = 0; + + this.cancelOperation = () => { + this.recording?.abort(); + this.operationMessage = null; + }; + + this.guacEventService.broadcast('guacPlayerLoading'); + } + } + } + + /** + * Clean up resources when player is destroyed + */ + ngOnDestroy() { + this.recording?.pause(); + this.recording?.abort(); + this.mouseActivityTimer !== null && window.clearTimeout(this.mouseActivityTimer); + } + + /** + * Clean up the mouse movement class after no mouse activity has been + * detected for the appropriate time period. + */ + private readonly scheduleCleanupTimeout = _.debounce(() => + this.mouseActivityTimer = window.setTimeout(() => { + this.mouseActivityTimer = null; + this.recentMouseMovement.set(false); + }, MOUSE_CLEANUP_TIMER_DELAY), + + /* + * Only schedule the cleanup task after the mouse hasn't moved + * for a reasonable amount of time to ensure that the number of + * created cleanup timers remains reasonable. + */ + MOUSE_DEBOUNCE_DELAY); + + /* + * When the mouse moves inside the player, add a CSS class signifying + * recent mouse movement, to be automatically cleaned up after a period + * of time with no detected mouse movement. + */ + @HostListener('mousemove') + onMouseMove() { + + // Clean up any existing cleanup timer + if (this.mouseActivityTimer !== null) { + window.clearTimeout(this.mouseActivityTimer); + this.mouseActivityTimer = null; + } + + // Add the marker CSS class, and schedule its removal + this.recentMouseMovement.set(true); + this.scheduleCleanupTimeout(); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/player/playerModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.module.ts similarity index 68% rename from guacamole/src/main/frontend/src/app/player/playerModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.module.ts index c3bf3ea4ad..f2ee1eca45 100644 --- a/guacamole/src/main/frontend/src/app/player/playerModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/player.module.ts @@ -44,9 +44,34 @@ * THE SOFTWARE. */ -/** - * Module providing in-browser playback of session recordings. - */ -angular.module('player', [ - 'element' -]); +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ElementModule } from 'guacamole-frontend-lib'; +import { PlayerDisplayComponent } from './player-display/player-display.component'; +import { PlayerTextViewComponent } from './player-text-view/player-text-view.component'; +import { PlayerComponent } from './player.component'; +import { ProgressIndicatorComponent } from './progress-indicator/progress-indicator.component'; + +@NgModule({ + declarations: [ + PlayerComponent, + PlayerDisplayComponent, + ProgressIndicatorComponent, + PlayerTextViewComponent + ], + imports : [ + CommonModule, + FormsModule, + ElementModule, + TranslocoModule + ], + exports : [ + PlayerComponent, + PlayerDisplayComponent, + ProgressIndicatorComponent + ] +}) +export class PlayerModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.html new file mode 100644 index 0000000000..eb82f8e14c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.html @@ -0,0 +1,31 @@ + + +
      {{ percentage }}%
      +
      +
      +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.spec.ts new file mode 100644 index 0000000000..f782cbca0d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { ProgressIndicatorComponent } from './progress-indicator.component'; + +describe('ProgressIndicatorComponent', () => { + let component: ProgressIndicatorComponent; + let fixture: ComponentFixture; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [ProgressIndicatorComponent] + }); + fixture = TestBed.createComponent(ProgressIndicatorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.ts index 8a793fa7dc..5f504cecc6 100644 --- a/guacamole/src/main/frontend/src/app/player/directives/progressIndicator.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/progress-indicator/progress-indicator.component.ts @@ -44,58 +44,46 @@ * THE SOFTWARE. */ +import { Component, Input, OnChanges, SimpleChanges, ViewEncapsulation } from '@angular/core'; + /** - * Directive which displays an indicator showing the current progress of an + * Component which displays an indicator showing the current progress of an * arbitrary operation. */ -angular.module('player').directive('guacPlayerProgressIndicator', [function guacPlayerProgressIndicator() { - - const config = { - restrict : 'E', - templateUrl : 'app/player/templates/progressIndicator.html' - }; - - config.scope = { - - /** - * A value between 0 and 1 inclusive which indicates current progress, - * where 0 represents no progress and 1 represents finished. - * - * @type {Number} - */ - progress : '=' - - }; - - config.controller = ['$scope', function guacPlayerProgressIndicatorController($scope) { - - /** - * The current progress of the operation as a percentage. This value is - * automatically updated as $scope.progress changes. - * - * @type {Number} - */ - $scope.percentage = 0; - - /** - * The CSS transform which should be applied to the bar portion of the - * progress indicator. This value is automatically updated as - * $scope.progress changes. - * - * @type {String} - */ - $scope.barTransform = null; +@Component({ + selector: 'guac-player-progress-indicator', + templateUrl: './progress-indicator.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ProgressIndicatorComponent implements OnChanges { - // Keep percentage and bar transform up-to-date with changes to - // progress value - $scope.$watch('progress', function progressChanged(progress) { - progress = progress || 0; - $scope.percentage = Math.floor(progress * 100); - $scope.barTransform = 'rotate(' + (360 * progress - 45) + 'deg)'; - }); + /** + * A value between 0 and 1 inclusive which indicates current progress, + * where 0 represents no progress and 1 represents finished. + */ + @Input() progress?: number; - }]; + /** + * The current progress of the operation as a percentage. This value is + * automatically updated as this.progress changes. + */ + percentage = 0; - return config; + /** + * The CSS transform which should be applied to the bar portion of the + * progress indicator. This value is automatically updated as + * this.progress changes. + */ + barTransform?: string = undefined; -}]); + ngOnChanges(changes: SimpleChanges): void { + if (changes['progress']) { + // Keep percentage and bar transform up-to-date with changes to + // progress value + const progress = this.progress || 0; + this.percentage = Math.floor(progress * 100); + this.barTransform = 'rotate(' + (360 * progress - 45) + 'deg)'; + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/key-event-display.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/key-event-display.service.ts new file mode 100644 index 0000000000..5dd85b2910 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/key-event-display.service.ts @@ -0,0 +1,375 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { Injectable } from '@angular/core'; +import _ from 'lodash'; + +/** + * A set of all keysyms corresponding to modifier keys. + * @type{Object.} + */ +const MODIFIER_KEYS = { + 0xFE03: true, // AltGr + 0xFFE1: true, // Left Shift + 0xFFE2: true, // Right Shift + 0xFFE3: true, // Left Control + 0xFFE4: true, // Right Control, + 0xFFE7: true, // Left Meta + 0xFFE8: true, // Right Meta + 0xFFE9: true, // Left Alt + 0xFFEA: true, // Right Alt + 0xFFEB: true, // Left Super + 0xFFEC: true, // Right Super + 0xFFED: true, // Left Hyper + 0xFFEE: true // Right Super +}; + +/** + * A set of all keysyms for which the name should be printed alongside the + * value of the key itself. + */ +const PRINT_NAME_TOO_KEYS: Record = { + 0xFF09: true, // Tab + 0xFF0D: true, // Return + 0xFF8D: true, // Enter +}; + +/** + * A set of all keysyms corresponding to keys commonly used in shortcuts. + */ +const SHORTCUT_KEYS: Record = { + 0xFFE3: true, // Left Control + 0xFFE4: true, // Right Control, + 0xFFE7: true, // Left Meta + 0xFFE8: true, // Right Meta + 0xFFE9: true, // Left Alt + 0xFFEA: true, // Right Alt + 0xFFEB: true, // Left Super + 0xFFEC: true, // Right Super + 0xFFED: true, // Left Hyper + 0xFFEE: true // Right Super +}; + +/** + * Format and return a key name for display. + * + * @param {*} name + * The name of the key + * + * @returns + * The formatted key name. + */ +const formatKeyName = (name: any) => ('<' + name + '>'); + +/** + * A batch of text associated with a recording. The batch consists of a + * string representation of the text that would be typed based on the key + * events in the recording, as well as a timestamp when the batch started. + */ +export class TextBatch { + + /** + * All key events for this batch, some of which may be consolidated, + * representing multiple raw events. + */ + events: ConsolidatedKeyEvent[]; + + /** + * The simplified, human-readable value representing the key events for + * this batch, equivalent to concatenating the `text` field of all key + * events in the batch. + */ + simpleValue: string; + + /** + * @param [template={}] + * The object whose properties should be copied within the new TextBatch. + */ + constructor(template: Partial = {}) { + + this.events = template?.events || []; + this.simpleValue = template.simpleValue || ''; + + } + +} + +/** + * A granular description of an extracted key event or sequence of events. + * It may contain multiple contiguous events of the same type, meaning that all + * event(s) that were combined into this event must have had the same `typed` + * field value. A single timestamp for the first combined event will be used + * for the whole batch if consolidated. + */ +class ConsolidatedKeyEvent { + + /** + * A human-readable representation of the event(s). If a series of printable + * characters was directly typed, this will just be those character(s). + * Otherwise, it will be a string describing the event(s). + */ + text: string; + + /** + * True if this text of this event is exactly a typed character, or false + * otherwise. + */ + typed: boolean; + + /** + * The timestamp from the recording when this event occurred. + */ + timestamp: number; + + /** + * @param template + * The object whose properties should be copied within the new KeyEventBatch. + */ + constructor(template: ConsolidatedKeyEvent) { + + this.text = template.text; + this.typed = template.typed; + this.timestamp = template.timestamp; + + } + +} + +/** + * A service for translating parsed key events in the format produced by + * KeyEventInterpreter into display-optimized text batches. + */ +@Injectable({ + providedIn: 'root' +}) +export class KeyEventDisplayService { + + /** + * Accepts key events in the format produced by KeyEventInterpreter and returns + * human-readable text batches, seperated by at least `batchSeperation` milliseconds + * if provided. + * + * NOTE: The event processing logic and output format is based on the `guaclog` + * tool, with the addition of batching support. + * + * @param rawEvents + * The raw key events to prepare for display. + * + * @param [batchSeperation=5000] + * The minimum number of milliseconds that must elapse between subsequent + * batches of key-event-generated text. If 0 or negative, no splitting will + * occur, resulting in a single batch for all provided key events. + * + * @param [consolidateEvents=false] + * Whether consecutive sequences of events with similar properties + * should be consolidated into a single ConsolidatedKeyEvent object for + * display performance reasons. + */ + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + parseEvents(rawEvents: Guacamole.KeyEventInterpreter.KeyEvent[], batchSeperation = 5000, consolidateEvents = false): TextBatch[] { + + // Default to 5 seconds if the batch separation was not provided + if (batchSeperation === undefined || batchSeperation === null) + batchSeperation = 5000; + /** + * A map of X11 keysyms to a KeyDefinition object, if the corresponding + * key is currently pressed. If a keysym has no entry in this map at all + * it means that the key is not being pressed. Note that not all keysyms + * are necessarily tracked within this map - only those that are + * explicitly tracked. + */ + const pressedKeys = {}; + + // The timestamp of the most recent key event processed + let lastKeyEvent = 0; + + // All text batches produced from the provided raw key events + const batches = [new TextBatch()]; + + // Process every provided raw + _.forEach(rawEvents, event => { + + // Extract all fields from the raw event + const { definition, pressed, timestamp } = event; + const { keysym, name, value } = definition; + + // Only switch to a new batch of text if sufficient time has passed + // since the last key event + const newBatch = (batchSeperation >= 0 + && (timestamp - lastKeyEvent) >= batchSeperation); + lastKeyEvent = timestamp; + + if (newBatch) + batches.push(new TextBatch()); + + const currentBatch = _.last(batches); + + /** + * Either push the a new event constructed using the provided fields + * into the latest batch, or consolidate into the latest event as + * appropriate given the consolidation configuration and event type. + * + * @param text + * The text representation of the event. + * + * @param typed + * Whether the text value would be literally produced by typing + * the key that produced the event. + */ + const pushEvent = (text: string, typed: boolean) => { + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + const latestEvent = _.last(currentBatch.events); + + // Only consolidate the event if configured to do so and it + // matches the type of the previous event + if (consolidateEvents && latestEvent && latestEvent.typed === typed) { + latestEvent.text += text; + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + currentBatch.simpleValue += text; + } + + // Otherwise, push a new event + else { + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + currentBatch.events.push(new ConsolidatedKeyEvent({ + text, typed, timestamp + })); + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + currentBatch.simpleValue += text; + } + }; + + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + // Track modifier state + if (MODIFIER_KEYS[keysym]) { + if (pressed) + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + pressedKeys[keysym] = definition; + else + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + delete pressedKeys[keysym]; + } + + // Append to the current typed value when a printable + // (non-modifier) key is pressed + else if (pressed) { + + // If any shorcut keys are currently pressed + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + if (_.some(pressedKeys, (def, key) => SHORTCUT_KEYS[key])) { + + let shortcutText = '<'; + + let firstKey = true; + + // Compose entry by inspecting the state of each tracked key. + // At least one key must be pressed when in a shortcut. + for (const pressedKeysym in pressedKeys) { + + // @ts-ignore TODO: Remove when guacamole-common-js 1.6.0 is released and the types are updated + const pressedKeyDefinition = pressedKeys[pressedKeysym]; + + // Print name of key + if (firstKey) { + shortcutText += pressedKeyDefinition.name; + firstKey = false; + } else + shortcutText += ('+' + pressedKeyDefinition.name); + + } + + // Finally, append the printable key to close the shortcut + shortcutText += ('+' + name + '>'); + + // Add the shortcut to the current batch + pushEvent(shortcutText, false); + } + } + + // Print the key itself + else { + + let keyText; + let typed; + + // Print the value if explicitly defined + if (value !== undefined) { + + keyText = value; + typed = true; + + // If the name should be printed in addition, add it as a + // seperate event before the actual character value + if (PRINT_NAME_TOO_KEYS[keysym]) + pushEvent(formatKeyName(name), false); + + } + + // Otherwise print the name + else { + + keyText = formatKeyName(name); + + // While this is a representation for a single character, + // the key text is the name of the key, not the actual + // character itself + typed = false; + + } + + // Add the key to the current batch + pushEvent(keyText, typed); + + } + + }); + + // All processed batches + return batches; + + } + +} diff --git a/guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-heatmap.service.ts similarity index 83% rename from guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-heatmap.service.ts index 9259aa4dc5..43ba205cdb 100644 --- a/guacamole/src/main/frontend/src/app/player/services/playerHeatmapService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-heatmap.service.ts @@ -4,56 +4,57 @@ * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the - * 'License'); you may not use this file except in compliance + * "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 + * "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. */ -import { curveCatmullRom } from 'd3-shape'; +import { Injectable } from '@angular/core'; import { path } from 'd3-path'; +import { curveCatmullRom } from 'd3-shape'; +import _ from 'lodash'; /** - * A service for generating heat maps of activity levels per time interval, - * for session recording playback. + * A default, relatively-gentle Gaussian smoothing kernel. This kernel + * should help heatmaps look a bit less jagged, while not reducing fidelity + * very much. */ -angular.module('player').factory('playerHeatmapService', [() => { +const GAUSSIAN_KERNEL = [0.0013, 0.1573, 0.6827, 0.1573, 0.0013]; - /** - * A default, relatively-gentle Gaussian smoothing kernel. This kernel - * should help heatmaps look a bit less jagged, while not reducing fidelity - * very much. - * - * @type {!number[]} - */ - const GAUSSIAN_KERNEL = [0.0013, 0.1573, 0.6827, 0.1573, 0.0013]; +/** + * The number of buckets that a series of activity timestamps should be + * divided into. + */ +const NUM_BUCKETS = 100; - /** - * The number of buckets that a series of activity timestamps should be - * divided into. - * - * @type {!number} - */ - const NUM_BUCKETS = 100; +/** + * A service for generating heat maps of activity levels per time interval, + * for session recording playback. + */ +@Injectable({ + providedIn: 'root' +}) +export class PlayerHeatmapService { /** * Given a list of values to smooth out, produce a smoothed data set with * the same length as the original provided list. * - * @param {!number[]} values + * @param values * The list of histogram values to smooth out. * - * @returns {!number[]} + * @returns * The smoothed value array. */ - function smooth(values) { + private smooth(values: number[]): number[] { // The starting offset into the values array for each calculation const lookBack = Math.floor(GAUSSIAN_KERNEL.length / 2); @@ -73,7 +74,7 @@ angular.module('player').factory('playerHeatmapService', [() => { // If the contribution to the final smoothed value would be outside // the bounds of the array, just use the original value instead const contribution = ((valuesIndex >= 0) && valuesIndex < values.length) - ? values[valuesIndex] : value; + ? values[valuesIndex] : value; // Use the provided weight from the kernel and add to the total return total + (contribution * weight); @@ -88,29 +89,29 @@ angular.module('player').factory('playerHeatmapService', [() => { * during a bucket of time, generate a smooth curve, scaled to PATH_HEIGHT * height, and PATH_WIDTH width. * - * @param {!number[]} bucketizedData + * @param bucketizedData * The bucketized counts to create an SVG path from. * - * @param {!number} maxBucketValue + * @param maxBucketValue * The size of the largest value in the bucketized data. * - * @param {!number} height + * @param height * The target height, in pixels, of the highest point in the heatmap. * - * @param {!number} width + * @param width * The target width, in pixels, of the heatmap. * - * @returns {!string} + * @returns * An SVG path representing a smooth curve, passing through all points * in the provided data. */ - function createPath(bucketizedData, maxBucketValue, height, width) { + private createPath(bucketizedData: number[], maxBucketValue: number, height: number, width: number): string { // Calculate scaling factor to ensure that paths are all the same heigh const yScalingFactor = height / maxBucketValue; // Scale a given Y value appropriately - const scaleYValue = yValue => height - (yValue * yScalingFactor); + const scaleYValue = (yValue: number) => height - (yValue * yScalingFactor); // Calculate scaling factor to ensure that paths are all the same width const xScalingFactor = width / bucketizedData.length; @@ -164,42 +165,40 @@ angular.module('player').factory('playerHeatmapService', [() => { return startAtOrigin + strippedPathText; } - const service = {}; - /** * Given a raw array of timestamps indicating when events of a certain type * occured during a record, generate and return a smoothed SVG path * indicating how many events occured during each equal-length bucket. * - * @param {!number[]} timestamps + * @param timestamps * A raw array of timestamps, one for every relevant event. These * must be monotonically increasing. * - * @param {!number} duration + * @param duration * The duration over which the heatmap should apply. This value may * be greater than the maximum timestamp value, in which case the path * will drop to 0 after the last timestamp in the provided array. * - * @param {number} maxRate + * @param maxRate * The maximum number of events per millisecond that should be displayed * in the final path. Any rates over this amount will just be capped at * this value. * - * @param {!number} height + * @param height * The target height, in pixels, of the highest point in the heatmap. * - * @param {!number} width + * @param width * The target width, in pixels, of the heatmap. * - * @returns {!string} + * @returns * A smoothed, graphable SVG path representing levels of activity over * time, as extracted from the provided timestamps. */ - service.generateHeatmapPath = (timestamps, duration, maxRate, height, width) => { + generateHeatmapPath = (timestamps: number[], duration: number, maxRate: number, height: number, width: number): string => { // The height and width must both be valid in order to create the path if (!height || !width) { - console.warn("Heatmap height and width must be positive."); + console.warn('Heatmap height and width must be positive.'); return ''; } @@ -251,14 +250,10 @@ angular.module('player').factory('playerHeatmapService', [() => { }); // Smooth the data for better aesthetics before creating the path - const smoothed = smooth(buckets); + const smoothed = this.smooth(buckets); // Create an SVG path based on the smoothed data - return createPath(smoothed, maxBucketValue, height, width); - - } - - - return service; + return this.createPath(smoothed, maxBucketValue, height, width); -}]); + }; +} diff --git a/guacamole/src/main/frontend/src/app/player/services/playerTimeService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-time.service.ts similarity index 50% rename from guacamole/src/main/frontend/src/app/player/services/playerTimeService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-time.service.ts index 919c4eec95..4c89f90f58 100644 --- a/guacamole/src/main/frontend/src/app/player/services/playerTimeService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/services/player-time.service.ts @@ -4,69 +4,72 @@ * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the - * 'License'); you may not use this file except in compliance + * "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 + * "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. */ - /* - * NOTE: This session recording player implementation is based on the Session - * Recording Player for Glyptodon Enterprise which is available at - * https://github.com/glyptodon/glyptodon-enterprise-player under the - * following license: - * - * Copyright (C) 2019 Glyptodon, Inc. - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN - * THE SOFTWARE. - */ +/* + * NOTE: This session recording player implementation is based on the Session + * Recording Player for Glyptodon Enterprise which is available at + * https://github.com/glyptodon/glyptodon-enterprise-player under the + * following license: + * + * Copyright (C) 2019 Glyptodon, Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ + +import { Injectable } from '@angular/core'; + /** * A service for formatting time, specifically for the recording player. */ -angular.module('player').factory('playerTimeService', - ['$injector', function playerTimeService($injector) { - - const service = {}; +@Injectable({ + providedIn: 'root' +}) +export class PlayerTimeService { /** * Formats the given number as a decimal string, adding leading zeroes * such that the string contains at least two digits. The given number * MUST NOT be negative. * - * @param {!number} value + * @param value * The number to format. * - * @returns {!string} + * @returns * The decimal string representation of the given value, padded * with leading zeroes up to a minimum length of two digits. */ - const zeroPad = function zeroPad(value) { - return value > 9 ? value : '0' + value; - }; + private zeroPad(value: number): string { + return value > 9 ? value.toString() : '0' + value; + } /** * Formats the given quantity of milliseconds as days, hours, minutes, @@ -76,34 +79,32 @@ angular.module('player').factory('playerTimeService', * groups are zero-padded to two digits with the exception of the * left-most group. * - * @param {!number} value + * @param value * The time to format, in milliseconds. * - * @returns {!string} + * @returns * The given quantity of milliseconds formatted as "DD:HH:MM:SS". */ - service.formatTime = function formatTime(value) { + formatTime(value: number): string { // Round provided value down to whole seconds value = Math.floor((value || 0) / 1000); // Separate seconds into logical groups of seconds, minutes, // hours, etc. - var groups = [ 1, 24, 60, 60 ]; - for (var i = groups.length - 1; i >= 0; i--) { - var placeValue = groups[i]; - groups[i] = zeroPad(value % placeValue); + const groups: (string | number)[] = [1, 24, 60, 60]; + for (let i = groups.length - 1; i >= 0; i--) { + const placeValue = groups[i] as number; + groups[i] = this.zeroPad(value % placeValue); value = Math.floor(value / placeValue); } // Format groups separated by colons, stripping leading zeroes and // groups which are entirely zeroes, leaving at least minutes and // seconds - var formatted = groups.join(':'); - return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)[1]; - - }; + const formatted = groups.join(':'); + return /^[0:]*([0-9]{1,2}(?::[0-9]{2})+)$/.exec(formatted)![1]; - return service; + } -}]); +} diff --git a/guacamole/src/main/frontend/src/app/player/styles/player.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/player.css similarity index 95% rename from guacamole/src/main/frontend/src/app/player/styles/player.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/player.css index a1566810a3..6aaeb78c92 100644 --- a/guacamole/src/main/frontend/src/app/player/styles/player.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/player.css @@ -119,6 +119,18 @@ guac-player .guac-player-controls { cursor: pointer; } +.guac-player-controls .guac-player-buttons { + display: flex; + flex-direction: row; + align-items: center; +} + +.guac-player-controls .guac-player-keys { + margin-left: auto; + padding-right: 0.5em; + cursor: pointer; +} + guac-player .guac-player-status { position: fixed; @@ -204,3 +216,4 @@ guac-player-text-view.fullscreen { } + diff --git a/guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/playerDisplay.css similarity index 100% rename from guacamole/src/main/frontend/src/app/player/styles/playerDisplay.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/playerDisplay.css diff --git a/guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/progressIndicator.css similarity index 100% rename from guacamole/src/main/frontend/src/app/player/styles/progressIndicator.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/progressIndicator.css diff --git a/guacamole/src/main/frontend/src/app/player/styles/seek.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/seek.css similarity index 100% rename from guacamole/src/main/frontend/src/app/player/styles/seek.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/seek.css diff --git a/guacamole/src/main/frontend/src/app/player/styles/textView.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/textView.css similarity index 100% rename from guacamole/src/main/frontend/src/app/player/styles/textView.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/player/styles/textView.css diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.spec.ts new file mode 100644 index 0000000000..2109599925 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.spec.ts @@ -0,0 +1,41 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { ErrorHandlingInterceptor } from './error-handling.interceptor'; + +describe('ErrorHandlingInterceptor', () => { + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + ErrorHandlingInterceptor + ], + }); + + + } + ); + + it('should be created', () => { + const interceptor: ErrorHandlingInterceptor = TestBed.inject(ErrorHandlingInterceptor); + expect(interceptor).toBeTruthy(); + }); + +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.ts new file mode 100644 index 0000000000..a2cd74f229 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/interceptor/error-handling.interceptor.ts @@ -0,0 +1,78 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpErrorResponse, HttpEvent, HttpHandler, HttpInterceptor, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { catchError, Observable, throwError } from 'rxjs'; +import { InterceptorService } from '../../util/interceptor.service'; +import { Error } from '../types/Error'; + +/** + * TODO + */ +@Injectable() +export class ErrorHandlingInterceptor implements HttpInterceptor { + + /** + * Inject required services. + */ + constructor(private interceptorService: InterceptorService) { + } + + /** + * If an HTTP error response is received from the REST API, this interceptor will + * map the response to an Observable that will throw an error strictly with an + * instance of an {@link Error} object. + */ + intercept(request: HttpRequest, next: HttpHandler): Observable> { + + // Skip this interceptor if the corresponding HttpContextToken is set + if (this.interceptorService.skipErrorHandlingInterceptor(request)) { + return next.handle(request); + } + + return next.handle(request) + .pipe( + // Catch any errors and map them to an Error object if the + // received error originates from the REST API + catchError(this.handleError) + ); + } + + /** + * Wraps a HttpErrorResponse into an Error object. + * + * @param error + * The HttpErrorResponse to wrap. + * + * @returns + * An Observable that will emit a single Error object wrapping the given HttpErrorResponse. + */ + private handleError(error: HttpErrorResponse) { + + // Wrap true error responses within REST Error objects + if (error.status !== 0) { + return throwError(() => new Error(error.error)); + } + + // Fall back to a generic internal error if the request couldn't + // even be issued (webapp is down, etc.) + return throwError(() => new Error({ message: 'Unknown failure sending HTTP request' })); + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/restModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/rest.module.ts similarity index 76% rename from guacamole/src/main/frontend/src/app/rest/restModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/rest.module.ts index 23901c07c0..af5ea8282c 100644 --- a/guacamole/src/main/frontend/src/app/rest/restModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/rest.module.ts @@ -17,11 +17,20 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { StorageModule } from '../storage/storage.module'; + /** * The module for code relating to communication with the REST API of the * Guacamole web application. */ -angular.module('rest', [ - 'auth', - 'locale' -]); +@NgModule({ + declarations: [], + imports : [ + CommonModule, + StorageModule + ] +}) +export class RestModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/active-connection.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/active-connection.service.ts new file mode 100644 index 0000000000..1838ca6162 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/active-connection.service.ts @@ -0,0 +1,165 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ActiveConnection } from '../types/ActiveConnection'; +import { UserCredentials } from '../types/UserCredentials'; + +/** + * Service for operating on active connections via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class ActiveConnectionService { + + /** + * Required services + */ + constructor(private http: HttpClient) { + } + + /** + * Makes a request to the REST API to get a single active connection, + * returning an Observable that provides the corresponding + * {@link ActiveConnection} if successful. + * + * @param dataSource + * The identifier of the data source to retrieve the active connection + * from. + * + * @param id + * The identifier of the active connection. + * + * @returns + * An Observable which will emit a @link{ActiveConnection} upon + * success. + */ + getActiveConnection(dataSource: string, id: string): Observable { + + // Retrieve active connection + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections/' + encodeURIComponent(id) + ); + } + + /** + * Makes a request to the REST API to get the list of active tunnels, + * returning an Observable that provides a map of @link{ActiveConnection} + * objects if successful. + * + * @param dataSource + * The identifier of the data source to retrieve the active connections + * from. + * + * @param permissionTypes + * The set of permissions to filter with. A user must have one or more + * of these permissions for an active connection to appear in the + * result. If null, no filtering will be performed. Valid values are + * listed within PermissionSet.ObjectType. + * + * @returns + * An Observable which will emit a map of {@link ActiveConnection} + * objects, where each key is the identifier of the corresponding + * active connection. + */ + getActiveConnections(dataSource: string, permissionTypes?: string[]): Observable> { + + // Add permission filter if specified + let httpParameters = new HttpParams(); + if (permissionTypes) + httpParameters = httpParameters.appendAll({ permission: permissionTypes }); + + // Retrieve tunnels + return this.http.get>( + 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections', + { params: httpParameters } + ); + + } + + /** + * Makes a request to the REST API to delete the active connections having + * the given identifiers, effectively disconnecting them, returning an + * Observable that can be used for processing the results of the call. + * + * @param dataSource + * The identifier of the data source to delete the active connections + * from. + * + * @param identifiers + * The identifiers of the active connections to delete. + * + * @returns + * An Observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteActiveConnections(dataSource: string, identifiers: string[]): Observable { + + // Convert provided array of identifiers to a patch + const activeConnectionPatch: any[] = []; + identifiers.forEach(function addActiveConnectionPatch(identifier) { + activeConnectionPatch.push({ + op : 'remove', + path: '/' + identifier + }); + }); + + // Perform active connection deletion via PATCH + return this.http.patch( + 'api/session/data/' + encodeURIComponent(dataSource) + '/activeConnections', + activeConnectionPatch + ); + + } + + /** + * Makes a request to the REST API to generate credentials which have + * access strictly to the given active connection, using the restrictions + * defined by the given sharing profile, returning an Observable that provides + * the resulting @link{UserCredentials} object if successful. + * + * @param dataSource + * The identifier of the data source to retrieve the active connection + * from. + * + * @param id + * The identifier of the active connection being shared. + * + * @param sharingProfile + * The identifier of the sharing profile dictating the + * semantics/restrictions which apply to the shared session. + * + * @returns + * An Observable which will emit a {@link UserCredentials} object + * upon success. + */ + getSharingCredentials(dataSource: string, id: string, sharingProfile: string): Observable { + + // Generate sharing credentials + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + + '/activeConnections/' + encodeURIComponent(id) + + '/sharingCredentials/' + encodeURIComponent(sharingProfile) + ); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection-group.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection-group.service.ts new file mode 100644 index 0000000000..4e33184819 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection-group.service.ts @@ -0,0 +1,155 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, Observable, tap } from 'rxjs'; +import { ConnectionGroup } from '../types/ConnectionGroup'; + +/** + * Service for operating on connection groups via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class ConnectionGroupService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } + + /** + * Makes a request to the REST API to get an individual connection group + * and all descendants, returning an observable that provides the corresponding + * {@link ConnectionGroup} if successful. Descendant groups and connections + * will be stored as children of that connection group. If a permission + * type is specified, the result will be filtering by that permission. + * + * @param connectionGroupID + * The ID of the connection group to retrieve. If not provided, the + * root connection group will be retrieved by default. + * + * @param permissionTypes + * The set of permissions to filter with. A user must have one or more + * of these permissions for a connection to appear in the result. + * If null, no filtering will be performed. Valid values are listed + * within PermissionSet.ObjectType. + * + * @returns + * An observable which will emit a {@link ConnectionGroup} upon + * success. + */ + getConnectionGroupTree(dataSource: string, connectionGroupID: string = ConnectionGroup.ROOT_IDENTIFIER, permissionTypes?: string[]): Observable { + + // Add permission filter if specified + let httpParameters = new HttpParams(); + if (permissionTypes) + httpParameters = httpParameters.appendAll({ 'permission': permissionTypes }); + + // Retrieve connection group + return this.http.get('api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID) + '/tree', + { + params: httpParameters + }); + + } + + /** + * Makes a request to the REST API to get an individual connection group, + * returning an observable that provides the corresponding + * {@link ConnectionGroup} if successful. + * + * @param [connectionGroupID=ConnectionGroup.ROOT_IDENTIFIER] + * The ID of the connection group to retrieve. If not provided, the + * root connection group will be retrieved by default. + * + * @returns + * An observable which will emit a {@link ConnectionGroup} upon + * success. + */ + getConnectionGroup(dataSource: string, connectionGroupID: string = ConnectionGroup.ROOT_IDENTIFIER): Observable { + + // Retrieve connection group + return this.http.get('api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroupID)); + + } + + /** + * Makes a request to the REST API to save a connection group, returning a + * promise that can be used for processing the results of the call. If the + * connection group is new, and thus does not yet have an associated + * identifier, the identifier will be automatically set in the provided + * connection group upon success. + * + * @param connectionGroup + * The connection group to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + saveConnectionGroup(dataSource: string, connectionGroup: ConnectionGroup): Observable { + + // If connection group is new, add it and set the identifier automatically + if (!connectionGroup.identifier) { + return this.http.post( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups', + connectionGroup + ) + + // Set the identifier on the new connection group + .pipe( + map((newConnectionGroup: ConnectionGroup) => { + connectionGroup.identifier = newConnectionGroup.identifier; + }) + ); + } + + // Otherwise, update the existing connection group + else { + return this.http.put( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier), + connectionGroup + ); + } + + } + + /** + * Makes a request to the REST API to delete a connection group, returning + * an observable that can be used for processing the results of the call. + * + * @param connectionGroup + * The connection group to delete. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteConnectionGroup(dataSource: string, connectionGroup: ConnectionGroup): Observable { + + // Delete connection group + return this.http.delete('api/session/data/' + encodeURIComponent(dataSource) + '/connectionGroups/' + encodeURIComponent(connectionGroup.identifier || '')); + + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection.service.ts new file mode 100644 index 0000000000..4f492ea7ae --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/connection.service.ts @@ -0,0 +1,237 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, Observable, tap } from 'rxjs'; +import { Connection } from '../types/Connection'; +import { ConnectionHistoryEntry } from '../types/ConnectionHistoryEntry'; +import { DirectoryPatch } from '../types/DirectoryPatch'; +import { DirectoryPatchResponse } from '../types/DirectoryPatchResponse'; + +/** + * Service for operating on connections via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class ConnectionService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } + + /** + * Makes a request to the REST API to get a single connection, returning an + * observable that provides the corresponding @link{Connection} if successful. + * + * @param id + * The ID of the connection. + * + * @returns + * An observable which will emit a @link{Connection} upon success. + * + * @example + * + * connectionService.getConnection('myConnection').subscribe(connection => { + * // Do something with the connection + * }); + */ + getConnection(dataSource: string, id: string): Observable { + + // Retrieve connection + // TODO: cache : cacheService.connections, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + ); + + } + + /** + * Makes a request to the REST API to get the usage history of a single + * connection, returning an observable that provides the corresponding + * array of @link{ConnectionHistoryEntry} objects if successful. + * + * @param id + * The identifier of the connection. + * + * @returns + * An observable which will emit an array of + * @link{ConnectionHistoryEntry} objects upon success. + */ + getConnectionHistory(dataSource: string, id: string): Observable { + + // Retrieve connection history + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/history' + ); + + } + + /** + * Makes a request to the REST API to get the parameters of a single + * connection, returning an observable that provides the corresponding + * map of parameter name/value pairs if successful. + * + * @param id + * The identifier of the connection. + * + * @returns + * An observable which will emit an map of parameter name/value + * pairs upon success. + */ + getConnectionParameters(dataSource: string, id: string): Observable> { + + // Retrieve connection parameters + // TODO: cache: cacheService.connections, + return this.http.get>( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(id) + '/parameters' + ); + + } + + /** + * Makes a request to the REST API to save a connection, returning an + * observable that can be used for processing the results of the call. If the + * connection is new, and thus does not yet have an associated identifier, + * the identifier will be automatically set in the provided connection + * upon success. + * + * @param connection + * The connection to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + saveConnection(dataSource: string, connection: Connection): Observable { + + // If connection is new, add it and set the identifier automatically + if (!connection.identifier) { + return this.http.post( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', + connection + ) + + // Set the identifier on the new connection and clear the cache + .pipe( + map(newConnection => { + connection.identifier = newConnection.identifier; + // TODO: cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly created connection + // TODO: cacheService.users.removeAll(); + }) + ); + } + + // Otherwise, update the existing connection + else { + return this.http.put( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier), + connection + ) + + // Clear the cache + .pipe( + tap(() => { + // TODO: cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly updated connection + // TODO: cacheService.users.removeAll(); + }) + ); + } + + } + + /** + * Makes a request to the REST API to apply a supplied list of connection + * patches, returning an observable that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param dataSource + * The identifier of the data source associated with the connections to + * be patched. + * + * @param patches + * An array of patches to apply. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + patchConnections(dataSource: string, patches: DirectoryPatch[]): Observable { + + // Make the PATCH request + return this.http.patch( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections', + patches + ) + + // Clear the cache + .pipe( + tap(patchResponse => { + // TODO: cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for any + // newly created or replaced connections + // TODO: cacheService.users.removeAll(); + + }) + ); + + } + + /** + * Makes a request to the REST API to delete a connection, + * returning an observable that can be used for processing the results of the call. + * + * @param connection + * The connection to delete. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteConnection(dataSource: string, connection: Connection): Observable { + + // Delete connection + return this.http.delete( + 'api/session/data/' + encodeURIComponent(dataSource) + '/connections/' + encodeURIComponent(connection.identifier!) + ) + + // Clear the cache + .pipe( + tap(() => { + // TODO: cacheService.connections.removeAll(); + }) + ); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/data-source-service.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/data-source-service.service.ts new file mode 100644 index 0000000000..489097398c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/data-source-service.service.ts @@ -0,0 +1,125 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Error } from '../types/Error'; +import { RequestService } from './request.service'; + +/** + * Service which contains all REST API response caches. + */ +@Injectable({ + providedIn: 'root' +}) +export class DataSourceService { + + constructor(private requestService: RequestService) { + } + + /** + * Invokes the given function once for each of the given data sources, + * passing that data source as the first argument to each invocation, + * followed by any additional arguments passed to apply(). The results of + * each invocation are aggregated into a map by data source identifier, + * and handled through a single promise which is resolved or rejected + * depending on the success/failure of each resulting REST call. Any error + * results in rejection of the entire apply() operation, except 404 ("NOT + * FOUND") errors, which are ignored. + * + * @param fn + * The function to call for each of the given data sources. The data + * source identifier will be given as the first argument, followed by + * the rest of the arguments given to apply(), in order. The function + * must return a Promise which is resolved or rejected depending on the + * result of the REST call. + * + * @param dataSources + * The array or data source identifiers against which the given + * function should be called. + * + * @param args + * Any additional arguments to pass to the given function each time it + * is called. + * + * @returns + * A Promise which resolves with a map of data source identifier to + * corresponding result. The result will be the exact object or value + * provided as the resolution to the Promise returned by calls to the + * given function. + */ + apply(fn: (dataSource: string, ...args: any[]) => Observable, dataSources: string[], ...args: any[]): Promise> { + + return new Promise((resolve, reject) => { + + const requests: Promise[] = []; + const results: Record = {}; + + // Retrieve the root group from all data sources + dataSources.forEach(dataSource => { + + // Add promise to list of pending requests + let deferredRequestResolve: () => void; + let deferredRequestReject: (reason: any) => void; + const deferredRequest = new Promise((resolve, reject) => { + deferredRequestResolve = resolve; + deferredRequestReject = reject; + }); + + requests.push(deferredRequest); + + // Retrieve root group from data source + fn(dataSource, ...args) + + // Store result on success + .subscribe({ + next: (data: TData) => { + results[dataSource] = data; + deferredRequestResolve(); + }, + + // Fail on any errors (except "NOT FOUND") + error: this.requestService.createPromiseErrorCallback(function immediateRequestFailed(error: any) { + + if (error.type === Error.Type.NOT_FOUND) + deferredRequestResolve(); + + // Explicitly abort for all other errors + else + deferredRequestReject(error); + + }) + }); + + }); + + // Resolve if all requests succeed + Promise.all(requests).then(() => { + resolve(results); + }, + + // Reject if at least one request fails + this.requestService.createPromiseErrorCallback((error: any) => { + reject(error); + })); + + }); + + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/historyService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/history.service.ts similarity index 65% rename from guacamole/src/main/frontend/src/app/rest/services/historyService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/history.service.ts index 96291e76aa..413e1552c5 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/historyService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/history.service.ts @@ -17,68 +17,70 @@ * under the License. */ +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { ConnectionHistoryEntry } from '../types/ConnectionHistoryEntry'; + /** * Service for operating on history records via the REST API. */ -angular.module('rest').factory('historyService', ['$injector', - function historyService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); +@Injectable({ + providedIn: 'root' +}) +export class HistoryService { - var service = {}; + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } /** * Makes a request to the REST API to get the usage history of all - * accessible connections, returning a promise that provides the + * accessible connections, returning an Observable that provides the * corresponding array of @link{ConnectionHistoryEntry} objects if * successful. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the connection * history records to be retrieved. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * - * @param {String[]} [requiredContents] + * @param requiredContents * The set of arbitrary strings to filter with. A ConnectionHistoryEntry * must contain each of these values within the associated username, * connection name, start date, or end date to appear in the result. If * null, no filtering will be performed. * - * @param {String[]} [sortPredicates] + * @param sortPredicates * The set of predicates to sort against. The resulting array of * ConnectionHistoryEntry objects will be sorted according to the * properties and sort orders defined by each predicate. If null, the * order of the resulting entries is undefined. Valid values are listed * within ConnectionHistoryEntry.SortPredicate. * - * @returns {Promise.} - * A promise which will resolve with an array of + * @returns + * An Observable which will emit an array of * @link{ConnectionHistoryEntry} objects upon success. */ - service.getConnectionHistory = function getConnectionHistory(dataSource, - requiredContents, sortPredicates) { + getConnectionHistory(dataSource: string, requiredContents?: string[], sortPredicates?: string[]): Observable { - var httpParameters = {}; + let httpParameters = new HttpParams(); // Filter according to contents if restrictions are specified if (requiredContents) - httpParameters.contains = requiredContents; + httpParameters = httpParameters.appendAll({ contains: requiredContents }); // Sort according to provided predicates, if any if (sortPredicates) - httpParameters.order = sortPredicates; + httpParameters = httpParameters.appendAll({ sort: sortPredicates }); // Retrieve connection history - return authenticationService.request({ - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/history/connections', - params : httpParameters - }); - - }; - - return service; + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/history/connections', + { params: httpParameters } + ); -}]); + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/languageService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/language.service.ts similarity index 55% rename from guacamole/src/main/frontend/src/app/rest/services/languageService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/language.service.ts index 8f55e2f118..b72a6984d2 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/languageService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/language.service.ts @@ -17,39 +17,38 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + /** * Service for operating on language metadata via the REST API. */ -angular.module('rest').factory('languageService', ['$injector', - function languageService($injector) { +@Injectable({ + providedIn: 'root' +}) +export class LanguageService { - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } - var service = {}; - /** * Makes a request to the REST API to get the list of languages, returning - * a promise that provides a map of language names by language key if + * an observable that provides a map of language names by language key if * successful. - * - * @returns {Promise.>} - * A promise which will resolve with a map of language names by + * + * @returns + * An observable which will emit a map of language names by * language key upon success. */ - service.getLanguages = function getLanguages() { + getLanguages(): Observable> { // Retrieve available languages - return authenticationService.request({ - cache : cacheService.languages, - method : 'GET', - url : 'api/languages' - }); - - }; - - return service; + // TODO cache : cacheService.languages, + return this.http.get>('api/languages'); -}]); + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/membershipService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/membership.service.ts similarity index 57% rename from guacamole/src/main/frontend/src/app/rest/services/membershipService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/membership.service.ts index 60c5c97e3d..5eed8d7f81 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/membershipService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/membership.service.ts @@ -17,58 +17,62 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { RelatedObjectPatch } from '../types/RelatedObjectPatch'; + /** * Service for operating on user group memberships via the REST API. */ -angular.module('rest').factory('membershipService', ['$injector', - function membershipService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - // Required types - var RelatedObjectPatch = $injector.get('RelatedObjectPatch'); +@Injectable({ + providedIn: 'root' +}) +export class MembershipService { - var service = {}; + /** + * Inject required services. + */ + constructor(private http: HttpClient, private authenticationService: AuthenticationService) { + } /** * Creates a new array of patches which represents the given changes to an * arbitrary set of objects sharing some common relation. * - * @param {String[]} [identifiersToAdd] + * @param identifiersToAdd * The identifiers of all objects which should be added to the * relation, if any. * - * @param {String[]} [identifiersToRemove] + * @param identifiersToRemove * The identifiers of all objects which should be removed from the * relation, if any. * - * @returns {RelatedObjectPatch[]} + * @returns * A new array of patches which represents the given changes. */ - var getRelatedObjectPatch = function getRelatedObjectPatch(identifiersToAdd, identifiersToRemove) { + private getRelatedObjectPatch(identifiersToAdd?: string[], identifiersToRemove?: string[]): RelatedObjectPatch[] { - var patch = []; + const patch: RelatedObjectPatch[] = []; - angular.forEach(identifiersToAdd, function addIdentifier(identifier) { + identifiersToAdd?.forEach(identifier => { patch.push(new RelatedObjectPatch({ - op : RelatedObjectPatch.Operation.ADD, - value : identifier + op : RelatedObjectPatch.Operation.ADD, + value: identifier })); }); - angular.forEach(identifiersToRemove, function removeIdentifier(identifier) { + identifiersToRemove?.forEach(identifier => { patch.push(new RelatedObjectPatch({ - op : RelatedObjectPatch.Operation.REMOVE, - value : identifier + op : RelatedObjectPatch.Operation.REMOVE, + value: identifier })); }); return patch; - }; + } /** * Returns the URL for the REST resource most appropriate for accessing @@ -79,28 +83,28 @@ angular.module('rest').factory('membershipService', ['$injector', * within that data source (and thus cannot be found beneath * "api/session/data/{dataSource}/users") * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user or * group whose parent user groups should be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @param {String} identifier + * @param identifier * The identifier of the user or group for which the URL of the proper * REST resource should be derived. * - * @param {Boolean} [group] + * @param group * Whether the provided identifier refers to a user group. If false or * omitted, the identifier given is assumed to refer to a user. * - * @returns {String} + * @returns * The URL for the REST resource representing the parent user groups of * the user or group having the given identifier. */ - var getUserGroupsResourceURL = function getUserGroupsResourceURL(dataSource, identifier, group) { + private getUserGroupsResourceURL(dataSource: string, identifier: string, group?: boolean): string { // Create base URL for data source - var base = 'api/session/data/' + encodeURIComponent(dataSource); + const base = 'api/session/data/' + encodeURIComponent(dataSource); // Access parent groups directly (there is no "self" for user groups // as there is for users) @@ -110,240 +114,228 @@ angular.module('rest').factory('membershipService', ['$injector', // If the username is that of the current user, do not rely on the // user actually existing (they may not). Access their parent groups via // "self" rather than the collection of defined users. - if (identifier === authenticationService.getCurrentUsername()) + if (identifier === this.authenticationService.getCurrentUsername()) return base + '/self/userGroups'; // Otherwise, the user must exist for their parent groups to be // accessible. Use the collection of defined users. return base + '/users/' + encodeURIComponent(identifier) + '/userGroups'; - }; + } /** * Makes a request to the REST API to retrieve the identifiers of all * parent user groups of which a given user or group is a member, returning - * a promise that can be used for processing the results of the call. + * an observable that can be used for processing the results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user or * group whose parent user groups should be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @param {String} identifier + * @param identifier * The identifier of the user or group to retrieve the parent user * groups of. * - * @param {Boolean} [group] + * @param group * Whether the provided identifier refers to a user group. If false or * omitted, the identifier given is assumed to refer to a user. * - * @returns {Promise.} - * A promise for the HTTP call which will resolve with an array + * @returns + * An observable for the HTTP call which will emit an array * containing the requested identifiers upon success. */ - service.getUserGroups = function getUserGroups(dataSource, identifier, group) { + getUserGroups(dataSource: string, identifier: string, group?: boolean): Observable { // Retrieve parent groups - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : getUserGroupsResourceURL(dataSource, identifier, group) - }); + // TODO: cache : cacheService.users, + return this.http.get(this.getUserGroupsResourceURL(dataSource, identifier, group)); - }; + } /** * Makes a request to the REST API to modify the parent user groups of - * which a given user or group is a member, returning a promise that can be + * which a given user or group is a member, returning an observable that can be * used for processing the results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user or * group whose parent user groups should be modified. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @param {String} identifier + * @param identifier * The identifier of the user or group to modify the parent user * groups of. * - * @param {String[]} [addToUserGroups] + * @param addToUserGroups * The identifier of all parent user groups to which the given user or * group should be added as a member, if any. * - * @param {String[]} [removeFromUserGroups] + * @param removeFromUserGroups * The identifier of all parent user groups from which the given member * user or group should be removed, if any. * - * @param {Boolean} [group] + * @param group * Whether the provided identifier refers to a user group. If false or * omitted, the identifier given is assumed to refer to a user. * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the + * @returns + * An observable for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchUserGroups = function patchUserGroups(dataSource, identifier, - addToUserGroups, removeFromUserGroups, group) { + patchUserGroups(dataSource: string, identifier: string, addToUserGroups?: string[], removeFromUserGroups?: string[], group?: boolean): Observable { // Update parent user groups - return authenticationService.request({ - method : 'PATCH', - url : getUserGroupsResourceURL(dataSource, identifier, group), - data : getRelatedObjectPatch(addToUserGroups, removeFromUserGroups) - }) - - // Clear the cache - .then(function parentUserGroupsChanged(){ - cacheService.users.removeAll(); - }); + return this.http.patch( + this.getUserGroupsResourceURL(dataSource, identifier, group), + this.getRelatedObjectPatch(addToUserGroups, removeFromUserGroups) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); - }; + } /** * Makes a request to the REST API to retrieve the identifiers of all - * users which are members of the given user group, returning a promise + * users which are members of the given user group, returning an observable * that can be used for processing the results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user group * whose member users should be retrieved. This identifier corresponds * to an AuthenticationProvider within the Guacamole web application. * - * @param {String} identifier + * @param identifier * The identifier of the user group to retrieve the member users of. * - * @returns {Promise.} - * A promise for the HTTP call which will resolve with an array + * @returns + * An observable for the HTTP call which will emit an array * containing the requested identifiers upon success. */ - service.getMemberUsers = function getMemberUsers(dataSource, identifier) { + getMemberUsers(dataSource: string, identifier: string): Observable { // Retrieve member users - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers' - }); + // TODO: cache : cacheService.users, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers' + ); - }; + } /** * Makes a request to the REST API to modify the member users of a given - * user group, returning a promise that can be used for processing the + * user group, returning an observable that can be used for processing the * results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user group * whose member users should be modified. This identifier corresponds * to an AuthenticationProvider within the Guacamole web application. * - * @param {String} identifier + * @param identifier * The identifier of the user group to modify the member users of. * - * @param {String[]} [usersToAdd] + * @param usersToAdd * The identifier of all users to add as members of the given user * group, if any. * - * @param {String[]} [usersToRemove] + * @param usersToRemove * The identifier of all users to remove from the given user group, * if any. * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the + * @returns + * An observable for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchMemberUsers = function patchMemberUsers(dataSource, identifier, - usersToAdd, usersToRemove) { + patchMemberUsers(dataSource: string, identifier: string, usersToAdd?: string[], usersToRemove?: string[]): Observable { // Update member users - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers', - data : getRelatedObjectPatch(usersToAdd, usersToRemove) - }) - - // Clear the cache - .then(function memberUsersChanged(){ - cacheService.users.removeAll(); - }); + return this.http.patch( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUsers', + this.getRelatedObjectPatch(usersToAdd, usersToRemove) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); - }; + } /** * Makes a request to the REST API to retrieve the identifiers of all - * user groups which are members of the given user group, returning a - * promise that can be used for processing the results of the call. + * user groups which are members of the given user group, returning an + * observable that can be used for processing the results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user group * whose member user groups should be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @param {String} identifier + * @param identifier * The identifier of the user group to retrieve the member user * groups of. * - * @returns {Promise.} - * A promise for the HTTP call which will resolve with an array + * @returns + * An observable for the HTTP call which will emit an array * containing the requested identifiers upon success. */ - service.getMemberUserGroups = function getMemberUserGroups(dataSource, identifier) { + getMemberUserGroups(dataSource: string, identifier: string): Observable { // Retrieve member user groups - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups' - }); + // TODO: cache: cacheService.users, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups' + ); - }; + } /** * Makes a request to the REST API to modify the member user groups of a - * given user group, returning a promise that can be used for processing + * given user group, returning an observable that can be used for processing * the results of the call. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user group * whose member user groups should be modified. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @param {String} identifier + * @param identifier * The identifier of the user group to modify the member user groups of. * - * @param {String[]} [userGroupsToAdd] + * @param userGroupsToAdd * The identifier of all user groups to add as members of the given * user group, if any. * - * @param {String[]} [userGroupsToRemove] + * @param userGroupsToRemove * The identifier of all member user groups to remove from the given * user group, if any. * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the + * @returns + * An observable for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchMemberUserGroups = function patchMemberUserGroups(dataSource, - identifier, userGroupsToAdd, userGroupsToRemove) { + patchMemberUserGroups(dataSource: string, identifier: string, userGroupsToAdd?: string[], userGroupsToRemove?: string[]): Observable { // Update member user groups - return authenticationService.request({ - method : 'PATCH', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups', - data : getRelatedObjectPatch(userGroupsToAdd, userGroupsToRemove) - }) - - // Clear the cache - .then(function memberUserGroupsChanged(){ - cacheService.users.removeAll(); - }); + return this.http.patch( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + '/memberUserGroups', + this.getRelatedObjectPatch(userGroupsToAdd, userGroupsToRemove) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } - }; - - return service; -}]); +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/patchService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/patch.service.ts similarity index 63% rename from guacamole/src/main/frontend/src/app/rest/services/patchService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/patch.service.ts index ea06e5b402..f071c099d6 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/patchService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/patch.service.ts @@ -17,39 +17,39 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; + /** * Service for operating on HTML patches via the REST API. */ -angular.module('rest').factory('patchService', ['$injector', - function patchService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var cacheService = $injector.get('cacheService'); +@Injectable({ + providedIn: 'root' +}) +export class PatchService { - var service = {}; + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } /** * Makes a request to the REST API to get the list of patches, returning - * a promise that provides the array of all applicable patches if + * an observable that provides the array of all applicable patches if * successful. Each patch is a string of raw HTML with meta information * describing the patch operation stored within meta tags. * - * @returns {Promise.} - * A promise which will resolve with an array of HTML patches upon + * @returns + * An observable which will emit an array of HTML patches upon * success. */ - service.getPatches = function getPatches() { + getPatches(): Observable { // Retrieve all applicable HTML patches - return requestService({ - cache : cacheService.patches, - method : 'GET', - url : 'api/patches' - }); - - }; + return this.http.get('api/patches'); - return service; + } -}]); +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/permissionService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/permission.service.ts similarity index 67% rename from guacamole/src/main/frontend/src/app/rest/services/permissionService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/permission.service.ts index ac09fa1dff..21b8d87c2a 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/permissionService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/permission.service.ts @@ -17,21 +17,56 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { PermissionPatch } from '../types/PermissionPatch'; +import { PermissionSet } from '../types/PermissionSet'; + /** * Service for operating on user permissions via the REST API. */ -angular.module('rest').factory('permissionService', ['$injector', - function permissionService($injector) { +@Injectable({ + providedIn: 'root' +}) +export class PermissionService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient, private authenticationService: AuthenticationService) { + } + + /** + * Makes a request to the REST API to get the list of effective permissions + * for a given user, returning an observable that provides a + * {@link PermissionSet} objects if successful. Effective permissions differ + * from the permissions returned via getPermissions() in that permissions + * which are not directly granted to the user are included. + * + * NOTE: Unlike getPermissions(), getEffectivePermissions() CANNOT be + * applied to user groups. Only users have retrievable effective + * permissions as far as the REST API is concerned. + * + * @param dataSource + * The unique identifier of the data source containing the user whose + * permissions should be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param userID + * The ID of the user to retrieve the permissions for. + * + * @returns + * An observable which will emit a {@link PermissionSet} upon + * success. + */ + getEffectivePermissions(dataSource: string, userID: string): Observable { - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); - - // Required types - var PermissionPatch = $injector.get('PermissionPatch'); + // Retrieve user permissions + return this.http.get(this.getEffectivePermissionsResourceURL(dataSource, userID)); - var service = {}; + } /** * Returns the URL for the REST resource most appropriate for accessing @@ -50,69 +85,68 @@ angular.module('rest').factory('permissionService', ['$injector', * Only users have retrievable effective permissions as far as the REST API * is concerned. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user whose * permissions should be retrieved. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * - * @param {String} username + * @param username * The username of the user for which the URL of the proper REST * resource should be derived. * - * @returns {String} + * @returns * The URL for the REST resource representing the user having the given * username. */ - var getEffectivePermissionsResourceURL = function getEffectivePermissionsResourceURL(dataSource, username) { + private getEffectivePermissionsResourceURL(dataSource: string, username: string): string { // Create base URL for data source - var base = 'api/session/data/' + encodeURIComponent(dataSource); + const base = 'api/session/data/' + encodeURIComponent(dataSource); // If the username is that of the current user, do not rely on the // user actually existing (they may not). Access their permissions via // "self" rather than the collection of defined users. - if (username === authenticationService.getCurrentUsername()) + if (username === this.authenticationService.getCurrentUsername()) return base + '/self/effectivePermissions'; // Otherwise, the user must exist for their permissions to be // accessible. Use the collection of defined users. return base + '/users/' + encodeURIComponent(username) + '/effectivePermissions'; - }; + } /** - * Makes a request to the REST API to get the list of effective permissions - * for a given user, returning a promise that provides an array of - * @link{Permission} objects if successful. Effective permissions differ - * from the permissions returned via getPermissions() in that permissions - * which are not directly granted to the user are included. + * Makes a request to the REST API to get the list of permissions for a + * given user or user group, returning a promise that provides a + * {@link PermissionSet} objects if successful. The permissions retrieved + * differ from effective permissions (those returned by + * getEffectivePermissions()) in that both users and groups may be queried, + * and only permissions which are directly granted to the user or group are + * included. * - * NOTE: Unlike getPermissions(), getEffectivePermissions() CANNOT be - * applied to user groups. Only users have retrievable effective - * permissions as far as the REST API is concerned. + * @param dataSource + * The unique identifier of the data source containing the user or group + * whose permissions should be retrieved. This identifier corresponds to + * an AuthenticationProvider within the Guacamole web application. * - * @param {String} dataSource - * The unique identifier of the data source containing the user whose - * permissions should be retrieved. This identifier corresponds to an - * AuthenticationProvider within the Guacamole web application. + * @param identifier + * The identifier of the user or group to retrieve the permissions for. * - * @param {String} userID - * The ID of the user to retrieve the permissions for. + * @param group + * Whether the provided identifier refers to a user group. If false or + * omitted, the identifier given is assumed to refer to a user. * - * @returns {Promise.} - * A promise which will resolve with a @link{PermissionSet} upon + * @returns + * An observable which will emit a {@link PermissionSet} upon * success. */ - service.getEffectivePermissions = function getEffectivePermissions(dataSource, userID) { + getPermissions(dataSource: string, identifier: string, group = false): Observable { - // Retrieve user permissions - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : getEffectivePermissionsResourceURL(dataSource, userID) - }); + // Retrieve user/group permissions + // TODO: cache : cacheService.users, + return this.http.get(this.getPermissionsResourceURL(dataSource, identifier, group)); - }; + } /** * Returns the URL for the REST resource most appropriate for accessing @@ -120,33 +154,33 @@ angular.module('rest').factory('permissionService', ['$injector', * permissions retrieved differ from effective permissions (those returned * by getEffectivePermissions()) in that only permissions which are directly * granted to the user or group are included. - * + * * It is important to note that a particular data source can authenticate * and provide permissions for a user, even if that user does not exist * within that data source (and thus cannot be found beneath * "api/session/data/{dataSource}/users") * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user whose * permissions should be retrieved. This identifier corresponds to an * AuthenticationProvider within the Guacamole web application. * - * @param {String} identifier + * @param identifier * The identifier of the user or group for which the URL of the proper * REST resource should be derived. * - * @param {Boolean} [group] + * @param group * Whether the provided identifier refers to a user group. If false or * omitted, the identifier given is assumed to refer to a user. * - * @returns {String} + * @returns * The URL for the REST resource representing the user or group having * the given identifier. */ - var getPermissionsResourceURL = function getPermissionsResourceURL(dataSource, identifier, group) { + private getPermissionsResourceURL(dataSource: string, identifier: string, group = false): string { // Create base URL for data source - var base = 'api/session/data/' + encodeURIComponent(dataSource); + const base = 'api/session/data/' + encodeURIComponent(dataSource); // Access group permissions directly (there is no "self" for user groups // as there is for users) @@ -156,190 +190,147 @@ angular.module('rest').factory('permissionService', ['$injector', // If the username is that of the current user, do not rely on the // user actually existing (they may not). Access their permissions via // "self" rather than the collection of defined users. - if (identifier === authenticationService.getCurrentUsername()) + if (identifier === this.authenticationService.getCurrentUsername()) return base + '/self/permissions'; // Otherwise, the user must exist for their permissions to be // accessible. Use the collection of defined users. return base + '/users/' + encodeURIComponent(identifier) + '/permissions'; - }; - - /** - * Makes a request to the REST API to get the list of permissions for a - * given user or user group, returning a promise that provides an array of - * @link{Permission} objects if successful. The permissions retrieved - * differ from effective permissions (those returned by - * getEffectivePermissions()) in that both users and groups may be queried, - * and only permissions which are directly granted to the user or group are - * included. - * - * @param {String} dataSource - * The unique identifier of the data source containing the user or group - * whose permissions should be retrieved. This identifier corresponds to - * an AuthenticationProvider within the Guacamole web application. - * - * @param {String} identifier - * The identifier of the user or group to retrieve the permissions for. - * - * @param {Boolean} [group] - * Whether the provided identifier refers to a user group. If false or - * omitted, the identifier given is assumed to refer to a user. - * - * @returns {Promise.} - * A promise which will resolve with a @link{PermissionSet} upon - * success. - */ - service.getPermissions = function getPermissions(dataSource, identifier, group) { - - // Retrieve user/group permissions - return authenticationService.request({ - cache : cacheService.users, - method : 'GET', - url : getPermissionsResourceURL(dataSource, identifier, group) - }); - - }; - - /** - * Adds patches for modifying the permissions associated with specific - * objects to the given array of patches. - * - * @param {PermissionPatch[]} patch - * The array of patches to add new patches to. - * - * @param {String} operation - * The operation to specify within each of the patches. Valid values - * for this are defined within PermissionPatch.Operation. - * - * @param {String} path - * The path of the permissions being patched. The path is a JSON path - * describing the position of the permissions within a PermissionSet. - * - * @param {Object.} permissions - * A map of object identifiers to arrays of permission type strings, - * where each type string is a value from - * PermissionSet.ObjectPermissionType. - */ - var addObjectPatchOperations = function addObjectPatchOperations(patch, operation, path, permissions) { - - // Add object permission operations to patch - for (var identifier in permissions) { - permissions[identifier].forEach(function addObjectPatch(type) { - patch.push({ - op : operation, - path : path + "/" + identifier, - value : type - }); - }); - } - - }; + } /** * Adds patches for modifying any permission that can be stored within a * @link{PermissionSet}. - * - * @param {PermissionPatch[]} patch + * + * @param patch * The array of patches to add new patches to. * - * @param {String} operation + * @param operation * The operation to specify within each of the patches. Valid values * for this are defined within PermissionPatch.Operation. * - * @param {PermissionSet} permissions + * @param permissions * The set of permissions for which patches should be added. */ - var addPatchOperations = function addPatchOperations(patch, operation, permissions) { + private addPatchOperations(patch: PermissionPatch[], operation: PermissionPatch.Operation, permissions: PermissionSet = new PermissionSet()): void { // Add connection permission operations to patch - addObjectPatchOperations(patch, operation, "/connectionPermissions", + this.addObjectPatchOperations(patch, operation, '/connectionPermissions', permissions.connectionPermissions); // Add connection group permission operations to patch - addObjectPatchOperations(patch, operation, "/connectionGroupPermissions", + this.addObjectPatchOperations(patch, operation, '/connectionGroupPermissions', permissions.connectionGroupPermissions); // Add sharing profile permission operations to patch - addObjectPatchOperations(patch, operation, "/sharingProfilePermissions", + this.addObjectPatchOperations(patch, operation, '/sharingProfilePermissions', permissions.sharingProfilePermissions); // Add active connection permission operations to patch - addObjectPatchOperations(patch, operation, "/activeConnectionPermissions", + this.addObjectPatchOperations(patch, operation, '/activeConnectionPermissions', permissions.activeConnectionPermissions); // Add user permission operations to patch - addObjectPatchOperations(patch, operation, "/userPermissions", + this.addObjectPatchOperations(patch, operation, '/userPermissions', permissions.userPermissions); // Add user group permission operations to patch - addObjectPatchOperations(patch, operation, "/userGroupPermissions", + this.addObjectPatchOperations(patch, operation, '/userGroupPermissions', permissions.userGroupPermissions); // Add system operations to patch permissions.systemPermissions.forEach(function addSystemPatch(type) { patch.push({ - op : operation, - path : "/systemPermissions", - value : type + op : operation, + path : '/systemPermissions', + value: type }); }); - }; - + } + + /** + * Adds patches for modifying the permissions associated with specific + * objects to the given array of patches. + * + * @param patch + * The array of patches to add new patches to. + * + * @param operation + * The operation to specify within each of the patches. Valid values + * for this are defined within PermissionPatch.Operation. + * + * @param path + * The path of the permissions being patched. The path is a JSON path + * describing the position of the permissions within a PermissionSet. + * + * @param permissions + * A map of object identifiers to arrays of permission type strings, + * where each type string is a value from + * PermissionSet.ObjectPermissionType. + */ + private addObjectPatchOperations(patch: PermissionPatch[], operation: PermissionPatch.Operation, path: string, permissions: Record): void { + + // Add object permission operations to patch + for (const identifier in permissions) { + permissions[identifier].forEach(function addObjectPatch(type) { + patch.push({ + op : operation, + path : path + '/' + identifier, + value: type + }); + }); + } + + } + /** * Makes a request to the REST API to modify the permissions for a given * user or group, returning a promise that can be used for processing the * results of the call. This request affects only the permissions directly * granted to the user or group, and may not affect permissions inherited * through other means (effective permissions). - * - * @param {String} dataSource + * + * @param dataSource * The unique identifier of the data source containing the user or group * whose permissions should be modified. This identifier corresponds to * an AuthenticationProvider within the Guacamole web application. * - * @param {String} identifier + * @param identifier * The identifier of the user or group to modify the permissions of. - * - * @param {PermissionSet} [permissionsToAdd] + * + * @param [permissionsToAdd] * The set of permissions to add, if any. * - * @param {PermissionSet} [permissionsToRemove] + * @param [permissionsToRemove] * The set of permissions to remove, if any. * - * @param {Boolean} [group] + * @param [group] * Whether the provided identifier refers to a user group. If false or * omitted, the identifier given is assumed to refer to a user. * - * @returns {Promise} - * A promise for the HTTP call which will succeed if and only if the + * @returns + * An observable for the HTTP call which will succeed if and only if the * patch operation is successful. */ - service.patchPermissions = function patchPermissions(dataSource, identifier, - permissionsToAdd, permissionsToRemove, group) { + patchPermissions(dataSource: string, identifier: string, permissionsToAdd?: PermissionSet, permissionsToRemove?: PermissionSet, group?: boolean): Observable { + + const permissionPatch: PermissionPatch[] = []; - var permissionPatch = []; - // Add all the add operations to the patch - addPatchOperations(permissionPatch, PermissionPatch.Operation.ADD, permissionsToAdd); + this.addPatchOperations(permissionPatch, PermissionPatch.Operation.ADD, permissionsToAdd); // Add all the remove operations to the patch - addPatchOperations(permissionPatch, PermissionPatch.Operation.REMOVE, permissionsToRemove); + this.addPatchOperations(permissionPatch, PermissionPatch.Operation.REMOVE, permissionsToRemove); // Patch user/group permissions - return authenticationService.request({ - method : 'PATCH', - url : getPermissionsResourceURL(dataSource, identifier, group), - data : permissionPatch - }) - - // Clear the cache - .then(function permissionsPatched(){ - cacheService.users.removeAll(); - }); - }; - - return service; - -}]); + return this.http.patch(this.getPermissionsResourceURL(dataSource, identifier, group), permissionPatch) + + // Clear the cache + .pipe(tap(() => { + // TODO: this.cacheService.users.removeAll(); + }) + ); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.spec.ts new file mode 100644 index 0000000000..be41273a00 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.spec.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { catchError, throwError } from 'rxjs'; +import { TestScheduler } from 'rxjs/testing'; +import { getTestScheduler } from '../../util/test-helper'; +import { Error } from '../types/Error'; +import { RequestService } from './request.service'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; +import Type = Error.Type; + +describe('RequestService', () => { + let service: RequestService; + let testScheduler: TestScheduler; + + beforeEach(() => { + testScheduler = getTestScheduler(); + TestBed.configureTestingModule({ + imports: [], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}); + service = TestBed.inject(RequestService); + }); + + describe('createErrorCallback', () => { + it('should invoke the given callback if the error is a REST Error', () => { + testScheduler.run(({ expectObservable }) => { + + const restError = new Error({ type: Type.NOT_FOUND }); + const defaultValue = 'foo'; + + const observable = throwError(() => restError).pipe( + catchError(service.defaultValue(defaultValue)) + ); + + expectObservable(observable).toBe('(a|)', { a: defaultValue }); + }); + }); + + it('should rethrow all other errors', () => { + testScheduler.run(({ expectObservable }) => { + const otherError = 'Other Error'; + const defaultValue = 'foo'; + + const observable = throwError(() => otherError).pipe( + catchError(service.defaultValue(defaultValue)) + ); + + expectObservable(observable).toBe('#', undefined, otherError); + + }); + }); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.ts new file mode 100644 index 0000000000..8bee25694d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/request.service.ts @@ -0,0 +1,198 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { Observable, of, throwError } from 'rxjs'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { LogService } from '../../util/log.service'; +import { Error } from '../types/Error'; + +/** + * Service for converting observables from the HttpService that pass the entire response into + * promises that pass only the data from that response. + */ +@Injectable({ + providedIn: 'root' +}) +export class RequestService { + + constructor(private log: LogService, + private guacEventService: GuacEventService) { + } + + /** + * Creates an Observable error callback which invokes the given callback only + * if the Observable failed with a REST @link{Error} object. If the + * Observable failed without an @link{Error} object, such as when a + * JavaScript error occurs within a callback earlier in the Observable chain, + * the error is logged without invoking the given callback and rethrown. + * + * @param callback + * The callback to invoke if the Observable failed with an + * @link{Error} object. + * + * @returns + * A function which can be provided as the catchError callback for an + * Observable. + */ + createErrorCallback(callback: (error: any) => Observable): (error: any) => Observable { + return (error: any): Observable => { + + // Invoke given callback ONLY if due to a legitimate REST error + if (error instanceof Error) + return callback(error); + + // Log all other errors + this.log.error(error); + return throwError(() => error); + }; + } + + /** + * Creates a promise error callback which invokes the given callback only + * if the promise was rejected with a REST @link{Error} object. If the + * promise is rejected without an @link{Error} object, such as when a + * JavaScript error occurs within a callback earlier in the promise chain, + * the rejection is logged without invoking the given callback. + * + * @param callback + * The callback to invoke if the promise is rejected with an + * @link{Error} object. + * + * @returns + * A function which can be provided as the error callback for a + * promise. + */ + createPromiseErrorCallback(callback: Function): ((reason: any) => (PromiseLike) | null | undefined) { + return ((error: any) => { + + // Invoke given callback ONLY if due to a legitimate REST error + if (error instanceof Error) + return callback(error); + + // Log all other errors + this.log.error(error); + + }); + } + + /** + * Creates an observable error callback which emits the + * given default value only if the @link{Error} is a NOT_FOUND error. + * All other errors are passed through and must be handled as yet more errors. + * + * @param value + * The default value to use to emit if the observable failed with a + * NOT_FOUND error. + * + * @returns + * A function which can be provided as the error callback for an + * observable. + */ + defaultValue(value: T): (error: any) => Observable { + return this.createErrorCallback((error: any) => { + + // Return default value only if not found + if (error.type === Error.Type.NOT_FOUND) + return of(value); + + // Reject promise with original error otherwise + throw error; + + }); + } + + /** + * Creates a promise error callback which resolves the promise with the + * given default value only if the @link{Error} in the original rejection + * is a NOT_FOUND error. All other errors are passed through and must be + * handled as yet more rejections. + * + * @param value + * The default value to use to resolve the promise if the promise is + * rejected with a NOT_FOUND error. + * + * @returns + * A function which can be provided as the error callback for a + * promise. + */ + defaultPromiseValue(value: any): Function { + return this.createErrorCallback(function resolveIfNotFound(error: any) { + + // Return default value only if not found + if (error.type === Error.Type.NOT_FOUND) + return value; + + // Reject promise with original error otherwise + throw error; + + }); + } + + /** + * Promise error callback which ignores all rejections due to REST errors, + * but logs all other rejections, such as those due to JavaScript errors. + * This callback should be used in cases where + * a REST response is being handled but REST errors should be ignored. + * + * @constant + */ + readonly IGNORE: (error: any) => Observable = this.createErrorCallback((error: any): Observable => { + return of(void (0)); + }); + + /** + * Promise error callback which logs all rejections due to REST errors as + * warnings to the browser console, and logs all other rejections as + * errors. This callback should be used in favor of + * @link{IGNORE} if REST errors are simply not expected. + * + * @constant + */ + readonly WARN: (error: any) => Observable = this.createErrorCallback((error: any): Observable => { + this.log.warn(error.type, error.message || error.translatableMessage); + return of(void (0)); + }); + + /** + * Promise error callback which replaces the content of the page with a + * generic error message warning that the page could not be displayed. All + * rejections are logged to the browser console as errors. This callback + * should be used in favor of @link{WARN} if REST errors will result in the + * page being unusable. + * + * @constant + */ + readonly DIE: (error: any) => Observable = this.createErrorCallback((error: any): Observable => { + this.guacEventService.broadcast('guacFatalPageError', error); + this.log.error(error.type, error.message || error.translatableMessage); + return of(void (0)); + }); + + readonly PROMISE_DIE: ((reason: any) => (PromiseLike) | null | undefined) = this.createPromiseErrorCallback((error: any) => { + this.guacEventService.broadcast('guacFatalPageError', error); + this.log.error(error.type, error.message || error.translatableMessage); + }); + + readonly PROMISE_WARN: ((reason: any) => (PromiseLike) | null | undefined) = this.createPromiseErrorCallback((error: any) => { + this.log.warn(error.type, error.message || error.translatableMessage); + }); + +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/schemaService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/schema.service.ts similarity index 55% rename from guacamole/src/main/frontend/src/app/rest/services/schemaService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/schema.service.ts index d6afa2b75b..883195858c 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/schemaService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/schema.service.ts @@ -17,212 +17,208 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Form } from '../types/Form'; +import { Protocol } from '../types/Protocol'; + /** * Service for operating on metadata via the REST API. */ -angular.module('rest').factory('schemaService', ['$injector', - function schemaService($injector) { - - // Required services - var requestService = $injector.get('requestService'); - var authenticationService = $injector.get('authenticationService'); - var cacheService = $injector.get('cacheService'); +@Injectable({ + providedIn: 'root' +}) +export class SchemaService { - var service = {}; + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } /** * Makes a request to the REST API to get the list of available attributes - * for user objects, returning a promise that provides an array of + * for user objects, returning an observable that provides an array of * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the users whose * available attributes are to be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getUserAttributes = function getUserAttributes(dataSource) { + getUserAttributes(dataSource: string): Observable { // Retrieve available user attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get('api/session/data/' + encodeURIComponent(dataSource) + '/schema/userAttributes'); - }; + } /** * Makes a request to the REST API to get the list of available user preference - * attributes, returning a promise that provides an array of @link{Form} objects + * attributes, returning an observable that provides an array of @link{Form} objects * if successful. Each element of the array describes a logical grouping of * possible user preference attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the users whose * available user preference attributes are to be retrieved. This * identifier corresponds to an AuthenticationProvider within the * Guacamole web application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getUserPreferenceAttributes = function getUserPreferenceAttributes(dataSource) { + getUserPreferenceAttributes(dataSource: string): Observable { // Retrieve available user attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userPreferenceAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userPreferenceAttributes' + ); - }; + } /** * Makes a request to the REST API to get the list of available attributes - * for user group objects, returning a promise that provides an array of + * for user group objects, returning an observable that provides an array of * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the user groups * whose available attributes are to be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getUserGroupAttributes = function getUserGroupAttributes(dataSource) { + getUserGroupAttributes(dataSource: string): Observable { // Retrieve available user group attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userGroupAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/userGroupAttributes' + ); - }; + } /** * Makes a request to the REST API to get the list of available attributes - * for connection objects, returning a promise that provides an array of + * for connection objects, returning an observable that provides an array of * @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the connections * whose available attributes are to be retrieved. This identifier * corresponds to an AuthenticationProvider within the Guacamole web * application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionAttributes = function getConnectionAttributes(dataSource) { + getConnectionAttributes(dataSource: string): Observable { // Retrieve available connection attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/connectionAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/connectionAttributes' + ); - }; + } /** * Makes a request to the REST API to get the list of available attributes - * for sharing profile objects, returning a promise that provides an array + * for sharing profile objects, returning an observable that provides an array * of @link{Form} objects if successful. Each element of the array describes * a logical grouping of possible attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the sharing * profiles whose available attributes are to be retrieved. This * identifier corresponds to an AuthenticationProvider within the * Guacamole web application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getSharingProfileAttributes = function getSharingProfileAttributes(dataSource) { + getSharingProfileAttributes(dataSource: string): Observable { // Retrieve available sharing profile attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/sharingProfileAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/sharingProfileAttributes' + ); - }; + } /** * Makes a request to the REST API to get the list of available attributes - * for connection group objects, returning a promise that provides an array + * for connection group objects, returning an observable that provides an array * of @link{Form} objects if successful. Each element of the array * a logical grouping of possible attributes. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source containing the connection * groups whose available attributes are to be retrieved. This * identifier corresponds to an AuthenticationProvider within the * Guacamole web application. * - * @returns {Promise.} - * A promise which will resolve with an array of @link{Form} + * @returns + * An observable which will emit an array of @link{Form} * objects, where each @link{Form} describes a logical grouping of * possible attributes. */ - service.getConnectionGroupAttributes = function getConnectionGroupAttributes(dataSource) { + getConnectionGroupAttributes(dataSource: string): Observable { // Retrieve available connection group attributes - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/connectionGroupAttributes' - }); + // TODO: cache : cacheService.schema, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/connectionGroupAttributes' + ); - }; + } /** * Makes a request to the REST API to get the list of protocols, returning - * a promise that provides a map of @link{Protocol} objects by protocol + * an observable that provides a map of @link{Protocol} objects by protocol * name if successful. * - * @param {String} dataSource + * @param dataSource * The unique identifier of the data source defining available * protocols. This identifier corresponds to an AuthenticationProvider * within the Guacamole web application. * - * @returns {Promise.>} - * A promise which will resolve with a map of @link{Protocol} + * @returns + * An observable which will emit a map of @link{Protocol} * objects by protocol name upon success. */ - service.getProtocols = function getProtocols(dataSource) { + getProtocols(dataSource: string): Observable> { // Retrieve available protocols - return authenticationService.request({ - cache : cacheService.schema, - method : 'GET', - url : 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/protocols' - }); - - }; + // TODO: cache : cacheService.schema, + return this.http.get>( + 'api/session/data/' + encodeURIComponent(dataSource) + '/schema/protocols' + ); - return service; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/sharing-profile.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/sharing-profile.service.ts new file mode 100644 index 0000000000..04d9ebdb65 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/sharing-profile.service.ts @@ -0,0 +1,167 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { map, Observable, tap } from 'rxjs'; +import { SharingProfile } from '../types/SharingProfile'; + +/** + * Service for operating on sharing profiles via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class SharingProfileService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + + } + + /** + * Makes a request to the REST API to get a single sharing profile, + * returning an observable that provides the corresponding @link{SharingProfile} + * if successful. + * + * @param id + * The ID of the sharing profile. + * + * @returns + * An observable which will emit a @link{SharingProfile} upon + * success. + * + * @example + * + * sharingProfileService.getSharingProfile('mySharingProfile').subscribe(sharingProfile => { + * // Do something with the sharing profile + * }); + */ + getSharingProfile(dataSource: string, id: string): Observable { + + // Retrieve sharing profile + // TODO: cache : cacheService.connections, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(id) + ); + + } + + /** + * Makes a request to the REST API to get the parameters of a single + * sharing profile, returning an observable that provides the corresponding + * map of parameter name/value pairs if successful. + * + * @param id + * The identifier of the sharing profile. + * + * @returns + * An observable which will emit a map of parameter name/value + * pairs upon success. + */ + getSharingProfileParameters(dataSource: string, id: string): Observable> { + + // Retrieve sharing profile parameters + // TODO: cache: cacheService.connections, + return this.http.get>( + 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(id) + '/parameters' + ); + + } + + /** + * Makes a request to the REST API to save a sharing profile, returning an + * observable that can be used for processing the results of the call. If the + * sharing profile is new, and thus does not yet have an associate + * identifier, the identifier will be automatically set in the provided + * sharing profile upon success. + * + * @param sharingProfile + * The sharing profile to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + saveSharingProfile(dataSource: string, sharingProfile: SharingProfile): Observable { + + // If sharing profile is new, add it and set the identifier automatically + if (!sharingProfile.identifier) { + return this.http.post( + 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles', + sharingProfile + ).pipe(map((newSharingProfile) => { + + // Set the identifier on the new sharing profile and clear the cache + sharingProfile.identifier = newSharingProfile.identifier; + // TODO: cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly created sharing profile + // TODO: cacheService.users.removeAll(); + })); + } + + // Otherwise, update the existing sharing profile + else { + return this.http.put( + 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(sharingProfile.identifier), + sharingProfile + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.connections.removeAll(); + + // Clear users cache to force reload of permissions for this + // newly updated sharing profile + // TODO: cacheService.users.removeAll(); + })); + } + + } + + /** + * Makes a request to the REST API to delete a sharing profile, + * returning an observable that can be used for processing the results of the call. + * + * @param sharingProfile + * The sharing profile to delete. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteSharingProfile(dataSource: string, sharingProfile: SharingProfile): Observable { + + // Delete sharing profile + return this.http.delete( + 'api/session/data/' + encodeURIComponent(dataSource) + '/sharingProfiles/' + encodeURIComponent(sharingProfile.identifier!) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.connections.removeAll(); + })); + + } + +} diff --git a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/tunnel.service.ts similarity index 57% rename from guacamole/src/main/frontend/src/app/rest/services/tunnelService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/tunnel.service.ts index 2ebb08bd78..152f40f6c0 100644 --- a/guacamole/src/main/frontend/src/app/rest/services/tunnelService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/tunnel.service.ts @@ -17,162 +17,137 @@ * under the License. */ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { AuthenticationService } from '../../auth/service/authentication.service'; +import { Error } from '../types/Error'; +import { Protocol } from '../types/Protocol'; +import { SharingProfile } from '../types/SharingProfile'; +import { UserCredentials } from '../types/UserCredentials'; + /** * Service for operating on the tunnels of in-progress connections (and their * underlying objects) via the REST API. */ -angular.module('rest').factory('tunnelService', ['$injector', - function tunnelService($injector) { - - // Required types - var Error = $injector.get('Error'); - - // Required services - var $q = $injector.get('$q'); - var $window = $injector.get('$window'); - var authenticationService = $injector.get('authenticationService'); - var requestService = $injector.get('requestService'); - - var service = {}; - - /** - * Reference to the window.document object. - * - * @private - * @type HTMLDocument - */ - var document = $window.document; +@Injectable({ + providedIn: 'root' +}) +export class TunnelService { /** * The number of milliseconds to wait after a stream download has completed * before cleaning up related DOM resources, if the browser does not * otherwise notify us that cleanup is safe. - * - * @private - * @constant - * @type Number */ - var DOWNLOAD_CLEANUP_WAIT = 5000; + private readonly DOWNLOAD_CLEANUP_WAIT: number = 5000; /** * The maximum size a chunk may be during uploadToStream() in bytes. - * - * @private - * @constant - * @type Number */ - const CHUNK_SIZE = 1024 * 1024 * 4; + private readonly CHUNK_SIZE: number = 1024 * 1024 * 4; + + /** + * Inject required services. + */ + constructor(private http: HttpClient, private authenticationService: AuthenticationService) { + } /** * Makes a request to the REST API to get the list of all tunnels - * associated with in-progress connections, returning a promise that + * associated with in-progress connections, returning an Observable that * provides an array of their UUIDs (strings) if successful. * - * @returns {Promise.>} - * A promise which will resolve with an array of UUID strings, uniquely + * @returns + * An Observable which will emit an array of UUID strings, uniquely * identifying each active tunnel. */ - service.getTunnels = function getTunnels() { + getTunnels(): Observable { // Retrieve tunnels - return authenticationService.request({ - method : 'GET', - url : 'api/session/tunnels' - }); - - }; + return this.http.get('api/session/tunnels'); + } /** * Makes a request to the REST API to retrieve the underlying protocol of - * the connection associated with a particular tunnel, returning a promise + * the connection associated with a particular tunnel, returning an Observable * that provides a @link{Protocol} object if successful. * - * @param {String} tunnel + * @param tunnel * The UUID of the tunnel associated with the Guacamole connection * whose underlying protocol is being retrieved. * - * @returns {Promise.} - * A promise which will resolve with a @link{Protocol} object upon + * @returns + * An Observable which will resolve with a @link{Protocol} object upon * success. */ - service.getProtocol = function getProtocol(tunnel) { + getProtocol(tunnel: string): Observable { - return authenticationService.request({ - method : 'GET', - url : 'api/session/tunnels/' + encodeURIComponent(tunnel) - + '/protocol' - }); + return this.http.get('api/session/tunnels/' + encodeURIComponent(tunnel) + '/protocol'); - }; + } /** * Retrieves the set of sharing profiles that the current user can use to * share the active connection of the given tunnel. * - * @param {String} tunnel + * @param tunnel * The UUID of the tunnel associated with the Guacamole connection * whose sharing profiles are being retrieved. * - * @returns {Promise.>} - * A promise which will resolve with a map of @link{SharingProfile} + * @returns + * An observable which will emit a map of @link{SharingProfile} * objects where each key is the identifier of the corresponding * sharing profile. */ - service.getSharingProfiles = function getSharingProfiles(tunnel) { + getSharingProfiles(tunnel: string): Observable> { // Retrieve all associated sharing profiles - return authenticationService.request({ - method : 'GET', - url : 'api/session/tunnels/' + encodeURIComponent(tunnel) - + '/activeConnection/connection/sharingProfiles' - }); + return this.http.get>('api/session/tunnels/' + encodeURIComponent(tunnel) + + '/activeConnection/connection/sharingProfiles'); - }; + } /** * Makes a request to the REST API to generate credentials which have * access strictly to the active connection associated with the given * tunnel, using the restrictions defined by the given sharing profile, - * returning a promise that provides the resulting @link{UserCredentials} + * returning an observable that emits the resulting @link{UserCredentials} * object if successful. * - * @param {String} tunnel + * @param tunnel * The UUID of the tunnel associated with the Guacamole connection * being shared. * - * @param {String} sharingProfile + * @param sharingProfile * The identifier of the connection object dictating the * semantics/restrictions which apply to the shared session. * - * @returns {Promise.} - * A promise which will resolve with a @link{UserCredentials} object + * @returns + * An observable which will emit a @link{UserCredentials} object * upon success. */ - service.getSharingCredentials = function getSharingCredentials(tunnel, sharingProfile) { + getSharingCredentials(tunnel: string, sharingProfile: string): Observable { // Generate sharing credentials - return authenticationService.request({ - method : 'GET', - url : 'api/session/tunnels/' + encodeURIComponent(tunnel) - + '/activeConnection/sharingCredentials/' - + encodeURIComponent(sharingProfile) - }); + return this.http.get('api/session/tunnels/' + encodeURIComponent(tunnel) + + '/activeConnection/sharingCredentials/' + encodeURIComponent(sharingProfile)); - }; + } /** - * Sanitize a filename, replacing all URL path seperators with safe + * Sanitize a filename, replacing all URL path separators with safe * characters. * - * @param {String} filename + * @param filename * An unsanitized filename that may need cleanup. * - * @returns {String} + * @returns * The sanitized filename. */ - var sanitizeFilename = function sanitizeFilename(filename) { + sanitizeFilename(filename: string): string { return filename.replace(/[\\\/]+/g, '_'); - }; + } /** * Makes a request to the REST API to retrieve the contents of a stream @@ -186,39 +161,35 @@ angular.module('rest').factory('tunnelService', ['$injector', * is overwritten after this function returns, resources may not be * properly cleaned up. * - * @param {String} tunnel + * @param tunnel * The UUID of the tunnel associated with the Guacamole connection * whose stream should be downloaded as a file. * - * @param {Guacamole.InputStream} stream + * @param stream * The stream whose contents should be downloaded. * - * @param {String} mimetype + * @param mimetype * The mimetype of the stream being downloaded. This is currently * ignored, with the download forced by using * "application/octet-stream". * - * @param {String} filename + * @param filename * The filename that should be given to the downloaded file. */ - service.downloadStream = function downloadStream(tunnel, stream, mimetype, filename) { + downloadStream(tunnel: string, stream: Guacamole.InputStream, mimetype: string, filename: string): void { - // Work-around for IE missing window.location.origin - if (!$window.location.origin) - var streamOrigin = $window.location.protocol + '//' + $window.location.hostname + ($window.location.port ? (':' + $window.location.port) : ''); - else - var streamOrigin = $window.location.origin; + const streamOrigin = window.location.origin; // Build download URL - var url = streamOrigin - + $window.location.pathname - + 'api/session/tunnels/' + encodeURIComponent(tunnel) - + '/streams/' + encodeURIComponent(stream.index) - + '/' + encodeURIComponent(sanitizeFilename(filename)) - + '?token=' + encodeURIComponent(authenticationService.getCurrentToken()); + const url = streamOrigin + + window.location.pathname + + 'api/session/tunnels/' + encodeURIComponent(tunnel) + + '/streams/' + encodeURIComponent(stream.index) + + '/' + encodeURIComponent(this.sanitizeFilename(filename)) + + '?token=' + encodeURIComponent(this.authenticationService.getCurrentToken() || ''); // Create temporary hidden iframe to facilitate download - var iframe = document.createElement('iframe'); + const iframe = document.createElement('iframe'); iframe.style.position = 'fixed'; iframe.style.border = 'none'; iframe.style.width = '1px'; @@ -231,29 +202,29 @@ angular.module('rest').factory('tunnelService', ['$injector', // Automatically remove iframe from DOM when download completes, if // browser supports tracking of iframe downloads via the "load" event - iframe.onload = function downloadComplete() { + iframe.onload = () => { document.body.removeChild(iframe); }; // Acknowledge (and ignore) any received blobs - stream.onblob = function acknowledgeData() { + stream.onblob = () => { stream.sendAck('OK', Guacamole.Status.Code.SUCCESS); }; // Automatically remove iframe from DOM a few seconds after the stream // ends, in the browser does NOT fire the "load" event for downloads - stream.onend = function downloadComplete() { - $window.setTimeout(function cleanupIframe() { + stream.onend = () => { + window.setTimeout(() => { if (iframe.parentElement) { document.body.removeChild(iframe); } - }, DOWNLOAD_CLEANUP_WAIT); + }, this.DOWNLOAD_CLEANUP_WAIT); }; // Begin download iframe.src = url; - }; + } /** * Makes a request to the REST API to send the contents of the given file @@ -262,58 +233,61 @@ angular.module('rest').factory('tunnelService', ['$injector', * will automatically be split into individual "blob" instructions, as if * sent by the connected Guacamole client. * - * @param {String} tunnel + * @param tunnel * The UUID of the tunnel associated with the Guacamole connection * whose stream should receive the given file. * - * @param {Guacamole.OutputStream} stream + * @param stream * The stream that should receive the given file. * - * @param {File} file + * @param file * The file that should be sent along the given stream. * - * @param {Function} [progressCallback] + * @param progressCallback * An optional callback which, if provided, will be invoked as the * file upload progresses. The current position within the file, in * bytes, will be provided to the callback as the sole argument. * - * @return {Promise} + * @return * A promise which resolves when the upload has completed, and is * rejected with an Error if the upload fails. The Guacamole protocol * status code describing the failure will be included in the Error if * available. If the status code is available, the type of the Error * will be STREAM_ERROR. */ - service.uploadToStream = function uploadToStream(tunnel, stream, file, - progressCallback) { + uploadToStream(tunnel: string, stream: Guacamole.OutputStream, file: File, progressCallback?: (progress: number) => void): Promise { - var deferred = $q.defer(); + let resolve: () => void; + let reject: (error: Error) => void; - // Work-around for IE missing window.location.origin - if (!$window.location.origin) - var streamOrigin = $window.location.protocol + '//' + $window.location.hostname + ($window.location.port ? (':' + $window.location.port) : ''); - else - var streamOrigin = $window.location.origin; + const deferred: Promise = new Promise( + (resolveFn: () => void, rejectFn: (error: Error) => void) => { + resolve = resolveFn; + reject = rejectFn; + } + ); + + const streamOrigin = window.location.origin; // Build upload URL - var url = streamOrigin - + $window.location.pathname - + 'api/session/tunnels/' + encodeURIComponent(tunnel) - + '/streams/' + encodeURIComponent(stream.index) - + '/' + encodeURIComponent(sanitizeFilename(file.name)) - + '?token=' + encodeURIComponent(authenticationService.getCurrentToken()); + const url = streamOrigin + + window.location.pathname + + 'api/session/tunnels/' + encodeURIComponent(tunnel) + + '/streams/' + encodeURIComponent(stream.index) + + '/' + encodeURIComponent(this.sanitizeFilename(file.name)) + + '?token=' + encodeURIComponent(this.authenticationService.getCurrentToken() || ''); /** * Creates a chunk of the inputted file to be uploaded. - * - * @param {Number} offset - * The byte at which to begin the chunk. - * - * @return {File} + * + * @param offset + * The byte at which to begin the chunk. + * + * @return * The file chunk created by this function. */ - const createChunk = (offset) => { - var chunkEnd = Math.min(offset + CHUNK_SIZE, file.size); + const createChunk = (offset: number): Blob => { + const chunkEnd = Math.min(offset + this.CHUNK_SIZE, file.size); const chunk = file.slice(offset, chunkEnd); return chunk; }; @@ -321,15 +295,15 @@ angular.module('rest').factory('tunnelService', ['$injector', /** * POSTs the inputted chunks and recursively calls uploadHandler() * until the upload is complete. - * - * @param {File} chunk + * + * @param chunk * The chunk to be uploaded to the stream. - * - * @param {Number} offset + * + * @param offset * The byte at which the inputted chunk begins. - */ - const uploadChunk = (chunk, offset) => { - var xhr = new XMLHttpRequest(); + */ + const uploadChunk = (chunk: Blob, offset: number): void => { + const xhr = new XMLHttpRequest(); xhr.open('POST', url, true); // Invoke provided callback if upload tracking is supported. @@ -337,11 +311,11 @@ angular.module('rest').factory('tunnelService', ['$injector', xhr.upload.addEventListener('progress', function updateProgress(e) { progressCallback(e.loaded + offset); }); - }; + } // Continue to next chunk, resolve, or reject promise as appropriate // once upload has stopped - xhr.onreadystatechange = function uploadStatusChanged() { + xhr.onreadystatechange = () => { // Ignore state changes prior to completion. if (xhr.readyState !== 4) @@ -350,32 +324,33 @@ angular.module('rest').factory('tunnelService', ['$injector', // Resolve if last chunk or begin next chunk if HTTP status // code indicates success. if (xhr.status >= 200 && xhr.status < 300) { - offset += CHUNK_SIZE; + offset += this.CHUNK_SIZE; if (offset < file.size) uploadHandler(offset); else - deferred.resolve(); + resolve(); } // Parse and reject with resulting JSON error else if (xhr.getResponseHeader('Content-Type') === 'application/json') - deferred.reject(new Error(angular.fromJson(xhr.responseText))); + reject(new Error(JSON.parse(xhr.responseText))); // Warn of lack of permission of a proxy rejects the upload else if (xhr.status >= 400 && xhr.status < 500) - deferred.reject(new Error({ - 'type': Error.Type.STREAM_ERROR, - 'statusCode': Guacamole.Status.Code.CLIENT_FORBIDDEN, - 'message': 'HTTP ' + xhr.status + reject(new Error({ + type : Error.Type.STREAM_ERROR, + statusCode: Guacamole.Status.Code.CLIENT_FORBIDDEN, + message : 'HTTP ' + xhr.status })); // Assume internal error for all other cases else - deferred.reject(new Error({ - 'type': Error.Type.STREAM_ERROR, - 'statusCode': Guacamole.Status.Code.INTERNAL_ERROR, - 'message': 'HTTP ' + xhr.status + reject(new Error({ + type : Error.Type.STREAM_ERROR, + statusCode: Guacamole.Status.Code.SERVER_ERROR, // TODO: Guacamole.Status.Code.INTERNAL_ERROR + // does not exist + message : 'HTTP ' + xhr.status })); }; @@ -386,25 +361,24 @@ angular.module('rest').factory('tunnelService', ['$injector', }; /** - * Handles the recursive upload process. Each time it is called, a + * Handles the recursive upload process. Each time it is called, a * chunk is made with createChunk(), starting at the offset parameter. - * The chunk is then sent by uploadChunk(), which recursively calls - * this handler until the upload process is either completed and the + * The chunk is then sent by uploadChunk(), which recursively calls + * this handler until the upload process is either completed and the * promise is resolved, or fails and the promise is rejected. - * - * @param {Number} offset + * + * @param offset * The byte at which to begin the chunk. */ - const uploadHandler = (offset) => { + const uploadHandler = (offset: number): void => { uploadChunk(createChunk(offset), offset); }; uploadHandler(0); - return deferred.promise; + return deferred; - }; + } - return service; -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user-group.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user-group.service.ts new file mode 100644 index 0000000000..4042e03d13 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user-group.service.ts @@ -0,0 +1,231 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { DirectoryPatch } from '../types/DirectoryPatch'; +import { DirectoryPatchResponse } from '../types/DirectoryPatchResponse'; +import { UserGroup } from '../types/UserGroup'; + +/** + * Service for operating on user groups via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserGroupService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } + + /** + * Makes a request to the REST API to get the list of user groups, + * returning an observable that provides an array of @link{UserGroup} objects if + * successful. + * + * @param dataSource + * The unique identifier of the data source containing the user groups + * to be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param permissionTypes + * The set of permissions to filter with. A user group must have one or + * more of these permissions for a user group to appear in the result. + * If null, no filtering will be performed. Valid values are listed + * within PermissionSet.ObjectType. + * + * @returns + * An observable which will emit a map of @link{UserGroup} objects + * where each key is the identifier of the corresponding user group. + */ + getUserGroups(dataSource: string, permissionTypes?: string[]): Observable> { + + // Add permission filter if specified + let httpParameters = new HttpParams(); + if (permissionTypes) + httpParameters = httpParameters.appendAll({ permission: permissionTypes }); + + // Retrieve user groups + // TODO: cache: cacheService.users, + return this.http.get>( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + { params: httpParameters } + ); + + } + + /** + * Makes a request to the REST API to get the user group having the given + * identifier, returning an observable that provides the corresponding + * @link{UserGroup} if successful. + * + * @param dataSource + * The unique identifier of the data source containing the user group to + * be retrieved. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param identifier + * The identifier of the user group to retrieve. + * + * @returns + * An observable which will emit a @link{UserGroup} upon success. + */ + getUserGroup(dataSource: string, identifier: string): Observable { + + // Retrieve user group + // TODO cache : cacheService.users, + return this.http.get( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(identifier) + ); + + } + + /** + * Makes a request to the REST API to delete a user group, returning an observable + * that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source containing the user group to + * be deleted. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param userGroup + * The user group to delete. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteUserGroup(dataSource: string, userGroup: UserGroup): Observable { + + // Delete user group + return this.http.delete( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier!) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + + } + + /** + * Makes a request to the REST API to create a user group, returning an observable + * that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source in which the user group + * should be created. This identifier corresponds to an + * AuthenticationProvider within the Guacamole web application. + * + * @param userGroup + * The user group to create. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * create operation is successful. + */ + createUserGroup(dataSource: string, userGroup: UserGroup): Observable { + + // Create user group + return this.http.post( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + userGroup + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } + + /** + * Makes a request to the REST API to save a user group, returning an observable + * that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source containing the user group to + * be updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param userGroup + * The user group to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + saveUserGroup(dataSource: string, userGroup: UserGroup): Observable { + + // Update user group + return this.http.put( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups/' + encodeURIComponent(userGroup.identifier!), + userGroup + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } + + /** + * Makes a request to the REST API to apply a supplied list of user group + * patches, returning an observable that can be used for processing the results + * of the call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param dataSource + * The identifier of the data source associated with the user groups to + * be patched. + * + * @param patches + * An array of patches to apply. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + patchUserGroups(dataSource: string, patches: DirectoryPatch[]): Observable { + + // Make the PATCH request + return this.http.patch( + 'api/session/data/' + encodeURIComponent(dataSource) + '/userGroups', + patches + ) + + // Clear the cache + .pipe(tap(patchResponse => { + // TODO: cacheService.users.removeAll(); + return patchResponse; + })); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user.service.ts new file mode 100644 index 0000000000..dcd2b9dca4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/service/user.service.ts @@ -0,0 +1,250 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpClient, HttpParams } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable, tap } from 'rxjs'; +import { DirectoryPatch } from '../types/DirectoryPatch'; +import { DirectoryPatchResponse } from '../types/DirectoryPatchResponse'; +import { User } from '../types/User'; +import { UserPasswordUpdate } from '../types/UserPasswordUpdate'; + +/** + * Service for operating on users via the REST API. + */ +@Injectable({ + providedIn: 'root' +}) +export class UserService { + + /** + * Inject required services. + */ + constructor(private http: HttpClient) { + } + + /** + * Makes a request to the REST API to get the list of users, + * returning an Observable that provides an object of User objects if + * successful. + * + * @param dataSource + * The unique identifier of the data source containing the users to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param permissionTypes + * The set of permissions to filter with. A user must have one or more + * of these permissions for a user to appear in the result. + * If null, no filtering will be performed. Valid values are listed + * within PermissionSet.ObjectType. + * + * @returns An Observable that will emit an object of User objects + * where each key is the identifier (username) of the corresponding user. + */ + getUsers(dataSource: string, permissionTypes?: string[]): Observable> { + const httpParameters = new HttpParams(); + + // Add permission filter if specified + if (permissionTypes) { + httpParameters.appendAll({ permission: permissionTypes }); + } + + // Retrieve users + // TODO: cache : cacheService.users, + return this.http.get>(`api/session/data/${encodeURIComponent(dataSource)}/users`, { params: httpParameters }); + } + + + /** + * Makes a request to the REST API to get the user having the given + * username, returning an observable that provides the corresponding + * @link{User} if successful. + * + * @param dataSource + * The unique identifier of the data source containing the user to be + * retrieved. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param username + * The username of the user to retrieve. + * + * @returns + * An observable which will emit a @link{User} upon success. + */ + getUser(dataSource: string, username: string): Observable { + + // Retrieve user + // TODO: cache: cacheService.users, + return this.http.get('api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username)); + + } + + /** + * Makes a request to the REST API to delete a user, returning an observable + * that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source containing the user to be + * deleted. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param user + * The user to delete. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * delete operation is successful. + */ + deleteUser(dataSource: string, user: User): Observable { + + // Delete user + return this.http.delete('api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username)); + + } + + /** + * Makes a request to the REST API to create a user, returning an observable + * that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source in which the user should be + * created. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param user + * The user to create. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * create operation is successful. + */ + createUser(dataSource: string, user: User): Observable { + + // Create user + return this.http.post('api/session/data/' + encodeURIComponent(dataSource) + '/users', user) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } + + /** + * Makes a request to the REST API to save a user, returning an observable that + * can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param user + * The user to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * save operation is successful. + */ + saveUser(dataSource: string, user: User): Observable { + + // Update user + return this.http.put('api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(user.username), user) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } + + /** + * Makes a request to the REST API to update the password for a user, + * returning an observable that can be used for processing the results of the call. + * + * @param dataSource + * The unique identifier of the data source containing the user to be + * updated. This identifier corresponds to an AuthenticationProvider + * within the Guacamole web application. + * + * @param username + * The username of the user to update. + * + * @param oldPassword + * The exiting password of the user to update. + * + * @param newPassword + * The new password of the user to update. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * password update operation is successful. + */ + updateUserPassword(dataSource: string, username: string, oldPassword: string, newPassword: string): Observable { + + // Update user password + return this.http.put( + 'api/session/data/' + encodeURIComponent(dataSource) + '/users/' + encodeURIComponent(username) + '/password', + new UserPasswordUpdate({ + oldPassword: oldPassword, + newPassword: newPassword + }) + ) + + // Clear the cache + .pipe(tap(() => { + // TODO: cacheService.users.removeAll(); + })); + + } + + + /** + * Makes a request to the REST API to apply a supplied list of user patches, + * returning an observable that can be used for processing the results of the + * call. + * + * This operation is atomic - if any errors are encountered during the + * connection patching process, the entire request will fail, and no + * changes will be persisted. + * + * @param dataSource + * The identifier of the data source associated with the users to be + * patched. + * + * @param patches + * An array of patches to apply. + * + * @returns + * An observable for the HTTP call which will succeed if and only if the + * patch operation is successful. + */ + patchUsers(dataSource: string, patches: DirectoryPatch[]): Observable { + + // Make the PATCH request + return this.http.patch('api/session/data/' + encodeURIComponent(dataSource) + '/users', patches) + + // Clear the cache + .pipe(tap(patchResponse => { + // TODO: cacheService.users.removeAll(); + })); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActiveConnection.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActiveConnection.ts new file mode 100644 index 0000000000..1e320b3ca7 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActiveConnection.ts @@ -0,0 +1,77 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Returned by REST API calls when representing the data + * associated with an active connection. Each active connection is + * effectively a pairing of a connection and the user currently using it, + * along with other information. + */ +export class ActiveConnection { + + /** + * The identifier which uniquely identifies this specific active + * connection. + */ + identifier?: string; + + /** + * The identifier of the connection associated with this active + * connection. + */ + connectionIdentifier?: string; + + /** + * The time that the connection began, in seconds since + * 1970-01-01 00:00:00 UTC, if known. + */ + startDate?: number; + + /** + * The remote host that initiated the connection, if known. + */ + remoteHost?: string; + + /** + * The username of the user associated with the connection, if known. + */ + username?: string; + + /** + * Whether this active connection may be connected to, just as a + * normal connection. + */ + connectable?: boolean; + + /** + * Creates a new ActiveConnection object. + * + * @param template + * The object whose properties should be copied within the new + * ActiveConnection. + */ + constructor(template: Partial = {}) { + this.identifier = template.identifier; + this.connectionIdentifier = template.connectionIdentifier; + this.startDate = template.startDate; + this.remoteHost = template.remoteHost; + this.username = template.username; + this.connectable = template.connectable; + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActivityLog.ts similarity index 59% rename from guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActivityLog.ts index f74e53e05b..267fa30a7a 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/ActivityLog.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ActivityLog.ts @@ -17,73 +17,62 @@ * under the License. */ +import { TranslatableMessage } from './TranslatableMessage'; + /** - * Service which defines the ActivityLog class. + * Returned by REST API calls when representing a log or + * recording associated with a connection's usage history, such as a + * session recording or typescript. */ -angular.module('rest').factory('ActivityLog', [function defineActivityLog() { +export class ActivityLog { + + /** + * The type of this ActivityLog. + */ + type?: string; + + /** + * A human-readable description of this log. + */ + description?: TranslatableMessage; /** - * The object returned by REST API calls when representing a log or - * recording associated with a connection's usage history, such as a - * session recording or typescript. + * Creates a new ActivityLog object. * - * @constructor - * @param {ActivityLog|Object} [template={}] + * @param template * The object whose properties should be copied within the new * ActivityLog. */ - var ActivityLog = function ActivityLog(template) { - - // Use empty object by default - template = template || {}; - - /** - * The type of this ActivityLog. - * - * @type {string} - */ + constructor(template: Partial = {}) { this.type = template.type; - - /** - * A human-readable description of this log. - * - * @type {TranslatableMessage} - */ this.description = template.description; - - }; + } /** * All possible types of ActivityLog. - * - * @type {!object.} */ - ActivityLog.Type = { - + static Type = { /** * A Guacamole session recording in the form of a Guacamole protocol * dump. */ - GUACAMOLE_SESSION_RECORDING : 'GUACAMOLE_SESSION_RECORDING', + GUACAMOLE_SESSION_RECORDING: 'GUACAMOLE_SESSION_RECORDING', /** * A text log from a server-side process, such as the Guacamole web * application or guacd. */ - SERVER_LOG : 'SERVER_LOG', + SERVER_LOG: 'SERVER_LOG', /** * A text session recording in the form of a standard typescript. */ - TYPESCRIPT : 'TYPESCRIPT', + TYPESCRIPT: 'TYPESCRIPT', /** * The timing file related to a typescript. */ - TYPESCRIPT_TIMING : 'TYPESCRIPT_TIMING' - + TYPESCRIPT_TIMING: 'TYPESCRIPT_TIMING' }; - return ActivityLog; - -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Connection.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Connection.ts new file mode 100644 index 0000000000..2c64677e64 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Connection.ts @@ -0,0 +1,107 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Optional } from '../../util/utility-types'; +import { SharingProfile } from './SharingProfile'; + +/** + * Returned by REST API calls when representing the data + * associated with a connection. + */ +export class Connection { + + /** + * The unique identifier associated with this connection. + */ + identifier?: string; + + /** + * The unique identifier of the connection group that contains this + * connection. + */ + parentIdentifier?: string; + + /** + * The human-readable name of this connection, which is not necessarily + * unique. + */ + name?: string; + + /** + * The name of the protocol associated with this connection, such as + * "vnc" or "rdp". + */ + protocol: string; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use, arranged as name/value pairs. This information may not be + * available until directly queried. If this information is + * unavailable, this property will be null or undefined. + */ + parameters?: Record; + + /** + * Arbitrary name/value pairs which further describe this connection. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + */ + attributes: Record; + + /** + * The count of currently active connections using this connection. + * This field will be returned from the REST API during a get + * operation, but manually setting this field will have no effect. + */ + activeConnections?: number; + + /** + * An array of all associated sharing profiles, if known. This property + * may be null or undefined if sharing profiles have not been queried, + * and thus the sharing profiles are unknown. + */ + sharingProfiles?: SharingProfile[]; + + /** + * The time that this connection was last used, in milliseconds since + * 1970-01-01 00:00:00 UTC. If this information is unknown or + * unavailable, this will be null. + */ + lastActive?: number; + + /** + * Creates a new Connection object. + * + * @param template + * The object whose properties should be copied within the new + * Connection. + */ + constructor(template: Optional) { + this.identifier = template.identifier; + this.parentIdentifier = template.parentIdentifier; + this.name = template.name; + this.protocol = template.protocol; + this.parameters = template.parameters; + this.attributes = template.attributes || {}; + this.activeConnections = template.activeConnections; + this.sharingProfiles = template.sharingProfiles; + this.lastActive = template.lastActive; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionGroup.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionGroup.ts new file mode 100644 index 0000000000..badf615b88 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionGroup.ts @@ -0,0 +1,126 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Optional } from '../../util/utility-types'; +import { Connection } from './Connection'; + +/** + * Returned by REST API calls when representing the data + * associated with a connection group. + */ +export class ConnectionGroup { + + /** + * The unique identifier associated with this connection group. + */ + identifier?: string; + + /** + * The unique identifier of the connection group that contains this + * connection group. + * + * @default ConnectionGroup.ROOT_IDENTIFIER + */ + parentIdentifier: string; + + /** + * The human-readable name of this connection group, which is not + * necessarily unique. + */ + name: string; + + /** + * The type of this connection group, which may be either + * ConnectionGroup.Type.ORGANIZATIONAL or + * ConnectionGroup.Type.BALANCING. + * + * @default ConnectionGroup.Type.ORGANIZATIONAL + */ + type: string; + + /** + * An array of all child connections, if known. This property may be + * null or undefined if children have not been queried, and thus the + * child connections are unknown. + */ + childConnections: Connection[] | null | undefined; + + /** + * An array of all child connection groups, if known. This property may + * be null or undefined if children have not been queried, and thus the + * child connection groups are unknown. + */ + childConnectionGroups: ConnectionGroup[] | null | undefined; + + /** + * Arbitrary name/value pairs which further describe this connection + * group. The semantics and validity of these attributes are dictated + * by the extension which defines them. + */ + attributes: Record; + + /** + * The count of currently active connections using this connection + * group. This field will be returned from the REST API during a get + * operation, but manually setting this field will have no effect. + */ + activeConnections: number; + + /** + * Creates a new ConnectionGroup object. + * + * @param template + * The object whose properties should be copied within the new + * ConnectionGroup. + */ + constructor(template: Optional) { + this.identifier = template.identifier; + this.parentIdentifier = template.parentIdentifier || ConnectionGroup.ROOT_IDENTIFIER; + this.name = template.name; + this.type = template.type || ConnectionGroup.Type.ORGANIZATIONAL; + this.childConnections = template.childConnections; + this.childConnectionGroups = template.childConnectionGroups; + this.attributes = template.attributes || {}; + this.activeConnections = template.activeConnections; + } + + /** + * The reserved identifier which always represents the root connection + * group. + */ + static ROOT_IDENTIFIER = 'ROOT'; + + /** + * All valid connection group types. + */ + static Type = { + + /** + * The type string associated with balancing connection groups. + */ + BALANCING: 'BALANCING', + + /** + * The type string associated with organizational connection groups. + */ + ORGANIZATIONAL: 'ORGANIZATIONAL' + + }; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionHistoryEntry.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionHistoryEntry.ts new file mode 100644 index 0000000000..19837adda8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/ConnectionHistoryEntry.ts @@ -0,0 +1,213 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ActivityLog } from './ActivityLog'; + +export declare namespace ConnectionHistoryEntry { + + /** + * Value/unit pair representing the length of time that a connection was + * used. + */ + type Duration = typeof ConnectionHistoryEntry.Duration.prototype; +} + +/** + * Returned by REST API calls when representing the data + * associated with an entry in a connection's usage history. Each history + * entry represents the time at which a particular started using a + * connection and, if applicable, the time that usage stopped. + */ +export class ConnectionHistoryEntry { + + /** + * An arbitrary identifier that uniquely identifies this record + * relative to other records in the same set, or null if no such unique + * identifier exists. + */ + identifier?: string; + + /** + * A UUID that uniquely identifies this record, or null if no such + * unique identifier exists. + */ + uuid?: string; + + /** + * The identifier of the connection associated with this history entry. + */ + connectionIdentifier?: string; + + /** + * The name of the connection associated with this history entry. + */ + connectionName?: string; + + /** + * The time that usage began, in milliseconds since 1970-01-01 00:00:00 UTC. + */ + startDate?: number; + + /** + * The time that usage ended, in milliseconds since 1970-01-01 00:00:00 UTC. + * The absence of an endDate does NOT necessarily indicate that the + * connection is still in use, particularly if the server was shutdown + * or restarted before the history entry could be updated. To determine + * whether a connection is still active, check the active property of + * this history entry. + */ + endDate?: number; + + /** + * The remote host that initiated this connection, if known. + */ + remoteHost?: number; + + /** + * The username of the user associated with this particular usage of + * the connection. + */ + username?: string; + + /** + * Whether this usage of the connection is still active. Note that this + *? is the only accurate way to check for connection activity; the + * absence of endDate does not necessarily imply the connection is + * active, as the history entry may simply be incomplete. + */ + active?: boolean; + + /** + * Arbitrary name/value pairs which further describe this history + * entry. The semantics and validity of these attributes are dictated + * by the extension which defines them. + */ + attributes?: Record; + + /** + * All logs associated and accessible via this record, stored by their + * corresponding unique names. + */ + logs?: Record; + + /** + * Creates a new ConnectionHistoryEntry. + * + * @param template + * The object whose properties should be copied within the new + * ConnectionHistoryEntry. + */ + constructor(template: Partial = {}) { + this.identifier = template.identifier; + this.uuid = template.uuid; + this.connectionIdentifier = template.connectionIdentifier; + this.connectionName = template.connectionName; + this.startDate = template.startDate; + this.endDate = template.endDate; + this.remoteHost = template.remoteHost; + this.username = template.username; + this.active = template.active; + this.attributes = template.attributes; + this.logs = template.logs; + } + + /** + * All possible predicates for sorting ConnectionHistoryEntry objects using + * the REST API. By default, each predicate indicates ascending order. To + * indicate descending order, add "-" to the beginning of the predicate. + */ + static SortPredicate = { + /** + * The date and time that the connection associated with the history + * entry began (connected). + */ + START_DATE: 'startDate' + + }; + + /** + * Value/unit pair representing the length of time that a connection was + * used. + */ + static Duration = class Duration { + + /** + * The number of seconds (or minutes, or hours, etc.) that the + * connection was used. The units associated with this value are + * represented by the unit property. + */ + value: number; + + /** + * The units associated with the value of this duration. Valid + * units are 'second', 'minute', 'hour', and 'day'. + */ + unit: string; + + /** + * Creates a new Duration. + * + * @param milliseconds + * The number of milliseconds that the associated connection was used. + */ + constructor(milliseconds: number) { + + /** + * The provided duration in seconds. + */ + const seconds = milliseconds / 1000; + + /** + * Rounds the given value to the nearest tenth. + * + * @param value The value to round. + * @returns The given value, rounded to the nearest tenth. + */ + const round = (value: number): number => { + return Math.round(value * 10) / 10; + }; + + // Days + if (seconds >= 86400) { + this.value = round(seconds / 86400); + this.unit = 'day'; + } + + // Hours + else if (seconds >= 3600) { + this.value = round(seconds / 3600); + this.unit = 'hour'; + } + + // Minutes + else if (seconds >= 60) { + this.value = round(seconds / 60); + this.unit = 'minute'; + } + + // Seconds + else { + this.value = round(seconds); + this.unit = 'second'; + } + } + + }; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatch.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatch.ts new file mode 100644 index 0000000000..0fb51b16c1 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatch.ts @@ -0,0 +1,88 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Consumed by REST API calls when representing changes to an + * arbitrary set of directory-based objects. + * + * @template T + * The type of object being modified. + */ +export class DirectoryPatch { + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + */ + op?: DirectoryPatch.Operation; + + /** + * The path of the objects to modify. For creation of new objects, this + * should be "/". Otherwise, it should be "/{identifier}", specifying + * the identifier of the existing object being modified. + * + * @default '/' + */ + path: string; + + /** + * The object being added/replaced, or undefined if deleting. + */ + value?: T; + + /** + * Creates a new DirectoryPatch. + * + * @param template + * The object whose properties should be copied within the new + * DirectoryPatch. + */ + constructor(template: Partial> = {}) { + this.op = template.op; + this.path = template.path || '/'; + this.value = template.value; + } + +} + +export namespace DirectoryPatch { + + /** + * All valid patch operations for directory-based objects. + */ + export enum Operation { + + /** + * Adds the specified object to the relation. + */ + ADD = 'add', + + /** + * Replaces (updates) the specified object from the relation. + */ + REPLACE = 'replace', + + /** + * Removes the specified object from the relation. + */ + REMOVE = 'remove' + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchOutcome.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchOutcome.ts new file mode 100644 index 0000000000..da75652938 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchOutcome.ts @@ -0,0 +1,70 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { DirectoryPatch } from './DirectoryPatch'; +import { TranslatableMessage } from './TranslatableMessage'; + +/** + * Returned by a PATCH request to a directory REST API, + * representing the outcome associated with a particular patch in the + * request. This object can indicate either a successful or unsuccessful + * response. The error field is only meaningful for unsuccessful patches. + */ +export class DirectoryPatchOutcome { + + /** + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within DirectoryPatch.Operation. + */ + op?: DirectoryPatch.Operation; + + /** + * The path of the object operated on by the corresponding patch in the + * request. + */ + path?: string; + + /** + * The identifier of the object operated on by the corresponding patch + * in the request. If the object was newly created and the PATCH request + * did not fail, this will be the identifier of the newly created object. + */ + identifier?: string; + + /** + * The error message associated with the failure, if the patch failed to + * apply. + */ + error?: TranslatableMessage; + + /** + * Creates a new DirectoryPatchOutcome. + * + * @param template + * The object whose properties should be copied within the new + * DirectoryPatchOutcome. + */ + constructor(template: Partial = {}) { + this.op = template.op; + this.path = template.path; + this.identifier = template.identifier; + this.error = template.error; + } + +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchResponse.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchResponse.ts index 9538c077ed..d4d1e0eeb6 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/DirectoryPatchResponse.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/DirectoryPatchResponse.ts @@ -17,34 +17,27 @@ * under the License. */ +import { DirectoryPatchOutcome } from './DirectoryPatchOutcome'; + /** - * Service which defines the DirectoryPatchResponse class. + * Returned by a PATCH request to a directory REST API, + * representing the successful response to a patch request. */ -angular.module('rest').factory('DirectoryPatchResponse', [ - function defineDirectoryPatchResponse() { +export class DirectoryPatchResponse { /** - * An object returned by a PATCH request to a directory REST API, - * representing the successful response to a patch request. + * An outcome for each patch in the corresponding patch request. + */ + patches?: DirectoryPatchOutcome[]; + + /** + * Creates a new DirectoryPatchResponse. * - * @param {DirectoryPatchResponse|Object} [template={}] + * @param template * The object whose properties should be copied within the new * DirectoryPatchResponse. */ - const DirectoryPatchResponse = function DirectoryPatchResponse(template) { - - // Use empty object by default - template = template || {}; - - /** - * An outcome for each patch in the corresponding patch request. - * - * @type {DirectoryPatchOutcome[]} - */ + constructor(template: DirectoryPatchResponse = {}) { this.patches = template.patches; - - }; - - return DirectoryPatchResponse; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Error.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Error.ts new file mode 100644 index 0000000000..48b19918b9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Error.ts @@ -0,0 +1,141 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpErrorResponse } from '@angular/common/http'; +import { DirectoryPatchOutcome } from './DirectoryPatchOutcome'; +import { Field } from './Field'; +import { TranslatableMessage } from './TranslatableMessage'; + +/** + * Returned by REST API calls when an error occurs. + */ +export class Error { + + /** + * A human-readable message describing the error that occurred. + */ + message?: string; + + /** + * A message which can be translated using the translation service, + * consisting of a translation guac-key and optional set of substitution + * variables. + */ + translatableMessage?: TranslatableMessage; + + /** + * The Guacamole protocol status code associated with the error that + * occurred. This is only valid for errors of type STREAM_ERROR. + */ + statusCode?: number | null; + + /** + * The type string defining which values this parameter may contain, + * as well as what properties are applicable. Valid types are listed + * within Error.Type. + * + * @default Error.Type.INTERNAL_ERROR + */ + type: Error.Type; + + /** + * Any parameters which were expected in the original request, or are + * now expected as a result of the original request, if any. If no + * such information is available, this will be undefined. + */ + expected?: Field[]; + + /** + * The outcome for each patch that was submitted as part of the request + * that generated this error, if the request was a directory PATCH + * request. In all other cases, this will be undefined. + */ + patches?: DirectoryPatchOutcome[]; + + /** + * Creates a new Error. + * + * @param template + * The object whose properties should be copied within the new + * Error. + */ + constructor(template: Partial | HttpErrorResponse = {}) { + + if (template instanceof HttpErrorResponse) { + this.message = template.message; + this.statusCode = template.status; + this.type = Error.Type.INTERNAL_ERROR; + return; + } + + this.message = template.message; + this.translatableMessage = template.translatableMessage; + this.statusCode = template.statusCode; + this.type = template.type || Error.Type.INTERNAL_ERROR; + this.expected = template.expected; + this.patches = template.patches; + } +} + +export namespace Error { + + /** + * All valid error types. + */ + export enum Type { + /** + * The requested operation could not be performed because the request + * itself was malformed. + */ + BAD_REQUEST = 'BAD_REQUEST', + + /** + * The credentials provided were invalid. + */ + INVALID_CREDENTIALS = 'INVALID_CREDENTIALS', + + /** + * The credentials provided were not necessarily invalid, but were not + * sufficient to determine validity. + */ + INSUFFICIENT_CREDENTIALS = 'INSUFFICIENT_CREDENTIALS', + + /** + * An internal server error has occurred. + */ + INTERNAL_ERROR = 'INTERNAL_ERROR', + + /** + * An object related to the request does not exist. + */ + NOT_FOUND = 'NOT_FOUND', + + /** + * Permission was denied to perform the requested operation. + */ + PERMISSION_DENIED = 'PERMISSION_DENIED', + + /** + * An error occurred within an intercepted stream, terminating that + * stream. The Guacamole protocol status code of that error will be + * stored within statusCode. + */ + STREAM_ERROR = 'STREAM_ERROR' + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/Field.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Field.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/rest/types/Field.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Field.ts index 195db82db9..85f7812579 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Field.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Field.ts @@ -17,97 +17,99 @@ * under the License. */ +import { FormField } from 'guacamole-frontend-ext-lib'; +import { TranslatableMessage } from './TranslatableMessage'; + /** - * Service which defines the Field class. + * The object returned by REST API calls when representing the data + * associated with a field or configuration parameter. */ -angular.module('rest').factory('Field', [function defineField() { - +export class Field implements FormField { + /** - * The object returned by REST API calls when representing the data - * associated with a field or configuration parameter. - * - * @constructor - * @param {Field|Object} [template={}] - * The object whose properties should be copied within the new - * Field. + * The name which uniquely identifies this parameter. */ - var Field = function Field(template) { + name: string; - // Use empty object by default - template = template || {}; + /** + * The type string defining which values this parameter may contain, + * as well as what properties are applicable. Valid types are listed + * within Field.Type. + * + * @default Field.Type.TEXT + */ + type: Field.Type; - /** - * The name which uniquely identifies this parameter. - * - * @type String - */ - this.name = template.name; + /** + * All possible legal values for this parameter. + */ + options?: string[]; - /** - * The type string defining which values this parameter may contain, - * as well as what properties are applicable. Valid types are listed - * within Field.Type. - * - * @type String - * @default Field.Type.TEXT - */ - this.type = template.type || Field.Type.TEXT; + /** + * A message which can be translated using the translation service, + * consisting of a translation key and optional set of substitution + * variables. + */ + translatableMessage?: TranslatableMessage; - /** - * All possible legal values for this parameter. - * - * @type String[] - */ + /** + * The URL to which the user should be redirected when a field of type + * REDIRECT is displayed. + */ + redirectUrl?: string; + + /** + * Creates a new Field instance. + * + * @param template + * The object whose properties should be copied within the new + * Field. + */ + constructor(template: { name: string, type?: Field.Type, options?: string[] }) { + this.name = template.name; + this.type = template.type || Field.Type.TEXT; this.options = template.options; + } - }; +} + +export namespace Field { /** * All valid field types. */ - Field.Type = { - + export enum Type { /** * The type string associated with parameters that may contain a single * line of arbitrary text. - * - * @type String */ - TEXT : 'TEXT', + TEXT = 'TEXT', /** * The type string associated with parameters that may contain an email * address. - * - * @type String */ - EMAIL : 'EMAIL', + EMAIL = 'EMAIL', /** * The type string associated with parameters that may contain an * arbitrary string, where that string represents the username of the * user authenticating with the remote desktop service. - * - * @type String */ - USERNAME : 'USERNAME', + USERNAME = 'USERNAME', /** * The type string associated with parameters that may contain an * arbitrary string, where that string represents the password of the * user authenticating with the remote desktop service. - * - * @type String */ - PASSWORD : 'PASSWORD', + PASSWORD = 'PASSWORD', /** * The type string associated with parameters that may contain only * numeric values. - * - * @type String */ - NUMERIC : 'NUMERIC', + NUMERIC = 'NUMERIC', /** * The type string associated with parameters that may contain only a @@ -115,72 +117,67 @@ angular.module('rest').factory('Field', [function defineField() { * effect. It is assumed that each BOOLEAN field will provide exactly * one possible value (option), which will be the value if that field * is true. - * - * @type String */ - BOOLEAN : 'BOOLEAN', + BOOLEAN = 'BOOLEAN', /** * The type string associated with parameters that may contain a * strictly-defined set of possible values. - * - * @type String */ - ENUM : 'ENUM', + ENUM = 'ENUM', /** * The type string associated with parameters that may contain any * number of lines of arbitrary text. - * - * @type String */ - MULTILINE : 'MULTILINE', + MULTILINE = 'MULTILINE', + + /** + * Field type which allows selection of languages. The languages + * displayed are the set of languages supported by the Guacamole web + * application. Legal values are valid language IDs, as dictated by + * the filenames of Guacamole's available translations. + */ + LANGUAGE = 'LANGUAGE', /** * The type string associated with parameters that may contain timezone - * IDs. Valid timezone IDs are dictated by Java: - * http://docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html#getAvailableIDs%28%29 - * - * @type String + * IDs. Valid timezone IDs are dictated by Java= + * http=//docs.oracle.com/javase/7/docs/api/java/util/TimeZone.html#getAvailableIDs%28%29 */ - TIMEZONE : 'TIMEZONE', + TIMEZONE = 'TIMEZONE', /** * The type string associated with parameters that may contain dates. * The format of the date is standardized as YYYY-MM-DD, zero-padded. - * - * @type String */ - DATE : 'DATE', + DATE = 'DATE', /** * The type string associated with parameters that may contain times. * The format of the time is stnadardized as HH:MM:DD, zero-padded, * 24-hour. - * - * @type String */ - TIME : 'TIME', + TIME = 'TIME', /** * An HTTP query parameter which is expected to be embedded in the URL * given to a user. - * - * @type String */ - QUERY_PARAMETER : 'QUERY_PARAMETER', + QUERY_PARAMETER = 'QUERY_PARAMETER', /** * The type string associated with parameters that may contain color * schemes accepted by the Guacamole server terminal emulator and * protocols which leverage it. - * - * @type String */ - TERMINAL_COLOR_SCHEME : 'TERMINAL_COLOR_SCHEME' - - }; + TERMINAL_COLOR_SCHEME = 'TERMINAL_COLOR_SCHEME', - return Field; + /** + * Field type that supports redirecting the client browser to another + * URL. + */ + REDIRECT = 'REDIRECT' + } -}]); \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/Form.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Form.ts similarity index 58% rename from guacamole/src/main/frontend/src/app/rest/types/Form.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Form.ts index 215a964afd..083c5b7042 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Form.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Form.ts @@ -17,42 +17,34 @@ * under the License. */ +import { Field } from './Field'; + /** - * Service which defines the Form class. + * The object returned by REST API calls when representing the data + * associated with a form or set of configuration parameters. */ -angular.module('rest').factory('Form', [function defineForm() { +export class Form { + /** + * The name which uniquely identifies this form, or null if this form + * has no name. + */ + name?: string; + + /** + * All fields contained within this form. + */ + fields: Field[]; /** - * The object returned by REST API calls when representing the data - * associated with a form or set of configuration parameters. + * Creates a new Form. This constructor initializes the properties of the + * new Form with the corresponding properties of the given template. * - * @constructor - * @param {Form|Object} [template={}] + * @param template * The object whose properties should be copied within the new * Form. */ - var Form = function Form(template) { - - // Use empty object by default - template = template || {}; - - /** - * The name which uniquely identifies this form, or null if this form - * has no name. - * - * @type String - */ + constructor(template: Partial
      = {}) { this.name = template.name; - - /** - * All fields contained within this form. - * - * @type Field[] - */ this.fields = template.fields || []; - - }; - - return Form; - -}]); + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionFlagSet.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionFlagSet.ts new file mode 100644 index 0000000000..6a96ac259e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionFlagSet.ts @@ -0,0 +1,239 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { PermissionSet } from './PermissionSet'; + +/** + * Alternative view of a @link{PermissionSet} which allows manipulation of + * each permission through the setting (or retrieval) of boolean property + * values. + */ +export class PermissionFlagSet { + + /** + * The granted state of each system permission, as a map of system + * permission type string to boolean value. A particular permission is + * granted if its corresponding boolean value is set to true. Valid + * permission type strings are defined within + * PermissionSet.SystemPermissionType. Permissions which are not + * granted may be set to false, but this is not required. + */ + systemPermissions: Record; + + /** + * The granted state of each permission for each connection, as a map + * of object permission type string to permission map. The permission + * map is, in turn, a map of connection identifier to boolean value. A + * particular permission is granted if its corresponding boolean value + * is set to true. Valid permission type strings are defined within + * PermissionSet.ObjectPermissionType. Permissions which are not + * granted may be set to false, but this is not required. + */ + connectionPermissions: Record>; + + /** + * The granted state of each permission for each connection group, as a + * map of object permission type string to permission map. The + * permission map is, in turn, a map of connection group identifier to + * boolean value. A particular permission is granted if its + * corresponding boolean value is set to true. Valid permission type + * strings are defined within PermissionSet.ObjectPermissionType. + * Permissions which are not granted may be set to false, but this is + * not required. + */ + connectionGroupPermissions: Record>; + + /** + * The granted state of each permission for each sharing profile, as a + * map of object permission type string to permission map. The + * permission map is, in turn, a map of sharing profile identifier to + * boolean value. A particular permission is granted if its + * corresponding boolean value is set to true. Valid permission type + * strings are defined within PermissionSet.ObjectPermissionType. + * Permissions which are not granted may be set to false, but this is + * not required. + */ + sharingProfilePermissions: Record>; + + /** + * The granted state of each permission for each active connection, as + * a map of object permission type string to permission map. The + * permission map is, in turn, a map of active connection identifier to + * boolean value. A particular permission is granted if its + * corresponding boolean value is set to true. Valid permission type + * strings are defined within PermissionSet.ObjectPermissionType. + * Permissions which are not granted may be set to false, but this is + * not required. + */ + activeConnectionPermissions: Record>; + + /** + * The granted state of each permission for each user, as a map of + * object permission type string to permission map. The permission map + * is, in turn, a map of username to boolean value. A particular + * permission is granted if its corresponding boolean value is set to + * true. Valid permission type strings are defined within + * PermissionSet.ObjectPermissionType. Permissions which are not + * granted may be set to false, but this is not required. + */ + userPermissions: Record>; + + /** + * The granted state of each permission for each user group, as a map of + * object permission type string to permission map. The permission map + * is, in turn, a map of group identifier to boolean value. A particular + * permission is granted if its corresponding boolean value is set to + * true. Valid permission type strings are defined within + * PermissionSet.ObjectPermissionType. Permissions which are not + * granted may be set to false, but this is not required. + */ + userGroupPermissions: Record>; + + /** + * Creates a new PermissionFlagSet. + * + * @param template + * The object whose properties should be copied within the new + * PermissionFlagSet. + */ + constructor(template: Partial = {}) { + + this.systemPermissions = template.systemPermissions || {}; + + this.connectionPermissions = template.connectionPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + + this.connectionGroupPermissions = template.connectionGroupPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + + this.sharingProfilePermissions = template.sharingProfilePermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + + this.activeConnectionPermissions = template.activeConnectionPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + + this.userPermissions = template.userPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + + this.userGroupPermissions = template.userGroupPermissions || { + 'READ' : {}, + 'UPDATE' : {}, + 'DELETE' : {}, + 'ADMINISTER': {} + }; + } + + /** + * Creates a new PermissionFlagSet, populating it with all the permissions + * indicated as granted within the given PermissionSet. + * + * @param permissionSet + * The PermissionSet containing the permissions to be copied into a new + * PermissionFlagSet. + * + * @returns + * A new PermissionFlagSet containing flags representing all granted + * permissions from the given PermissionSet. + */ + static fromPermissionSet(permissionSet: PermissionSet): PermissionFlagSet { + + const permissionFlagSet = new PermissionFlagSet(); + + // Add all granted system permissions + permissionSet.systemPermissions.forEach(function addSystemPermission(type) { + permissionFlagSet.systemPermissions[type] = true; + }); + + // Add all granted connection permissions + PermissionFlagSet.addObjectPermissions(permissionSet.connectionPermissions, permissionFlagSet.connectionPermissions); + + // Add all granted connection group permissions + PermissionFlagSet.addObjectPermissions(permissionSet.connectionGroupPermissions, permissionFlagSet.connectionGroupPermissions); + + // Add all granted sharing profile permissions + PermissionFlagSet.addObjectPermissions(permissionSet.sharingProfilePermissions, permissionFlagSet.sharingProfilePermissions); + + // Add all granted active connection permissions + PermissionFlagSet.addObjectPermissions(permissionSet.activeConnectionPermissions, permissionFlagSet.activeConnectionPermissions); + + // Add all granted user permissions + PermissionFlagSet.addObjectPermissions(permissionSet.userPermissions, permissionFlagSet.userPermissions); + + // Add all granted user group permissions + PermissionFlagSet.addObjectPermissions(permissionSet.userGroupPermissions, permissionFlagSet.userGroupPermissions); + + return permissionFlagSet; + + } + + /** + * Iterates through all permissions in the given permission map, setting + * the corresponding permission flags in the given permission flag map. + * + * @param permMap + * Map of object identifiers to the set of granted permissions. Each + * permission is represented by a string listed within + * PermissionSet.ObjectPermissionType. + * + * @param flagMap + * Map of permission type strings to identifier/flag pairs representing + * whether the permission of that type is granted for the object having + * the associated identifier. + */ + private static addObjectPermissions(permMap: Record, flagMap: Record>): void { + + // For each defined identifier in the permission map + for (const identifier in permMap) { + + // Pull the permission array and loop through each permission + const permissions = permMap[identifier]; + permissions.forEach(function addObjectPermission(type) { + + // Get identifier/flag mapping, creating first if necessary + const objectFlags = flagMap[type] = flagMap[type] || {}; + + // Set flag for current permission + objectFlags[identifier] = true; + + }); + + } + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionPatch.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionPatch.ts new file mode 100644 index 0000000000..3c309f8f8a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionPatch.ts @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Returned by REST API calls when representing changes to the + * permissions granted to a specific user. + */ +export class PermissionPatch { + + /** + * The operation to apply to the permissions indicated by the path. + * Valid operation values are defined within PermissionPatch.Operation. + */ + op?: PermissionPatch.Operation; + + /** + * The path of the permissions to modify. Depending on the type of the + * permission, this will be either "/connectionPermissions/ID", + * "/connectionGroupPermissions/ID", "/userPermissions/ID", or + * "/systemPermissions", where "ID" is the identifier of the object + * to which the permissions apply, if any. + */ + path?: string; + + /** + * The permissions being added or removed. If the permission applies to + * an object, such as a connection or connection group, this will be a + * value from PermissionSet.ObjectPermissionType. If the permission + * applies to the system as a whole (the path is "/systemPermissions"), + * this will be a value from PermissionSet.SystemPermissionType. + */ + value?: string; + + /** + * Creates a new PermissionPatch. + * + * @param template + * The object whose properties should be copied within the new + * PermissionPatch. + */ + constructor(template: PermissionPatch = {}) { + this.op = template.op; + this.path = template.path; + this.value = template.value; + } + +} + +export namespace PermissionPatch { + + /** + * All valid patch operations for permissions. Currently, only add and + * remove are supported. + */ + export enum Operation { + + /** + * Adds (grants) the specified permission. + */ + ADD = 'add', + + /** + * Removes (revokes) the specified permission. + */ + REMOVE = 'remove' + + } + +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/PermissionSet.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionSet.ts similarity index 63% rename from guacamole/src/main/frontend/src/app/rest/types/PermissionSet.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionSet.ts index 2a549456bb..2ca932264b 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/PermissionSet.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/PermissionSet.ts @@ -18,176 +18,95 @@ */ /** - * Service which defines the PermissionSet class. + * Returned by REST API calls when representing the permissions + * granted to a specific user. */ -angular.module('rest').factory('PermissionSet', [function definePermissionSet() { - - /** - * The object returned by REST API calls when representing the permissions - * granted to a specific user. - * - * @constructor - * @param {PermissionSet|Object} [template={}] - * The object whose properties should be copied within the new - * PermissionSet. - */ - var PermissionSet = function PermissionSet(template) { - - // Use empty object by default - template = template || {}; - - /** - * Map of connection identifiers to the corresponding array of granted - * permissions. Each permission is represented by a string listed - * within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.connectionPermissions = template.connectionPermissions || {}; - - /** - * Map of connection group identifiers to the corresponding array of - * granted permissions. Each permission is represented by a string - * listed within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.connectionGroupPermissions = template.connectionGroupPermissions || {}; - - /** - * Map of sharing profile identifiers to the corresponding array of - * granted permissions. Each permission is represented by a string - * listed within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.sharingProfilePermissions = template.sharingProfilePermissions || {}; - - /** - * Map of active connection identifiers to the corresponding array of - * granted permissions. Each permission is represented by a string - * listed within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.activeConnectionPermissions = template.activeConnectionPermissions || {}; - - /** - * Map of user identifiers to the corresponding array of granted - * permissions. Each permission is represented by a string listed - * within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.userPermissions = template.userPermissions || {}; - - /** - * Map of user group identifiers to the corresponding array of granted - * permissions. Each permission is represented by a string listed - * within PermissionSet.ObjectPermissionType. - * - * @type Object. - */ - this.userGroupPermissions = template.userGroupPermissions || {}; - - /** - * Array of granted system permissions. Each permission is represented - * by a string listed within PermissionSet.SystemPermissionType. - * - * @type String[] - */ - this.systemPermissions = template.systemPermissions || []; - - }; +export class PermissionSet { /** - * Valid object permission type strings. + * Map of connection identifiers to the corresponding array of granted + * permissions. Each permission is represented by a string listed + * within PermissionSet.ObjectPermissionType. */ - PermissionSet.ObjectPermissionType = { - - /** - * Permission to read from the specified object. - */ - READ : "READ", - - /** - * Permission to update the specified object. - */ - UPDATE : "UPDATE", - - /** - * Permission to delete the specified object. - */ - DELETE : "DELETE", - - /** - * Permission to administer the specified object - */ - ADMINISTER : "ADMINISTER" - - }; + connectionPermissions: Record; /** - * Valid system permission type strings. + * Map of connection group identifiers to the corresponding array of + * granted permissions. Each permission is represented by a string + * listed within PermissionSet.ObjectPermissionType. */ - PermissionSet.SystemPermissionType = { - - /** - * Permission to administer the entire system. - */ - ADMINISTER : "ADMINISTER", - - /** - * Permission to view connection and user records for the entire system. - */ - AUDIT : "AUDIT", + connectionGroupPermissions: Record; - /** - * Permission to create new users. - */ - CREATE_USER : "CREATE_USER", + /** + * Map of sharing profile identifiers to the corresponding array of + * granted permissions. Each permission is represented by a string + * listed within PermissionSet.ObjectPermissionType. + */ + sharingProfilePermissions: Record; - /** - * Permission to create new user groups. - */ - CREATE_USER_GROUP : "CREATE_USER_GROUP", + /** + * Map of active connection identifiers to the corresponding array of + * granted permissions. Each permission is represented by a string + * listed within PermissionSet.ObjectPermissionType. + */ + activeConnectionPermissions: Record; - /** - * Permission to create new connections. - */ - CREATE_CONNECTION : "CREATE_CONNECTION", + /** + * Map of user identifiers to the corresponding array of granted + * permissions. Each permission is represented by a string listed + * within PermissionSet.ObjectPermissionType. + */ + userPermissions: Record; - /** - * Permission to create new connection groups. - */ - CREATE_CONNECTION_GROUP : "CREATE_CONNECTION_GROUP", + /** + * Map of user group identifiers to the corresponding array of granted + * permissions. Each permission is represented by a string listed + * within PermissionSet.ObjectPermissionType. + */ + userGroupPermissions: Record; - /** - * Permission to create new sharing profiles. - */ - CREATE_SHARING_PROFILE : "CREATE_SHARING_PROFILE" + /** + * Array of granted system permissions. Each permission is represented + * by a string listed within PermissionSet.SystemPermissionType. + */ + systemPermissions: string[]; - }; + /** + * Creates a new PermissionSet. + * + * @param template + * The object whose properties should be copied within the new + * PermissionSet. + */ + constructor(template: Partial = {}) { + this.connectionPermissions = template.connectionPermissions || {}; + this.connectionGroupPermissions = template.connectionGroupPermissions || {}; + this.sharingProfilePermissions = template.sharingProfilePermissions || {}; + this.activeConnectionPermissions = template.activeConnectionPermissions || {}; + this.userPermissions = template.userPermissions || {}; + this.userGroupPermissions = template.userGroupPermissions || {}; + this.systemPermissions = template.systemPermissions || []; + } /** * Returns whether the given permission is granted for at least one * arbitrary object, regardless of ID. * - * @param {Object.} permMap + * @param permMap * The permission map to check, where each entry maps an object * identifer to the array of granted permissions. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * - * @returns {Boolean} + * + * @returns * true if the permission is present (granted), false otherwise. */ - var containsPermission = function containsPermission(permMap, type) { + private static containsPermission(permMap: Record, type: PermissionSet.ObjectPermissionType): boolean { // Search all identifiers for given permission - for (var identifier in permMap) { + for (const identifier in permMap) { // If permission is granted, then no further searching is necessary if (permMap[identifier].indexOf(type) !== -1) @@ -197,29 +116,28 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // No such permission exists return false; - - }; + } /** * Returns whether the given permission is granted for the arbitrary * object having the given ID. If no ID is given, this function determines * whether the permission is granted at all for any such arbitrary object. * - * @param {Object.} permMap + * @param permMap * The permission map to check, where each entry maps an object - * identifer to the array of granted permissions. + * identifier to the array of granted permissions. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * - * @param {String} [identifier] + * + * @param identifier * The identifier of the object to which the permission applies. * * @returns {Boolean} * true if the permission is present (granted), false otherwise. */ - var hasPermission = function hasPermission(permMap, type, identifier) { + private static hasPermission(permMap: Record, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { // No permission if no permission map at all if (!permMap) @@ -227,7 +145,7 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // If no identifier given, search ignoring the identifier if (!identifier) - return containsPermission(permMap, type); + return PermissionSet.containsPermission(permMap, type); // If identifier not present at all, there are no such permissions if (!(identifier in permMap)) @@ -235,172 +153,172 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() return permMap[identifier].indexOf(type) !== -1; - }; + } /** * Returns whether the given permission is granted for the connection * having the given ID. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier + * + * @param identifier * The identifier of the connection to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasConnectionPermission = function hasConnectionPermission(permSet, type, identifier) { - return hasPermission(permSet.connectionPermissions, type, identifier); - }; + static hasConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.connectionPermissions, type, identifier); + } /** * Returns whether the given permission is granted for the connection group * having the given ID. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier + * + * @param identifier * The identifier of the connection group to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasConnectionGroupPermission = function hasConnectionGroupPermission(permSet, type, identifier) { - return hasPermission(permSet.connectionGroupPermissions, type, identifier); - }; + static hasConnectionGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.connectionGroupPermissions, type, identifier); + } /** * Returns whether the given permission is granted for the sharing profile * having the given ID. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the sharing profile to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasSharingProfilePermission = function hasSharingProfilePermission(permSet, type, identifier) { - return hasPermission(permSet.sharingProfilePermissions, type, identifier); - }; + static hasSharingProfilePermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.sharingProfilePermissions, type, identifier); + } /** * Returns whether the given permission is granted for the active * connection having the given ID. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. - * - * @param {String} identifier + * + * @param identifier * The identifier of the active connection to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasActiveConnectionPermission = function hasActiveConnectionPermission(permSet, type, identifier) { - return hasPermission(permSet.activeConnectionPermissions, type, identifier); - }; + static hasActiveConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.activeConnectionPermissions, type, identifier); + } /** * Returns whether the given permission is granted for the user having the * given ID. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasUserPermission = function hasUserPermission(permSet, type, identifier) { - return hasPermission(permSet.userPermissions, type, identifier); - }; + static hasUserPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.userPermissions, type, identifier); + } /** * Returns whether the given permission is granted for the user group having * the given identifier. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user group to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasUserGroupPermission = function hasUserGroupPermission(permSet, type, identifier) { - return hasPermission(permSet.userGroupPermissions, type, identifier); - }; + static hasUserGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier?: string): boolean { + return PermissionSet.hasPermission(permSet.userGroupPermissions, type, identifier); + } /** * Returns whether the given permission is granted at the system level. * - * @param {PermissionSet|Object} permSet + * @param permSet * The permission set to check. * - * @param {String} type + * @param type * The permission to search for, as defined by * PermissionSet.SystemPermissionType. * - * @returns {Boolean} + * @returns * true if the permission is present (granted), false otherwise. */ - PermissionSet.hasSystemPermission = function hasSystemPermission(permSet, type) { + static hasSystemPermission(permSet: PermissionSet, type: PermissionSet.SystemPermissionType): boolean { if (!permSet.systemPermissions) return false; return permSet.systemPermissions.indexOf(type) !== -1; - }; + } /** * Adds the given system permission to the given permission set, if not * already present. If the permission is already present, this function has * no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.SystemPermissionType. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addSystemPermission = function addSystemPermission(permSet, type) { + static addSystemPermission(permSet: PermissionSet, type: PermissionSet.SystemPermissionType): boolean { permSet.systemPermissions = permSet.systemPermissions || []; @@ -412,30 +330,29 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // Permission already present return false; - - }; + } /** * Removes the given system permission from the given permission set, if * present. If the permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.SystemPermissionType. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeSystemPermission = function removeSystemPermission(permSet, type) { + static removeSystemPermission(permSet: PermissionSet, type: PermissionSet.SystemPermissionType): boolean { permSet.systemPermissions = permSet.systemPermissions || []; // Remove permission, if it exists - var permLocation = permSet.systemPermissions.indexOf(type); + const permLocation = permSet.systemPermissions.indexOf(type); if (permLocation !== -1) { permSet.systemPermissions.splice(permLocation, 1); return true; @@ -444,33 +361,33 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // Permission not present return false; - }; + } /** - * Adds the given permission applying to the arbitrary object with the + * Adds the given permission applying to the arbitrary object with the * given ID to the given permission set, if not already present. If the * permission is already present, this function has no effect. * - * @param {Object.} permMap + * @param permMap * The permission map to modify, where each entry maps an object - * identifer to the array of granted permissions. + * identifier to the array of granted permissions. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the arbitrary object to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - var addObjectPermission = function addObjectPermission(permMap, type, identifier) { + static addObjectPermission(permMap: Record, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { // Pull array of permissions, creating it if necessary - var permArray = permMap[identifier] = permMap[identifier] || []; + const permArray = permMap[identifier] = permMap[identifier] || []; // Add permission, if it doesn't already exist if (permArray.indexOf(type) === -1) { @@ -481,40 +398,40 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // Permission already present return false; - }; + } /** - * Removes the given permission applying to the arbitrary object with the + * Removes the given permission applying to the arbitrary object with the * given ID from the given permission set, if present. If the permission is * not present, this function has no effect. * - * @param {Object.} permMap + * @param permMap * The permission map to modify, where each entry maps an object - * identifer to the array of granted permissions. + * identifier to the array of granted permissions. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the arbitrary object to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - var removeObjectPermission = function removeObjectPermission(permMap, type, identifier) { + static removeObjectPermission(permMap: Record, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { // Pull array of permissions - var permArray = permMap[identifier]; + const permArray = permMap[identifier]; // If no permissions present at all, nothing to remove if (!(identifier in permMap)) return false; // Remove permission, if it exists - var permLocation = permArray.indexOf(type); + const permLocation = permArray.indexOf(type); if (permLocation !== -1) { permArray.splice(permLocation, 1); return true; @@ -523,55 +440,55 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() // Permission not present return false; - }; + } /** * Adds the given connection permission applying to the connection with * the given ID to the given permission set, if not already present. If the * permission is already present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the connection to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addConnectionPermission = function addConnectionPermission(permSet, type, identifier) { + static addConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.connectionPermissions = permSet.connectionPermissions || {}; - return addObjectPermission(permSet.connectionPermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.connectionPermissions, type, identifier); + } /** * Removes the given connection permission applying to the connection with * the given ID from the given permission set, if present. If the * permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the connection to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeConnectionPermission = function removeConnectionPermission(permSet, type, identifier) { + static removeConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.connectionPermissions = permSet.connectionPermissions || {}; - return removeObjectPermission(permSet.connectionPermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.connectionPermissions, type, identifier); + } /** * Adds the given connection group permission applying to the connection @@ -579,100 +496,100 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() * present. If the permission is already present, this function has no * effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the connection group to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addConnectionGroupPermission = function addConnectionGroupPermission(permSet, type, identifier) { + static addConnectionGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.connectionGroupPermissions = permSet.connectionGroupPermissions || {}; - return addObjectPermission(permSet.connectionGroupPermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.connectionGroupPermissions, type, identifier); + } /** * Removes the given connection group permission applying to the connection * group with the given ID from the given permission set, if present. If * the permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the connection group to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeConnectionGroupPermission = function removeConnectionGroupPermission(permSet, type, identifier) { + static removeConnectionGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.connectionGroupPermissions = permSet.connectionGroupPermissions || {}; - return removeObjectPermission(permSet.connectionGroupPermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.connectionGroupPermissions, type, identifier); + } /** * Adds the given sharing profile permission applying to the sharing profile * with the given ID to the given permission set, if not already present. If * the permission is already present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the sharing profile to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addSharingProfilePermission = function addSharingProfilePermission(permSet, type, identifier) { + static addSharingProfilePermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.sharingProfilePermissions = permSet.sharingProfilePermissions || {}; - return addObjectPermission(permSet.sharingProfilePermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.sharingProfilePermissions, type, identifier); + } /** * Removes the given sharing profile permission applying to the sharing * profile with the given ID from the given permission set, if present. If * the permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the sharing profile to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeSharingProfilePermission = function removeSharingProfilePermission(permSet, type, identifier) { + static removeSharingProfilePermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.sharingProfilePermissions = permSet.sharingProfilePermissions || {}; - return removeObjectPermission(permSet.sharingProfilePermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.sharingProfilePermissions, type, identifier); + } /** * Adds the given active connection permission applying to the connection @@ -680,147 +597,213 @@ angular.module('rest').factory('PermissionSet', [function definePermissionSet() * present. If the permission is already present, this function has no * effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the active connection to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addActiveConnectionPermission = function addActiveConnectionPermission(permSet, type, identifier) { + static addActiveConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.activeConnectionPermissions = permSet.activeConnectionPermissions || {}; - return addObjectPermission(permSet.activeConnectionPermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.activeConnectionPermissions, type, identifier); + } /** * Removes the given active connection permission applying to the * connection group with the given ID from the given permission set, if * present. If the permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the active connection to which the permission * applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeActiveConnectionPermission = function removeActiveConnectionPermission(permSet, type, identifier) { + static removeActiveConnectionPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.activeConnectionPermissions = permSet.activeConnectionPermissions || {}; - return removeObjectPermission(permSet.activeConnectionPermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.activeConnectionPermissions, type, identifier); + } /** * Adds the given user permission applying to the user with the given ID to * the given permission set, if not already present. If the permission is * already present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addUserPermission = function addUserPermission(permSet, type, identifier) { + static addUserPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.userPermissions = permSet.userPermissions || {}; - return addObjectPermission(permSet.userPermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.userPermissions, type, identifier); + } /** * Removes the given user permission applying to the user with the given ID * from the given permission set, if present. If the permission is not * present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user to whom the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeUserPermission = function removeUserPermission(permSet, type, identifier) { + static removeUserPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.userPermissions = permSet.userPermissions || {}; - return removeObjectPermission(permSet.userPermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.userPermissions, type, identifier); + } /** * Adds the given user group permission applying to the user group with the * given identifier to the given permission set, if not already present. If * the permission is already present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to add, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user group to which the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was added, false if the permission was * already present in the given permission set. */ - PermissionSet.addUserGroupPermission = function addUserGroupPermission(permSet, type, identifier) { + static addUserGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.userGroupPermissions = permSet.userGroupPermissions || {}; - return addObjectPermission(permSet.userGroupPermissions, type, identifier); - }; + return PermissionSet.addObjectPermission(permSet.userGroupPermissions, type, identifier); + } /** * Removes the given user group permission applying to the user group with * the given identifier from the given permission set, if present. If the * permission is not present, this function has no effect. * - * @param {PermissionSet} permSet + * @param permSet * The permission set to modify. * - * @param {String} type + * @param type * The permission to remove, as defined by * PermissionSet.ObjectPermissionType. * - * @param {String} identifier + * @param identifier * The identifier of the user group to whom the permission applies. * - * @returns {Boolean} + * @returns * true if the permission was removed, false if the permission was not * present in the given permission set. */ - PermissionSet.removeUserGroupPermission = function removeUserGroupPermission(permSet, type, identifier) { + static removeUserGroupPermission(permSet: PermissionSet, type: PermissionSet.ObjectPermissionType, identifier: string): boolean { permSet.userGroupPermissions = permSet.userGroupPermissions || {}; - return removeObjectPermission(permSet.userGroupPermissions, type, identifier); - }; + return PermissionSet.removeObjectPermission(permSet.userGroupPermissions, type, identifier); + } + +} - return PermissionSet; +export namespace PermissionSet { -}]); \ No newline at end of file + /** + * Valid object permission type strings. + */ + export enum ObjectPermissionType { + /** + * Permission to read from the specified object. + */ + READ = 'READ', + + /** + * Permission to update the specified object. + */ + UPDATE = 'UPDATE', + + /** + * Permission to delete the specified object. + */ + DELETE = 'DELETE', + + /** + * Permission to administer the specified object + */ + ADMINISTER = 'ADMINISTER' + } + + /** + * Valid system permission type strings. + */ + export enum SystemPermissionType { + /** + * Permission to administer the entire system. + */ + ADMINISTER = 'ADMINISTER', + + /** + * Permission to view connection and user records for the entire system. + */ + AUDIT = 'AUDIT', + + /** + * Permission to create new users. + */ + CREATE_USER = 'CREATE_USER', + + /** + * Permission to create new user groups. + */ + CREATE_USER_GROUP = 'CREATE_USER_GROUP', + + /** + * Permission to create new connections. + */ + CREATE_CONNECTION = 'CREATE_CONNECTION', + + /** + * Permission to create new connection groups. + */ + CREATE_CONNECTION_GROUP = 'CREATE_CONNECTION_GROUP', + + /** + * Permission to create new sharing profiles. + */ + CREATE_SHARING_PROFILE = 'CREATE_SHARING_PROFILE' + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/Protocol.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Protocol.ts similarity index 55% rename from guacamole/src/main/frontend/src/app/rest/types/Protocol.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Protocol.ts index 0b86d5b2db..3262b00672 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/Protocol.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/Protocol.ts @@ -17,55 +17,49 @@ * under the License. */ +import { canonicalize } from '../../locale/service/translation.service'; +import { Form } from './Form'; + /** - * Service which defines the Protocol class. + * Returned by REST API calls when representing the data + * associated with a supported remote desktop protocol. */ -angular.module('rest').factory('Protocol', ['$injector', function defineProtocol($injector) { +export class Protocol { - // Required services - var translationStringService = $injector.get('translationStringService'); + /** + * The name which uniquely identifies this protocol. + */ + name?: string; /** - * The object returned by REST API calls when representing the data - * associated with a supported remote desktop protocol. - * - * @constructor - * @param {Protocol|Object} [template={}] - * The object whose properties should be copied within the new - * Protocol. + * An array of forms describing all known parameters for a connection + * using this protocol, including their types and other information. + * + * @default [] */ - var Protocol = function Protocol(template) { + connectionForms: Form[]; - // Use empty object by default - template = template || {}; + /** + * An array of forms describing all known parameters relevant to a + * sharing profile whose primary connection uses this protocol, + * including their types, and other information. + * + * @default [] + */ + sharingProfileForms: Form[]; - /** - * The name which uniquely identifies this protocol. - * - * @type String - */ + /** + * Creates a new Protocol. + * + * @param template + * The object whose properties should be copied within the new + * Protocol. + */ + constructor(template: Partial = {}) { this.name = template.name; - - /** - * An array of forms describing all known parameters for a connection - * using this protocol, including their types and other information. - * - * @type Form[] - * @default [] - */ this.connectionForms = template.connectionForms || []; - - /** - * An array of forms describing all known parameters relevant to a - * sharing profile whose primary connection uses this protocol, - * including their types, and other information. - * - * @type Form[] - * @default [] - */ this.sharingProfileForms = template.sharingProfileForms || []; - - }; + } /** * Returns the translation string namespace for the protocol having the @@ -74,24 +68,24 @@ angular.module('rest').factory('Protocol', ['$injector', function defineProtocol * PROTOCOL_NAME * * where NAME is the protocol name transformed via - * translationStringService.canonicalize(). + * canonicalize(). * - * @param {String} protocolName + * @param protocolName * The name of the protocol. * - * @returns {String} - * The translation namespace for the protocol specified, or null if no + * @returns + * The translation namespace for the protocol specified, or undefined if no * namespace could be generated. */ - Protocol.getNamespace = function getNamespace(protocolName) { + static getNamespace(protocolName: string): string | undefined { // Do not generate a namespace if no protocol is selected if (!protocolName) - return null; + return undefined; - return 'PROTOCOL_' + translationStringService.canonicalize(protocolName); + return 'PROTOCOL_' + canonicalize(protocolName); - }; + } /** * Given the internal name of a protocol, produces the translation string @@ -101,19 +95,17 @@ angular.module('rest').factory('Protocol', ['$injector', function defineProtocol * NAMESPACE.NAME * * where NAMESPACE is the namespace generated from - * $scope.getNamespace(). + * Protocol.getNamespace(). * - * @param {String} protocolName + * @param protocolName * The name of the protocol. * - * @returns {String} + * @returns * The translation string which produces the localized name of the * protocol specified. */ - Protocol.getName = function getProtocolName(protocolName) { + static getName(protocolName: string): string | undefined { return Protocol.getNamespace(protocolName) + '.NAME'; - }; - - return Protocol; + } -}]); \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/RelatedObjectPatch.ts similarity index 53% rename from guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/RelatedObjectPatch.ts index c06493816a..a5acc1626d 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/RelatedObjectPatch.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/RelatedObjectPatch.ts @@ -18,68 +18,63 @@ */ /** - * Service which defines the RelatedObjectPatch class. + * Returned by REST API calls when representing changes to an + * arbitrary set of objects which share some common relation. */ -angular.module('rest').factory('RelatedObjectPatch', [function defineRelatedObjectPatch() { - +export class RelatedObjectPatch { + /** - * The object returned by REST API calls when representing changes to an - * arbitrary set of objects which share some common relation. - * - * @constructor - * @param {RelatedObjectPatch|Object} [template={}] - * The object whose properties should be copied within the new - * RelatedObjectPatch. + * The operation to apply to the objects indicated by the path. Valid + * operation values are defined within RelatedObjectPatch.Operation. */ - var RelatedObjectPatch = function RelatedObjectPatch(template) { + op?: RelatedObjectPatch.Operation; - // Use empty object by default - template = template || {}; + /** + * The path of the objects to modify. This will always be "/". + * + * @default '/' + */ + path: string; - /** - * The operation to apply to the objects indicated by the path. Valid - * operation values are defined within RelatedObjectPatch.Operation. - * - * @type String - */ - this.op = template.op; + /** + * The identifier of the object being added or removed from the + * relation. + */ + value?: string; - /** - * The path of the objects to modify. This will always be "/". - * - * @type String - * @default '/' - */ + /** + * Creates a new RelatedObjectPatch. + * + * @param template + * The object whose properties should be copied within the new + * RelatedObjectPatch. + */ + constructor(template: Partial = {}) { + this.op = template.op; this.path = template.path || '/'; - - /** - * The identifier of the object being added or removed from the - * relation. - * - * @type String - */ this.value = template.value; + } - }; +} + +export namespace RelatedObjectPatch { /** * All valid patch operations for objects sharing some common relation. * Currently, only add and remove are supported. */ - RelatedObjectPatch.Operation = { + export enum Operation { /** * Adds the specified object to the relation. */ - ADD : "add", + ADD = 'add', /** * Removes the specified object from the relation. */ - REMOVE : "remove" - - }; + REMOVE = 'remove' - return RelatedObjectPatch; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/SharingProfile.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/SharingProfile.ts new file mode 100644 index 0000000000..4e70d98685 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/SharingProfile.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Returned by REST API calls when representing the data + * associated with a sharing profile. + */ +export class SharingProfile { + + /** + * The unique identifier associated with this sharing profile. + */ + identifier?: string; + + /** + * The unique identifier of the connection that this sharing profile + * can be used to share. + */ + primaryConnectionIdentifier?: string; + + /** + * The human-readable name of this sharing profile, which is not + * necessarily unique. + */ + name?: string; + + /** + * Connection configuration parameters, as dictated by the protocol in + * use by the primary connection, arranged as name/value pairs. This + * information may not be available until directly queried. If this + * information is unavailable, this property will be null or undefined. + */ + parameters?: Record; + + /** + * Arbitrary name/value pairs which further describe this sharing + * profile. The semantics and validity of these attributes are dictated + * by the extension which defines them. + */ + attributes: Record; + + /** + * Creates a new SharingProfile object. + * + * @param template + * The object whose properties should be copied within the new + * SharingProfile. + */ + constructor(template: Partial = {}) { + this.identifier = template.identifier || ''; + this.primaryConnectionIdentifier = template.primaryConnectionIdentifier; + this.name = template.name; + this.parameters = template.parameters; + this.attributes = template.attributes || {}; + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/TranslatableMessage.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/TranslatableMessage.ts new file mode 100644 index 0000000000..0d82273364 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/TranslatableMessage.ts @@ -0,0 +1,54 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Returned by REST API calls when representing a message which + * can be translated using the translation service, providing a translation + * key and optional set of values to be substituted into the translation + * string associated with that key. + */ +export class TranslatableMessage { + + /** + * The key associated with the translation string that used when + * displaying this message. + */ + key?: string; + + /** + * The object which should be passed through to the translation service + * for the sake of variable substitution. Each property of the provided + * object will be substituted for the variable of the same name within + * the translation string. + */ + variables?: object; + + /** + * Creates a new TranslatableMessage. + * + * @param template + * The object whose properties should be copied within the new + * TranslatableMessage. + */ + constructor(template: TranslatableMessage = {}) { + this.key = template.key; + this.variables = template.variables; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/User.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/User.ts new file mode 100644 index 0000000000..4cae9f9ada --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/User.ts @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Returned by REST API calls when representing the data + * associated with a user. + */ +export class User { + + /** + * The name which uniquely identifies this user. + */ + username: string; + + /** + * This user's password. Note that the REST API may not populate this + * property for the sake of security. In most cases, it's not even + * possible for the authentication layer to retrieve the user's true + * password. + */ + password: string; + + /** + * The time that this user was last logged in, in milliseconds since + * 1970-01-01 00:00:00 UTC. If this information is unknown or + * unavailable, this will be null. + */ + lastActive?: number; + + /** + * True if this user account is disabled, otherwise false. + */ + disabled?: boolean; + + /** + * Arbitrary name/value pairs which further describe this user. The + * semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @default {} + */ + attributes: Record; + + /** + * Creates a new User object. + * + * @param template + * The object whose properties should be copied within the new + * User. + */ + constructor(template: Partial = {}) { + this.username = template.username || ''; + this.password = template.password || ''; + this.lastActive = template.lastActive; + this.disabled = template.disabled; + this.attributes = template.attributes || {}; + } + +} + +export namespace User { + + /** + * All standard attribute names with semantics defined by the Guacamole web + * application. Extensions may additionally define their own attributes + * with completely arbitrary names and semantics, so long as those names do + * not conflict with the names listed here. All standard attribute names + * have a "guac-" prefix to avoid such conflicts. + */ + export enum Attributes { + + /** + * The user's full name. + */ + FULL_NAME = 'guac-full-name', + + /** + * The email address of the user. + */ + EMAIL_ADDRESS = 'guac-email-address', + + /** + * The organization, company, group, etc. that the user belongs to. + */ + ORGANIZATION = 'guac-organization', + + /** + * The role that the user has at the organization, company, group, etc. + * they belong to. + */ + ORGANIZATIONAL_ROLE = 'guac-organizational-role' + + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/UserCredentials.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserCredentials.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/rest/types/UserCredentials.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserCredentials.ts index dc6c75e3d3..de48c75e60 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/UserCredentials.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserCredentials.ts @@ -17,50 +17,39 @@ * under the License. */ +import { Field } from './Field'; + /** - * Service which defines the UserCredentials class. + * Returned by REST API calls to define a full set of valid + * credentials, including field definitions and corresponding expected + * values. */ -angular.module('rest').factory('UserCredentials', ['$injector', function defineUserCredentials($injector) { +export class UserCredentials { - // Required services - var $window = $injector.get('$window'); + /** + * Any parameters which should be provided when these credentials are + * submitted. If no such information is available, this will be undefined. + */ + expected: Field[]; - // Required types - var Field = $injector.get('Field'); + /** + * A map of all field values by field name. The fields having the names + * used within this map should be defined within the @link{Field} array + * stored under the @link{expected} property. + */ + values: Record; /** - * The object returned by REST API calls to define a full set of valid - * credentials, including field definitions and corresponding expected - * values. + * Creates a new UserCredentials object. * - * @constructor - * @param {UserCredentials|Object} [template={}] + * @param template * The object whose properties should be copied within the new * UserCredentials. */ - var UserCredentials = function UserCredentials(template) { - - // Use empty object by default - template = template || {}; - - /** - * Any parameters which should be provided when these credentials are - * submitted. If no such information is available, this will be null. - * - * @type Field[] - */ + constructor(template: UserCredentials) { this.expected = template.expected; - - /** - * A map of all field values by field name. The fields having the names - * used within this map should be defined within the @link{Field} array - * stored under the @link{expected} property. - * - * @type Object. - */ this.values = template.values; - - }; + } /** * Generates a query string containing all QUERY_PARAMETER fields from the @@ -68,27 +57,27 @@ angular.module('rest').factory('UserCredentials', ['$injector', function defineU * parameter names and values will be appropriately URL-encoded and * separated by ampersands. * - * @param {UserCredentials} userCredentials + * @param userCredentials * The UserCredentials to retrieve all query parameters from. * - * @returns {String} + * @returns * A string containing all QUERY_PARAMETER fields as name/value pairs * separated by ampersands, where each name is separated by the value * by an equals sign. */ - UserCredentials.getQueryParameters = function getQueryParameters(userCredentials) { + static getQueryParameters(userCredentials: UserCredentials): string { // Build list of parameter name/value pairs - var parameters = []; - angular.forEach(userCredentials.expected, function addQueryParameter(field) { + const parameters: string[] = []; + userCredentials.expected?.forEach(field => { // Only add query parameters if (field.type !== Field.Type.QUERY_PARAMETER) return; // Pull parameter name and value - var name = field.name; - var value = userCredentials.values[name]; + const name = field.name; + const value = userCredentials.values[name]; // Properly encode name/value pair parameters.push(encodeURIComponent(name) + '=' + encodeURIComponent(value)); @@ -98,42 +87,35 @@ angular.module('rest').factory('UserCredentials', ['$injector', function defineU // Separate each name/value pair by an ampersand return parameters.join('&'); - }; + } /** * Returns a fully-qualified, absolute URL to Guacamole prepopulated with * any query parameters dictated by the QUERY_PARAMETER fields defined in * the given UserCredentials. * - * @param {UserCredentials} userCredentials + * @param userCredentials * The UserCredentials to retrieve all query parameters from. * - * @returns {String} + * @returns * A fully-qualified, absolute URL to Guacamole prepopulated with the * query parameters dictated by the given UserCredentials. */ - UserCredentials.getLink = function getLink(userCredentials) { + static getLink(userCredentials: UserCredentials): string { - // Work-around for IE missing window.location.origin - if (!$window.location.origin) - var linkOrigin = $window.location.protocol + '//' + $window.location.hostname + ($window.location.port ? (':' + $window.location.port) : ''); - else - var linkOrigin = $window.location.origin; + const linkOrigin = window.location.origin; // Build base link - var link = linkOrigin - + $window.location.pathname - + '#/'; + let link = linkOrigin + + window.location.pathname + + '#/'; // Add any required parameters - var params = UserCredentials.getQueryParameters(userCredentials); + const params = UserCredentials.getQueryParameters(userCredentials); if (params) link += '?' + params; return link; - }; - - return UserCredentials; - -}]); + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/UserGroup.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserGroup.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/rest/types/UserGroup.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserGroup.ts index e3bc7eae23..6f20e488cf 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/UserGroup.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserGroup.ts @@ -18,49 +18,40 @@ */ /** - * Service which defines the UserGroup class. + * Returned by REST API calls when representing the data + * associated with a user group. */ -angular.module('rest').factory('UserGroup', [function defineUserGroup() { +export class UserGroup { /** - * The object returned by REST API calls when representing the data - * associated with a user group. + * The name which uniquely identifies this user group. + */ + identifier?: string; + + /** + * True if this user group is disabled, otherwise false. + */ + disabled?: boolean; + + /** + * Arbitrary name/value pairs which further describe this user group. + * The semantics and validity of these attributes are dictated by the + * extension which defines them. + * + * @default {} + */ + attributes: Record; + + /** + * Creates a new UserGroup object. * - * @constructor - * @param {UserGroup|Object} [template={}] + * @param template * The object whose properties should be copied within the new * UserGroup. */ - var UserGroup = function UserGroup(template) { - - // Use empty object by default - template = template || {}; - - /** - * The name which uniquely identifies this user group. - * - * @type String - */ + constructor(template: Partial = {}) { this.identifier = template.identifier; - - /** - * True if this user group is disabled, otherwise false. - * - * @type boolean - */ this.disabled = template.disabled; - - /** - * Arbitrary name/value pairs which further describe this user group. - * The semantics and validity of these attributes are dictated by the - * extension which defines them. - * - * @type Object. - */ this.attributes = template.attributes || {}; - - }; - - return UserGroup; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/frontend/src/app/rest/types/UserPasswordUpdate.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserPasswordUpdate.ts similarity index 54% rename from guacamole/src/main/frontend/src/app/rest/types/UserPasswordUpdate.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserPasswordUpdate.ts index e76e761989..cd7f62fd20 100644 --- a/guacamole/src/main/frontend/src/app/rest/types/UserPasswordUpdate.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/rest/types/UserPasswordUpdate.ts @@ -18,41 +18,31 @@ */ /** - * Service which defines the UserPasswordUpdate class. + * The object sent to the REST API when representing the data + * associated with a user password update. */ -angular.module('rest').factory('UserPasswordUpdate', [function defineUserPasswordUpdate() { - +export class UserPasswordUpdate { + /** - * The object sent to the REST API when representing the data - * associated with a user password update. - * - * @constructor - * @param {UserPasswordUpdate|Object} [template={}] - * The object whose properties should be copied within the new - * UserPasswordUpdate. + * This user's current password. Required for authenticating the user + * as part of to the password update operation. */ - var UserPasswordUpdate = function UserPasswordUpdate(template) { + oldPassword?: string; - // Use empty object by default - template = template || {}; + /** + * The new password to set for the user. + */ + newPassword?: string; - /** - * This user's current password. Required for authenticating the user - * as part of to the password update operation. - * - * @type String - */ + /** + * Creates a new UserPasswordUpdate object. + * + * @param template + * The object whose properties should be copied within the new + * UserPasswordUpdate. + */ + constructor(template: Partial = {}) { this.oldPassword = template.oldPassword; - - /** - * The new password to set for the user. - * - * @type String - */ this.newPassword = template.newPassword; - - }; - - return UserPasswordUpdate; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.html new file mode 100644 index 0000000000..9edb42790a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.html @@ -0,0 +1,29 @@ + + + + + +
      + + + {{ item.name }} + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.ts new file mode 100644 index 0000000000..b6fcd0b050 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-group/connection-group.component.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a single connection group within the + * list of accessible connections and groups. + */ +@Component({ + selector: 'guac-connection-group', + templateUrl: './connection-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionGroupComponent { + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.html new file mode 100644 index 0000000000..4ed99d422c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.html @@ -0,0 +1,25 @@ + + + + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.ts new file mode 100644 index 0000000000..98963a47f9 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection-history-player/connection-history-player.component.ts @@ -0,0 +1,72 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; + +/** + * The component for the session recording player page. + */ +@Component({ + selector: 'guac-connection-history-player', + templateUrl: './connection-history-player.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionHistoryPlayerComponent { + + /** + * The data source of the session recording to play. + */ + @Input({ required: true }) dataSource!: string; + + /** + * The identifier of the connection associated with the session + * recording to play. + */ + @Input({ required: true }) identifier!: string; + + /** + * The name of the session recording to play. + */ + @Input({ required: true }) name!: string; + + /** + * The URL of the REST API resource exposing the requested session + * recording. + */ + private readonly recordingURL = 'api/session/data/' + encodeURIComponent(this.dataSource) + + '/history/connections/' + encodeURIComponent(this.identifier) + + '/logs/' + encodeURIComponent(this.name); + + /** + * The tunnel which should be used to download the Guacamole session + * recording. + */ + readonly tunnel: Guacamole.Tunnel = new Guacamole.StaticHTTPTunnel(this.recordingURL, false, { + 'Guacamole-Token': this.authenticationService.getCurrentToken()! + }); + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService) { + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.html new file mode 100644 index 0000000000..00a1bd8b77 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.html @@ -0,0 +1,35 @@ + + + + + +
      + + + {{ item.name }} + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.ts new file mode 100644 index 0000000000..0abd01870f --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/connection/connection.component.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a single connection entry within the + * list of accessible connections and groups. + */ +@Component({ + selector: 'guac-connection', + templateUrl: './connection.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class ConnectionComponent { + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.html new file mode 100644 index 0000000000..9c29b8e40a --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.html @@ -0,0 +1,106 @@ + + +
      + + +

      {{ 'SETTINGS_CONNECTION_HISTORY.HELP_CONNECTION_HISTORY' | transloco }}

      + + + + + + + + + +
      + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
      + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME' | transloco }} + + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE' | transloco }} + + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION' | transloco }} + + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME' | transloco }} + + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST' | transloco }} + + {{ 'SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_LOGS' | transloco }} +
      + + {{ historyEntryWrapper.entry.startDate || 0 | date : dateFormat || undefined }}{{ historyEntryWrapper.entry.connectionName }}{{ historyEntryWrapper.entry.remoteHost }} + + {{ 'SETTINGS_CONNECTION_HISTORY.ACTION_VIEW_RECORDING' | transloco }} + +
      + + +

      + {{ 'SETTINGS_CONNECTION_HISTORY.INFO_NO_HISTORY' | transloco }} +

      + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.ts new file mode 100644 index 0000000000..be633e67c4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connection-history/guac-settings-connection-history.component.ts @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Component, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import FileSaver from 'file-saver'; +import { BehaviorSubject, combineLatest, take } from 'rxjs'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { SortService } from '../../../list/services/sort.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { FilterToken } from '../../../list/types/FilterToken'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { HistoryService } from '../../../rest/service/history.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { NonNullableProperties } from '../../../util/utility-types'; +import { CsvService } from '../../services/csv.service'; +import { ConnectionHistoryEntryWrapper } from '../../types/ConnectionHistoryEntryWrapper'; + +/** + * A component for viewing connection history records. + */ +@Component({ + selector: 'guac-settings-connection-history', + templateUrl: './guac-settings-connection-history.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsConnectionHistoryComponent implements OnInit { + + /** + * The identifier of the currently-selected data source. + */ + @Input({ required: true }) dataSource!: string; + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * All wrapped matching connection history entries, or null if these + * entries have not yet been retrieved. + */ + historyEntryWrappers: ConnectionHistoryEntryWrapper[] | null = null; + + /** + * The initial SortOrder instance which stores the sort order of the history + * records. + */ + private readonly initialOrder = new SortOrder([ + '-entry.startDate', + '-duration', + 'entry.username', + 'entry.connectionName', + 'entry.remoteHost' + ]); + + /** + * Observable of the current SortOrder instance which stores the sort order of the history + * records. The value is updated by the GuacSortOrderDirective. + */ + order: BehaviorSubject = new BehaviorSubject(this.initialOrder); + + /** + * The search terms to use when filtering the history records. + */ + searchString = ''; + + /** + * TODO: Document + */ + dataSourceView: DataSource | null = null; + + /** + * The date format for use for start/end dates. + */ + dateFormat: string | null = null; + + /** + * The names of sortable properties supported by the REST API that + * correspond to the properties that may be stored within + * $scope.order. + */ + private readonly apiSortProperties: Record = { + 'entry.startDate' : 'startDate', + '-entry.startDate': '-startDate' + }; + + /** + * Inject required services. + */ + constructor(private csvService: CsvService, + private historyService: HistoryService, + private requestService: RequestService, + private translocoService: TranslocoService, + private sortService: SortService, + private ds: DataSourceBuilderService) { + } + + ngOnInit(): void { + + // Get session date format + this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.FORMAT_DATE') + .pipe(take(1)) + .subscribe(retrievedDateFormat => { + + // Store received date format + this.dateFormat = retrievedDateFormat; + + }); + + // Build a view on the history entries + this.dataSourceView = + this.ds.getBuilder() + // Start with an empty list + .source([]) + .sort(this.order) + .paginate(this.pager.page) + .build(); + + // Initialize search results + this.search(); + + } + + /** + * Converts the given sort predicate to a corresponding array of + * sortable properties supported by the REST API. Any properties + * within the predicate that are not supported will be dropped. + * + * @param predicate + * The sort predicate to convert, as exposed by the predicate + * property of SortOrder. + * + * @returns + * A corresponding array of sortable properties, omitting any + * properties not supported by the REST API. + */ + private toAPISortPredicate(predicate: string[]): string[] { + return predicate + .map((name) => this.apiSortProperties[name]) + .filter((name) => !!name); + } + + /** + * Returns true if the connection history records have been loaded, + * indicating that information needed to render the page is fully + * loaded. + * + * @returns + * true if the history records have been loaded, false + * otherwise. + * + */ + isLoaded(): this is NonNullableProperties { + return this.historyEntryWrappers !== null + && this.dateFormat !== null + && this.dataSourceView !== null; + } + + /** + * Returns whether the search has completed but contains no history + * records. This function will return false if there are history + * records in the results OR if the search has not yet completed. + * + * @returns + * true if the search results have been loaded but no history + * records are present, false otherwise. + */ + isHistoryEmpty(): boolean { + return this.isLoaded() && this.historyEntryWrappers.length === 0; + } + + /** + * Query the API for the connection record history, filtered by + * searchString, and ordered by order. + */ + search(): void { + + // Clear current results + this.historyEntryWrappers = null; + + // Tokenize search string + const tokens = FilterToken.tokenize(this.searchString); + + // Transform tokens into list of required string contents + const requiredContents: any[] = []; + + tokens.forEach(token => { + + // Transform depending on token type + switch (token.type) { + + // For string literals, use parsed token value + case 'LITERAL': + requiredContents.push(token.value); + break; + + // Ignore whitespace + case 'WHITESPACE': + break; + + // For all other token types, use the relevant portion + // of the original search string + default: + requiredContents.push(token.consumed); + + } + + }); + + this.historyService.getConnectionHistory( + this.dataSource, + requiredContents, + this.toAPISortPredicate(this.order.getValue().predicate) + ) + .subscribe({ + next : historyEntries => { + + // Wrap all history entries for sake of display + this.historyEntryWrappers = []; + historyEntries.forEach(historyEntry => { + this.historyEntryWrappers!.push(new ConnectionHistoryEntryWrapper(this.dataSource, historyEntry)); + }); + + if (this.dataSourceView) { + this.dataSourceView.updateSource(this.historyEntryWrappers); + } + + }, error: this.requestService.DIE + + }); + + } + + /** + * Initiates a download of a CSV version of the displayed history + * search results. + */ + downloadCSV(): void { + + // Translate CSV header + const tableHeaderSessionUsername = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_USERNAME'); + const tableHeaderSessionStartdate = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_STARTDATE'); + const tableHeaderSessionDuration = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_DURATION'); + const tableHeaderSessionConnectionName = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_CONNECTION_NAME'); + const tableHeaderSessionRemotehost = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.TABLE_HEADER_SESSION_REMOTEHOST'); + const filenameHistoryCsv = this.translocoService.selectTranslate('SETTINGS_CONNECTION_HISTORY.FILENAME_HISTORY_CSV'); + + combineLatest([tableHeaderSessionUsername, tableHeaderSessionStartdate, tableHeaderSessionDuration, tableHeaderSessionConnectionName, tableHeaderSessionRemotehost, filenameHistoryCsv]) + .pipe(take(1)) + .subscribe(([tableHeaderSessionUsername, tableHeaderSessionStartdate, tableHeaderSessionDuration, tableHeaderSessionConnectionName, tableHeaderSessionRemotehost, filenameHistoryCsv]) => { + + // Initialize records with translated header row + const records: any[][] = [[ + tableHeaderSessionUsername, + tableHeaderSessionStartdate, + tableHeaderSessionDuration, + tableHeaderSessionConnectionName, + tableHeaderSessionRemotehost, + ]]; + + // Add rows for all history entries, using the same sort + // order as the displayed table + this.sortService.orderByPredicate(this.historyEntryWrappers, this.order.getValue().predicate).forEach(historyEntryWrapper => { + records.push([ + historyEntryWrapper.entry.username, + formatDate(historyEntryWrapper.entry.startDate || 0, this.dateFormat!, 'en-US'), + historyEntryWrapper.duration / 1000, + historyEntryWrapper.entry.connectionName, + historyEntryWrapper.entry.remoteHost + ]); + } + ); + + // Save the result + FileSaver.saveAs(this.csvService.toBlob(records), filenameHistoryCsv); + + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.html new file mode 100644 index 0000000000..5e3beade2b --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.html @@ -0,0 +1,96 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.ts new file mode 100644 index 0000000000..899845951e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-connections/guac-settings-connections.component.ts @@ -0,0 +1,462 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Router } from '@angular/router'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { + GuacGroupListFilterComponent +} from '../../../group-list/components/guac-group-list-filter/guac-group-list-filter.component'; +import { ConnectionGroupDataSource } from '../../../group-list/types/ConnectionGroupDataSource'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; +import { FilterService } from '../../../list/services/filter.service'; +import { GuacNotificationService } from '../../../notification/services/guac-notification.service'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { NonNullableProperties } from '../../../util/utility-types'; + +/** + * A component for managing all connections and connection groups in the system. + */ +@Component({ + selector: 'guac-settings-connections', + templateUrl: './guac-settings-connections.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsConnectionsComponent implements OnInit { + + /** + * The identifier of the current user. + */ + private currentUsername: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The identifier of the currently-selected data source. + */ + @Input({ required: true }) dataSource!: string; + + /** + * The root connection group of the connection group hierarchy. + */ + rootGroups: Record | null = null; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacGroupListFilterComponent, { static: true }) filter!: GuacGroupListFilterComponent; + + /** + * TODO + */ + rootGroupsDataSource: ConnectionGroupDataSource | null = null; + + /** + * All permissions associated with the current user, or null if the + * user's permissions have not yet been loaded. + */ + permissions: PermissionSet | null = null; + + /** + * Array of all connection properties that are filterable. + */ + filteredConnectionProperties: string[] = [ + 'name', + 'protocol' + ]; + + /** + * Array of all connection group properties that are filterable. + */ + filteredConnectionGroupProperties: string[] = [ + 'name' + ]; + + constructor(private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private dataSourceService: DataSourceService, + private guacNotification: GuacNotificationService, + private permissionService: PermissionService, + private requestService: RequestService, + private filterService: FilterService, + private router: Router) { + } + + ngOnInit(): void { + + // Create a new data source for the root connection groups + this.rootGroupsDataSource = new ConnectionGroupDataSource(this.filterService, + {}, // Start without data + this.filter.searchStringChange, + this.filteredConnectionProperties, + this.filteredConnectionGroupProperties); + + // Retrieve current permissions + this.permissionService.getEffectivePermissions(this.dataSource, this.currentUsername!) + .subscribe({ + next : permissions => { + + // Store retrieved permissions + this.permissions = permissions; + + // Ignore permission to update root group + PermissionSet.removeConnectionGroupPermission(this.permissions, PermissionSet.ObjectPermissionType.UPDATE, ConnectionGroup.ROOT_IDENTIFIER); + + // Return to home if there's nothing to do here + if (!this.canManageConnections()) + this.router.navigate(['/']); + + // Retrieve all connections for which we have UPDATE or DELETE permission + this.dataSourceService.apply( + (dataSource: string, connectionGroupID?: string, permissionTypes?: string[] | undefined) => + this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID, permissionTypes), + [this.dataSource], + ConnectionGroup.ROOT_IDENTIFIER, + [PermissionSet.ObjectPermissionType.UPDATE, PermissionSet.ObjectPermissionType.DELETE] + ) + .then(rootGroups => { + this.rootGroups = rootGroups; + this.rootGroupsDataSource?.updateSource(this.rootGroups); + }, this.requestService.PROMISE_DIE); + + }, error: this.requestService.DIE + }); // end retrieve permissions + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface + * to be useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + + return this.rootGroups !== null + && this.permissions !== null + && this.rootGroupsDataSource !== null; + + } + + /** + * Returns whether the current user has the ADMINISTER system + * permission (i.e. they are an administrator). + * + * @return + * true if the current user is an administrator. + */ + canAdminister(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Return whether the current user is an administrator + return PermissionSet.hasSystemPermission( + this.permissions, PermissionSet.SystemPermissionType.ADMINISTER); + } + + /** + * Returns whether the current user can create new connections + * within the current data source. + * + * @return + * true if the current user can create new connections within + * the current data source, false otherwise. + */ + canCreateConnections(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Can create connections if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION)) + return true; + + // No data sources allow connection creation + return false; + + } + + /** + * Returns whether the current user can create new connection + * groups within the current data source. + * + * @return + * true if the current user can create new connection groups + * within the current data source, false otherwise. + */ + canCreateConnectionGroups(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Can create connections groups if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.CREATE_CONNECTION_GROUP)) + return true; + + // No data sources allow connection group creation + return false; + + } + + /** + * Returns whether the current user can create new sharing profiles + * within the current data source. + * + * @return + * true if the current user can create new sharing profiles + * within the current data source, false otherwise. + */ + canCreateSharingProfiles(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Can create sharing profiles if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.CREATE_SHARING_PROFILE)) + return true; + + // Current data source does not allow sharing profile creation + return false; + + } + + /** + * Returns whether the current user can create new connections or + * connection groups or make changes to existing connections or + * connection groups within the current data source. The + * connection management interface as a whole is useless if this + * function returns false. + * + * @return + * true if the current user can create new connections/groups + * or make changes to existing connections/groups within the + * current data source, false otherwise. + */ + canManageConnections(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Creating connections/groups counts as management + if (this.canCreateConnections() + || this.canCreateConnectionGroups() + || this.canCreateSharingProfiles()) + return true; + + // Can manage connections if granted explicit update or delete + if (PermissionSet.hasConnectionPermission(this.permissions, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionPermission(this.permissions, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + // Can manage connections groups if granted explicit update or delete + if (PermissionSet.hasConnectionGroupPermission(this.permissions, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasConnectionGroupPermission(this.permissions, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + // No data sources allow management of connections or groups + return false; + + } + + /** + * Returns whether the current user can update the connection having + * the given identifier within the current data source. + * + * @param identifier + * The identifier of the connection to check. + * + * @return + * true if the current user can update the connection having the + * given identifier within the current data source, false + * otherwise. + */ + canUpdateConnection(identifier: string): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Can update the connection if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasConnectionPermission(this.permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier)) + return true; + + // Current data sources does not allow the connection to be updated + return false; + + } + + /** + * Returns whether the current user can update the connection group + * having the given identifier within the current data source. + * + * @param identifier + * The identifier of the connection group to check. + * + * @return + * true if the current user can update the connection group + * having the given identifier within the current data source, + * false otherwise. + */ + canUpdateConnectionGroup(identifier: string): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Can update the connection if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(this.permissions, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasConnectionGroupPermission(this.permissions, PermissionSet.ObjectPermissionType.UPDATE, identifier)) + return true; + + // Current data sources does not allow the connection group to be updated + return false; + + } + + /** + * Adds connection-group-specific contextual actions to the given + * array of GroupListItems. Each contextual action will be + * represented by a new GroupListItem. + * + * @param items + * The array of GroupListItems to which new GroupListItems + * representing connection-group-specific contextual actions + * should be added. + * + * @param parent + * The GroupListItem representing the connection group which + * contains the given array of GroupListItems, if known. + */ + private addConnectionGroupActions(items: GroupListItem[], parent?: GroupListItem): void { + + // Do nothing if we lack permission to modify the parent at all + if (parent && !this.canUpdateConnectionGroup(parent.identifier!)) + return; + + // Add action for creating a child connection, if the user has + // permission to do so + if (this.canCreateConnections()) + items.push(new GroupListItem({ + type : 'new-connection', + dataSource : this.dataSource, + weight : 1, + wrappedItem: parent + })); + + // Add action for creating a child connection group, if the user + // has permission to do so + if (this.canCreateConnectionGroups()) + items.push(new GroupListItem({ + type : 'new-connection-group', + dataSource : this.dataSource, + weight : 1, + wrappedItem: parent + })); + + } + + /** + * Adds connection-specific contextual actions to the given array of + * GroupListItems. Each contextual action will be represented by a + * new GroupListItem. + * + * @param items + * The array of GroupListItems to which new GroupListItems + * representing connection-specific contextual actions should + * be added. + * + * @param parent + * The GroupListItem representing the connection which contains + * the given array of GroupListItems, if known. + */ + private addConnectionActions(items: GroupListItem[], parent?: GroupListItem): void { + + // Do nothing if we lack permission to modify the parent at all + if (parent && !this.canUpdateConnection(parent.identifier!)) + return; + + // Add action for creating a child sharing profile, if the user + // has permission to do so + if (this.canCreateSharingProfiles()) + items.push(new GroupListItem({ + type : 'new-sharing-profile', + dataSource : this.dataSource, + weight : 1, + wrappedItem: parent + })); + + } + + /** + * Decorates the given GroupListItem, including all descendants, + * adding contextual actions. + * + * @param item + * The GroupListItem which should be decorated with additional + * GroupListItems representing contextual actions. + */ + private decorateItem(item: GroupListItem): void { + + // If the item is a connection group, add actions specific to + // connection groups + if (item.type === GroupListItem.Type.CONNECTION_GROUP) + this.addConnectionGroupActions(item.children, item); + + // If the item is a connection, add actions specific to + // connections + else if (item.type === GroupListItem.Type.CONNECTION) { + this.addConnectionActions(item.children, item); + } + + // Decorate all children + item.children.forEach(child => this.decorateItem(child)); + + } + + /** + * Callback which decorates all items within the given array of + * GroupListItems, including their descendants, adding contextual + * actions. + * + * @param items + * The array of GroupListItems which should be decorated with + * additional GroupListItems representing contextual actions. + */ + rootItemDecorator(items: GroupListItem[]): void { + + // Decorate each root-level item + items.forEach(item => this.decorateItem(item)); + + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.html new file mode 100644 index 0000000000..d311c30f32 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.html @@ -0,0 +1,162 @@ + + +
      + + +
      +

      {{ 'SETTINGS_PREFERENCES.HELP_LOCALE' | transloco }}

      + +
      + + +

      {{ 'SETTINGS_PREFERENCES.SECTION_HEADER_APPEARANCE' | transloco }}

      +
      +

      {{ 'SETTINGS_PREFERENCES.HELP_APPEARANCE' | transloco }}

      +
      + + + + + + + + + +
      {{ 'SETTINGS_PREFERENCES.FIELD_HEADER_SHOW_RECENT_CONNECTIONS' | transloco }}
      {{ 'SETTINGS_PREFERENCES.FIELD_HEADER_NUMBER_RECENT_CONNECTIONS' | transloco }}
      +
      +
      + + +

      {{ 'SETTINGS_PREFERENCES.SECTION_HEADER_UPDATE_PASSWORD' | transloco }}

      +
      +

      {{ 'SETTINGS_PREFERENCES.HELP_UPDATE_PASSWORD' | transloco }}

      + + +
      + + + + + + + + + + + + + +
      {{ 'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_OLD' | transloco }}
      {{ 'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW' | transloco }}
      {{ 'SETTINGS_PREFERENCES.FIELD_HEADER_PASSWORD_NEW_AGAIN' | transloco }}
      +
      + + +
      + +
      +
      + + +

      {{ 'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_INPUT_METHOD' | transloco }}

      +
      +

      {{ 'SETTINGS_PREFERENCES.HELP_DEFAULT_INPUT_METHOD' | transloco }}

      +
      + + +
      + +

      +
      + + +
      + +

      +
      + + +
      + +

      +
      + +
      +
      + + +

      {{ 'SETTINGS_PREFERENCES.SECTION_HEADER_DEFAULT_MOUSE_MODE' | transloco }}

      +
      +

      {{ 'SETTINGS_PREFERENCES.HELP_DEFAULT_MOUSE_MODE' | transloco }}

      +
      + + +
      + +
      + +

      +
      +
      + + +
      + +
      + +

      +
      +
      + +
      +
      + + +
      + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.ts new file mode 100644 index 0000000000..bcd4287a74 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-preferences/guac-settings-preferences.component.ts @@ -0,0 +1,313 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, DestroyRef, OnInit, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormGroup } from '@angular/forms'; +import { TranslocoService } from '@ngneat/transloco'; +import { EMPTY, forkJoin } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { FormService } from '../../../form/service/form.service'; +import { GuacNotificationService } from '../../../notification/services/guac-notification.service'; +import { NotificationAction } from '../../../notification/types/NotificationAction'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { SchemaService } from '../../../rest/service/schema.service'; +import { UserService } from '../../../rest/service/user.service'; +import { Field } from '../../../rest/types/Field'; +import { Form } from '../../../rest/types/Form'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { User } from '../../../rest/types/User'; +import { PreferenceService } from '../../services/preference.service'; + +/** + * A component for managing preferences local to the current user. + */ +@Component({ + selector: 'guac-settings-preferences', + templateUrl: './guac-settings-preferences.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsPreferencesComponent implements OnInit { + + /** + * The user being modified. + */ + user: User | null = null; + + /** + * The username of the current user. + */ + private username: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The identifier of the data source which authenticated the + * current user. + */ + private dataSource: string | null = this.authenticationService.getDataSource(); + + /** + * All available user attributes. This is only the set of attribute + * definitions, organized as logical groupings of attributes, not attribute + * values. + */ + attributes: Form[] | null = null; + + /** + * The fields which should be displayed for choosing locale + * preferences. Each field name must be a property on + * $scope.preferences. + */ + localeFields: Field[] = [ + { type: Field.Type.LANGUAGE, name: 'language' }, + { type: Field.Type.TIMEZONE, name: 'timezone' } + ]; + + /** + * The new password for the user. + */ + newPassword: string | null = null; + + /** + * The password match for the user. The update password action will + * fail if $scope.newPassword !== $scope.passwordMatch. + */ + newPasswordMatch: string | null = null; + + /** + * The old password of the user. + */ + oldPassword: string | null = null; + + /** + * Whether the current user can edit themselves - i.e. update their + * password or change user preference attributes, or null if this + * is not yet known. + */ + canUpdateSelf: boolean | null = null; + + /** + * Form group for editing locale settings. + */ + preferencesFormGroup: FormGroup = new FormGroup({}); + + /** + * Form group for editing user attributes. + */ + userAttributesFormGroup: FormGroup = new FormGroup({}); + + /** + * An action to be provided along with the object sent to + * showStatus which closes the currently-shown status dialog. + */ + private readonly ACKNOWLEDGE_ACTION: NotificationAction = { + name: 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', + // Handle action + callback: () => { + this.guacNotification.showStatus(false); + } + }; + + /** + * An action which closes the current dialog, and refreshes + * the user data on dialog close. + */ + private readonly ACKNOWLEDGE_ACTION_RELOAD = { + name: 'SETTINGS_PREFERENCES.ACTION_ACKNOWLEDGE', + // Handle action + callback: () => { + this.userService.getUser(this.dataSource!, this.username!) + .subscribe(user => { + this.user = user; + this.guacNotification.showStatus(false); + }); + } + }; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private guacNotification: GuacNotificationService, + private permissionService: PermissionService, + protected preferenceService: PreferenceService, + private requestService: RequestService, + private schemaService: SchemaService, + private userService: UserService, + private formService: FormService, + private translocoService: TranslocoService, + private destroyRef: DestroyRef) { + } + + ngOnInit(): void { + + // Retrieve current permissions + this.permissionService.getEffectivePermissions(this.dataSource!, this.username!) + .subscribe({ + next : permissions => { + + // Add action for updating password or user preferences if permission is granted + this.canUpdateSelf = ( + + // If permission is explicitly granted + PermissionSet.hasUserPermission(permissions, + PermissionSet.ObjectPermissionType.UPDATE, this.username!) + + // Or if implicitly granted through being an administrator + || PermissionSet.hasSystemPermission(permissions, + PermissionSet.SystemPermissionType.ADMINISTER)); + + }, + error: this.requestService.createErrorCallback(error => { + this.canUpdateSelf = false; + return EMPTY; + }) + }); + + // Fetch the user record + const user = this.userService.getUser(this.dataSource!, this.username!); + + // Fetch all user preference attribute forms defined + const attributes = this.schemaService.getUserPreferenceAttributes(this.dataSource!); + + forkJoin([user, attributes]) + .subscribe(([user, attributes]) => { + // Store the fetched data + this.attributes = attributes; + this.user = user; + + // Create form group for editing user attributes + this.userAttributesFormGroup = this.formService.getFormGroup(attributes); + this.userAttributesFormGroup.patchValue(user.attributes); + this.userAttributesFormGroup.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + if (this.user) + this.user.attributes = value; + }); + }); + + // Create form group for editing locale settings + const localeForm = this.formService.asFormArray(this.localeFields); + this.preferencesFormGroup = this.formService.getFormGroup(localeForm); + this.preferencesFormGroup.patchValue(this.preferenceService.preferences); + this.preferencesFormGroup.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(value => { + //Automatically update applied translation when language preference is changed + if (this.preferenceService.preferences.language !== value.language) { + this.translocoService.setActiveLang(value.language); + } + + const newPreferences = { ...this.preferenceService.preferences, ...value }; + this.preferenceService.preferences = newPreferences; + }); + + } + + /** + * Update the current user's password to the password currently set within + * the password change dialog. + */ + updatePassword(): void { + + // Verify passwords match + if (this.newPasswordMatch !== this.newPassword) { + this.guacNotification.showStatus({ + className: 'error', + title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', + text : { + key: 'SETTINGS_PREFERENCES.ERROR_PASSWORD_MISMATCH' + }, + actions : [this.ACKNOWLEDGE_ACTION] + }); + return; + } + + // Verify that the new password is not blank + if (!this.newPassword) { + this.guacNotification.showStatus({ + className: 'error', + title : 'SETTINGS_PREFERENCES.DIALOG_HEADER_ERROR', + text : { + key: 'SETTINGS_PREFERENCES.ERROR_PASSWORD_BLANK' + }, + actions : [this.ACKNOWLEDGE_ACTION] + }); + return; + } + + // Save the user with the new password + this.userService.updateUserPassword(this.dataSource!, this.username!, this.oldPassword!, this.newPassword) + .subscribe({ + next : () => { + + // Clear the password fields + this.oldPassword = null; + this.newPassword = null; + this.newPasswordMatch = null; + + // Indicate that the password has been changed + this.guacNotification.showStatus({ + text : { + key: 'SETTINGS_PREFERENCES.INFO_PASSWORD_CHANGED' + }, + actions: [this.ACKNOWLEDGE_ACTION] + }); + }, + error: this.guacNotification.SHOW_REQUEST_ERROR + }); + + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface to be + * useful, false otherwise. + */ + isLoaded(): boolean { + + return this.canUpdateSelf !== null; + + } + + /** + * Saves the current user, displaying an acknowledgement message if + * saving was successful, or an error if the save failed. + */ + saveUser(): void { + this.userService.saveUser(this.dataSource!, this.user!) + .subscribe({ + next : () => this.guacNotification.showStatus({ + text: { + key: 'SETTINGS_PREFERENCES.INFO_PREFERENCE_ATTRIBUTES_CHANGED' + }, + + // Reload the user on successful save in case any attributes changed + actions: [this.ACKNOWLEDGE_ACTION_RELOAD] + }), + error: this.guacNotification.SHOW_REQUEST_ERROR + }); + } + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.html new file mode 100644 index 0000000000..f303586832 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.html @@ -0,0 +1,79 @@ + + +
      + +

      {{ 'SETTINGS_SESSIONS.HELP_SESSIONS' | transloco }}

      + + +
      + +
      + + + + + + + + + + + + + + + + + + + + + + + + +
      + {{ 'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_USERNAME' | transloco }} + + {{ 'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_STARTDATE' | transloco }} + + {{ 'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_REMOTEHOST' | transloco }} + + {{ 'SETTINGS_SESSIONS.TABLE_HEADER_SESSION_CONNECTION_NAME' | transloco }} +
      + + + + {{ wrapper.startDate }}{{ wrapper.activeConnection.remoteHost }}{{ wrapper.name }}
      + + +

      + {{ 'SETTINGS_SESSIONS.INFO_NO_SESSIONS' | transloco }} +

      + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.ts new file mode 100644 index 0000000000..4c5650b4aa --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-sessions/guac-settings-sessions.component.ts @@ -0,0 +1,461 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { formatDate } from '@angular/common'; +import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { TranslocoService } from '@ngneat/transloco'; +import { BehaviorSubject, forkJoin, Observable, take } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { GuacFilterComponent } from '../../../list/components/guac-filter/guac-filter.component'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { SortService } from '../../../list/services/sort.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { ClientIdentifierService } from '../../../navigation/service/client-identifier.service'; +import { ClientIdentifier } from '../../../navigation/types/ClientIdentifier'; +import { GuacNotificationService } from '../../../notification/services/guac-notification.service'; +import { NotificationAction } from '../../../notification/types/NotificationAction'; +import { ActiveConnectionService } from '../../../rest/service/active-connection.service'; +import { ConnectionGroupService } from '../../../rest/service/connection-group.service'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { ActiveConnection } from '../../../rest/types/ActiveConnection'; +import { Connection } from '../../../rest/types/Connection'; +import { ConnectionGroup } from '../../../rest/types/ConnectionGroup'; +import { NonNullableProperties } from '../../../util/utility-types'; +import { ActiveConnectionWrapper } from '../../types/ActiveConnectionWrapper'; + +/** + * A component for managing all active Guacamole sessions. + */ +@Component({ + selector: 'guac-guac-settings-sessions', + templateUrl: './guac-settings-sessions.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsSessionsComponent implements OnInit { + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacFilterComponent, { static: true }) filter!: GuacFilterComponent; + + /** + * The identifiers of all data sources accessible by the current + * user. + */ + private dataSources: string[] = this.authenticationService.getAvailableDataSources(); + + /** + * TODO: document + */ + dataSourceView: DataSource | null = null; + + /** + * The ActiveConnectionWrappers of all active sessions accessible + * by the current user, or null if the active sessions have not yet + * been loaded. + */ + wrappers: ActiveConnectionWrapper[] | null = null; + + /** + * Initial SortOrder instance which maintains the sort order of the visible + * connection wrappers. + */ + private readonly initialOrder = new SortOrder([ + 'activeConnection.username', + 'startDate', + 'activeConnection.remoteHost', + 'name' + ]); + + /** + * Observable of the current SortOrder instance which stores the sort order of the + * visible connection wrappers. The value is updated by the GuacSortOrderDirective. + */ + order: BehaviorSubject = new BehaviorSubject(this.initialOrder); + + /** + * Array of all wrapper properties that are filterable. + */ + filteredWrapperProperties: string[] = [ + 'activeConnection.username', + 'startDate', + 'activeConnection.remoteHost', + 'name' + ]; + + /** + * All active connections, if known, grouped by corresponding data + * source identifier, or null if active connections have not yet + * been loaded. + */ + private allActiveConnections: Record> | null = null; + + /** + * Map of all visible connections by data source identifier and + * object identifier, or null if visible connections have not yet + * been loaded. + */ + private allConnections: Record> | null = null; + + /** + * The date format for use for session-related dates. + */ + private sessionDateFormat: string | null = null; + + /** + * Map of all currently-selected active connection wrappers by + * data source and identifier. + */ + private allSelectedWrappers: Record> = {}; + + /** + * Inject required services. + */ + constructor(private activeConnectionService: ActiveConnectionService, + private authenticationService: AuthenticationService, + private connectionGroupService: ConnectionGroupService, + private dataSourceService: DataSourceService, + private guacNotification: GuacNotificationService, + private requestService: RequestService, + private translocoService: TranslocoService, + private sortService: SortService, + private clientIdentifierService: ClientIdentifierService, + private dataSourceBuilderService: DataSourceBuilderService) { + } + + ngOnInit(): void { + + // Build the data source for the connection list entries. + this.dataSourceView = this.dataSourceBuilderService.getBuilder() + .source([]) + .filter(this.filter.searchStringChange, this.filteredWrapperProperties) + .sort(this.order) + .paginate(this.pager.page) + .build(); + + // Retrieve all connections + this.dataSourceService.apply( + (dataSource: string, connectionGroupID: string) => this.connectionGroupService.getConnectionGroupTree(dataSource, connectionGroupID), + this.dataSources, + ConnectionGroup.ROOT_IDENTIFIER + ) + .then(rootGroups => { + + this.allConnections = {}; + + // Load connections from each received root group + for (const dataSource in rootGroups) { + const rootGroup = rootGroups[dataSource]; + this.allConnections[dataSource] = {}; + this.addDescendantConnections(dataSource, rootGroup); + } + + // Attempt to produce wrapped list of active connections + this.wrapAllActiveConnections(); + + }, this.requestService.PROMISE_DIE); + + // Query active sessions + this.dataSourceService.apply( + (dataSource: string) => this.activeConnectionService.getActiveConnections(dataSource), + this.dataSources + ) + .then(retrievedActiveConnections => { + + // Store received map of active connections + this.allActiveConnections = retrievedActiveConnections; + + // Attempt to produce wrapped list of active connections + this.wrapAllActiveConnections(); + + }, this.requestService.PROMISE_DIE); + + // Get session date format + this.translocoService.selectTranslate('SETTINGS_SESSIONS.FORMAT_STARTDATE') + .pipe(take(1)) + .subscribe(retrievedSessionDateFormat => { + + // Store received date format + this.sessionDateFormat = retrievedSessionDateFormat; + + // Attempt to produce wrapped list of active connections + this.wrapAllActiveConnections(); + + }); + + } + + + /** + * Adds the given connection to the internal set of visible + * connections. + * + * @param dataSource + * The identifier of the data source associated with the given + * connection. + * + * @param connection + * The connection to add to the internal set of visible + * connections. + */ + private addConnection(dataSource: string, connection: Connection): void { + + // Add given connection to set of visible connections + (this.allConnections!)[dataSource][connection.identifier!] = connection; + + } + + /** + * Adds all descendant connections of the given connection group to + * the internal set of connections. + * + * @param dataSource + * The identifier of the data source associated with the given + * connection group. + * + * @param connectionGroup + * The connection group whose descendant connections should be + * added to the internal set of connections. + */ + private addDescendantConnections(dataSource: string, connectionGroup: ConnectionGroup): void { + + // Add all child connections + connectionGroup.childConnections?.forEach(connection => { + this.addConnection(dataSource, connection); + }); + + // Add all child connection groups + connectionGroup.childConnectionGroups?.forEach(connectionGroup => { + this.addDescendantConnections(dataSource, connectionGroup); + }); + + } + + /** + * Wraps all loaded active connections, storing the resulting array + * within the scope. If required data has not yet finished loading, + * this function has no effect. + */ + private wrapAllActiveConnections(): void { + + // Abort if not all required data is available + if (!this.allActiveConnections || !this.allConnections || !this.sessionDateFormat) + return; + + // Wrap all active connections for sake of display + this.wrappers = []; + for (const dataSource in this.allActiveConnections) { + const activeConnections = this.allActiveConnections[dataSource]; + for (const identifier in activeConnections) { + const activeConnection = activeConnections[identifier]; + + // Retrieve corresponding connection + const connection = this.allConnections[dataSource][activeConnection.connectionIdentifier!]; + + // Add wrapper + if (activeConnection.username !== null) { + this.wrappers.push(new ActiveConnectionWrapper({ + dataSource : dataSource, + name : connection.name, + startDate : formatDate(activeConnection.startDate || 0, this.sessionDateFormat, 'en-US'), + activeConnection: activeConnection + })); + } + + } + + } + + this.dataSourceView?.updateSource(this.wrappers); + + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface + * to be useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + return this.wrappers !== null + && this.dataSourceView !== null; + } + + /** + * An action to be provided along with the object sent to + * showStatus which closes the currently-shown status dialog. + */ + private readonly CANCEL_ACTION: NotificationAction = { + name: 'SETTINGS_SESSIONS.ACTION_CANCEL', + // Handle action + callback: () => { + this.guacNotification.showStatus(false); + } + }; + + /** + * An action to be provided along with the object sent to + * showStatus which immediately deletes the currently selected + * sessions. + */ + private readonly DELETE_ACTION: NotificationAction = { + name : 'SETTINGS_SESSIONS.ACTION_DELETE', + className: 'danger', + // Handle action + callback: () => { + this.deleteAllSessionsImmediately(); + this.guacNotification.showStatus(false); + } + }; + + /** + * Immediately deletes the selected sessions, without prompting the + * user for confirmation. + */ + private deleteAllSessionsImmediately(): void { + + const deletionRequests: Observable[] = []; + + // Perform deletion for each relevant data source + for (const dataSource in this.allSelectedWrappers) { + const selectedWrappers = this.allSelectedWrappers[dataSource]; + + // Delete sessions, if any are selected + const identifiers = Object.keys(selectedWrappers); + if (identifiers.length) + deletionRequests.push(this.activeConnectionService.deleteActiveConnections(dataSource, identifiers)); + + } + + // Update interface + forkJoin(deletionRequests) + .subscribe({ + next : () => { + // Remove deleted connections from wrapper array and update view + this.wrappers = this.wrappers!.filter(wrapper => { + return !(wrapper.activeConnection.identifier! in (this.allSelectedWrappers[wrapper.dataSource] || {})); + }); + this.dataSourceView?.updateSource(this.wrappers); + + // Clear selection + this.allSelectedWrappers = {}; + }, + error: this.guacNotification.SHOW_REQUEST_ERROR + }); + + } + + /** + * Delete all selected sessions, prompting the user first to + * confirm that deletion is desired. + */ + deleteSessions(): void { + // Confirm deletion request + this.guacNotification.showStatus({ + 'title' : 'SETTINGS_SESSIONS.DIALOG_HEADER_CONFIRM_DELETE', + 'text' : { + 'key': 'SETTINGS_SESSIONS.TEXT_CONFIRM_DELETE' + }, + 'actions': [this.DELETE_ACTION, this.CANCEL_ACTION] + }); + } + + /** + * Returns the relative URL of the client page which accesses the + * given active connection. If the active connection is not + * connectable, null is returned. + * + * @param dataSource + * The unique identifier of the data source containing the + * active connection. + * + * @param activeConnection + * The active connection to determine the relative URL of. + * + * @returns + * The relative URL of the client page which accesses the given + * active connection, or null if the active connection is not + * connectable. + */ + getClientURL(dataSource: string, activeConnection: ActiveConnection): string | null { + + if (!activeConnection.connectable) + return null; + + return '/client/' + encodeURIComponent(this.clientIdentifierService.getString({ + dataSource: dataSource, + type : ClientIdentifier.Types.ACTIVE_CONNECTION, + id : activeConnection.identifier + })); + + } + + /** + * Returns whether the selected sessions can be deleted. + * + * @returns + * true if selected sessions can be deleted, false otherwise. + */ + canDeleteSessions(): boolean { + + // We can delete sessions if at least one is selected + for (const dataSource in this.allSelectedWrappers) { + for (const identifier in this.allSelectedWrappers[dataSource]) + return true; + } + + return false; + + } + + /** + * Called whenever an active connection wrapper changes selected + * status. + * + * @param wrapper + * The wrapper whose selected status has changed. + */ + wrapperSelectionChange(wrapper: ActiveConnectionWrapper): void { + + // Get selection map for associated data source, creating if necessary + let selectedWrappers = this.allSelectedWrappers[wrapper.dataSource]; + if (!selectedWrappers) + selectedWrappers = this.allSelectedWrappers[wrapper.dataSource] = {}; + + // Add wrapper to map if selected + if (wrapper.checked) + selectedWrappers[wrapper.activeConnection.identifier!] = wrapper; + + // Otherwise, remove wrapper from map + else + delete selectedWrappers[wrapper.activeConnection.identifier!]; + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.html new file mode 100644 index 0000000000..81cad93d2e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.html @@ -0,0 +1,64 @@ + + +
      + +

      {{ 'SETTINGS_USER_GROUPS.HELP_USER_GROUPS' | transloco }}

      + + + + + + + + + + + + + + + + +
      + {{ 'SETTINGS_USER_GROUPS.TABLE_HEADER_USER_GROUP_NAME' | transloco }} +
      + +
      + {{ manageableUserGroup.userGroup.identifier }} +
      +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.ts new file mode 100644 index 0000000000..ab64751615 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-user-groups/guac-settings-user-groups.component.ts @@ -0,0 +1,319 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Router } from '@angular/router'; +import _ from 'lodash'; +import { BehaviorSubject } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { GuacFilterComponent } from '../../../list/components/guac-filter/guac-filter.component'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { ManageableUserGroup } from '../../../manage/types/ManageableUserGroup'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { UserGroupService } from '../../../rest/service/user-group.service'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { UserGroup } from '../../../rest/types/UserGroup'; +import { NonNullableProperties } from '../../../util/utility-types'; + +/** + * A component for managing all user groups in the system. + */ +@Component({ + selector: 'guac-settings-user-groups', + templateUrl: './guac-settings-user-groups.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsUserGroupsComponent implements OnInit { + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacFilterComponent, { static: true }) filter!: GuacFilterComponent; + + /** + * Identifier of the current user. + */ + private currentUsername: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The identifiers of all data sources accessible by the current + * user. + */ + private dataSources: string[] = this.authenticationService.getAvailableDataSources(); + + /** + * TODO: document + */ + dataSourceView: DataSource | null = null; + + /** + * All visible user groups, along with their corresponding data + * sources. + */ + manageableUserGroups: ManageableUserGroup[] | null = null; + + /** + * Array of all user group properties that are filterable. + */ + filteredUserGroupProperties: string[] = [ + 'userGroup.identifier' + ]; + + /** + * The initial SortOrder which stores the sort order of the listed + * user groups. + */ + private readonly initialOrder: SortOrder = new SortOrder([ + 'userGroup.identifier' + ]); + + /** + * Observable of the current SortOrder instance which stores the sort order of the listed + * user groups. The value is updated by the GuacSortOrderDirective. + */ + order: BehaviorSubject = new BehaviorSubject(this.initialOrder); + + /** + * Map of data source identifiers to all permissions associated + * with the current user within that data source, or null if the + * user's permissions have not yet been loaded. + */ + private permissions: Record | null = null; + + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private dataSourceService: DataSourceService, + private permissionService: PermissionService, + private requestService: RequestService, + private userGroupService: UserGroupService, + private dataSourceBuilderService: DataSourceBuilderService, + private router: Router) { + } + + ngOnInit(): void { + + // Build the data source for the user group list entries. + this.dataSourceView = this.dataSourceBuilderService.getBuilder() + .source([]) + .filter(this.filter.searchStringChange, this.filteredUserGroupProperties) + .sort(this.order) + .paginate(this.pager.page) + .build(); + + // Retrieve current permissions + this.dataSourceService.apply( + (dataSource: string, userID: string) => this.permissionService.getEffectivePermissions(dataSource, userID), + this.dataSources, + this.currentUsername + ) + .then(retrievedPermissions => { + + // Store retrieved permissions + this.permissions = retrievedPermissions; + + // Return to home if there's nothing to do here + if (!this.canManageUserGroups()) + this.router.navigate(['/']); + + // If user groups can be created, list all readable user groups + if (this.canCreateUserGroups()) { + return this.dataSourceService.apply( + (dataSource: string) => this.userGroupService.getUserGroups(dataSource), + this.dataSources + ); + } + + // Otherwise, list only updateable/deletable users + return this.dataSourceService.apply( + (dataSource: string, permissionTypes: string[]) => this.userGroupService.getUserGroups(dataSource, permissionTypes), + this.dataSources, + [ + PermissionSet.ObjectPermissionType.UPDATE, + PermissionSet.ObjectPermissionType.DELETE + ] + ); + + }) + .then(userGroups => { + this.setDisplayedUserGroups(this.permissions!, userGroups); + }, this.requestService.PROMISE_WARN); + } + + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user group + * interface to be useful, false otherwise. + */ + isLoaded(): this is NonNullableProperties { + return this.manageableUserGroups !== null + && this.dataSourceView !== null; + } + + /** + * Returns the identifier of the data source that should be used by + * default when creating a new user group. + * + * @return + * The identifier of the data source that should be used by + * default when creating a new user group, or null if user group + * creation is not allowed. + */ + getDefaultDataSource(): string | null { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return null; + + // For each data source + const dataSources = _.keys(this.permissions).sort(); + for (let i = 0; i < dataSources.length; i++) { + + // Retrieve corresponding permission set + const dataSource = dataSources[i]; + const permissionSet = (this.permissions)[dataSource]; + + // Can create user groups if adminstrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER_GROUP)) + return dataSource; + + } + + // No data sources allow user group creation + return null; + + } + + /** + * Returns whether the current user can create new user groups + * within at least one data source. + * + * @return + * true if the current user can create new user groups within at + * least one data source, false otherwise. + */ + canCreateUserGroups(): boolean { + return this.getDefaultDataSource() !== null; + } + + /** + * Returns whether the current user can create new user groups or + * make changes to existing user groups within at least one data + * source. The user group management interface as a whole is useless + * if this function returns false. + * + * @return {Boolean} + * true if the current user can create new user groups or make + * changes to existing user groups within at least one data + * source, false otherwise. + */ + private canManageUserGroups(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Creating user groups counts as management + if (this.canCreateUserGroups()) + return true; + + // For each data source + for (const dataSource in this.permissions) { + + // Retrieve corresponding permission set + const permissionSet = (this.permissions)[dataSource]; + + // Can manage user groups if granted explicit update or delete + if (PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasUserGroupPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + } + + // No data sources allow management of user groups + return false; + + } + + /** + * Sets the displayed list of user groups. If any user groups are + * already shown within the interface, those user groups are replaced + * with the given user groups. + * + * @param permissions + * A map of data source identifiers to all permissions associated + * with the current user within that data source. + * + * @param userGroups + * A map of all user groups which should be displayed, where each + * key is the data source identifier from which the user groups + * were retrieved and each value is a map of user group identifiers + * to their corresponding @link{UserGroup} objects. + */ + setDisplayedUserGroups(permissions: Record, userGroups: Record>): void { + + const addedUserGroups: Record = {}; + this.manageableUserGroups = []; + + // For each user group in each data source + this.dataSources.forEach(dataSource => { + for (const userGroupIdentifier in userGroups[dataSource]) { + const userGroup = userGroups[dataSource][userGroupIdentifier]; + + // Do not add the same user group twice + if (addedUserGroups[userGroup.identifier!]) + return; + + // Link to default creation data source if we cannot manage this user + if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER) + && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, userGroup.identifier) + && !PermissionSet.hasUserGroupPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, userGroup.identifier)) + dataSource = this.getDefaultDataSource()!; + + // Add user group to overall list + addedUserGroups[userGroup.identifier!] = userGroup; + this.manageableUserGroups!.push(new ManageableUserGroup({ + 'dataSource': dataSource, + 'userGroup' : userGroup + })); + + } + + this.dataSourceView?.updateSource(this.manageableUserGroups!); + }); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.html new file mode 100644 index 0000000000..048d921370 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.html @@ -0,0 +1,88 @@ + + +
      + + +

      {{ 'SETTINGS_USERS.HELP_USERS' | transloco }}

      + + + + + + + + + + + + + + + + + + + + + + + +
      + {{ 'SETTINGS_USERS.TABLE_HEADER_USERNAME' | transloco }} + + {{ 'SETTINGS_USERS.TABLE_HEADER_ORGANIZATION' | transloco }} + + {{ 'SETTINGS_USERS.TABLE_HEADER_FULL_NAME' | transloco }} + + {{ 'SETTINGS_USERS.TABLE_HEADER_LAST_ACTIVE' | transloco }} +
      + +
      + {{ manageableUser.user.username }} +
      +
      {{ manageableUser.user.attributes['guac-organization'] }}{{ manageableUser.user.attributes['guac-full-name'] }}{{ + (manageableUser.user.lastActive !== undefined) + ? (manageableUser.user.lastActive | date : dateFormat || undefined) + : '' + }} +
      + + + + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.ts new file mode 100644 index 0000000000..c86728e967 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/guac-settings-users/guac-settings-users.component.ts @@ -0,0 +1,354 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, OnInit, ViewChild, ViewEncapsulation } from '@angular/core'; +import { Router } from '@angular/router'; +import { TranslocoService } from '@ngneat/transloco'; +import _ from 'lodash'; +import { BehaviorSubject, take } from 'rxjs'; +import { AuthenticationService } from '../../../auth/service/authentication.service'; +import { GuacFilterComponent } from '../../../list/components/guac-filter/guac-filter.component'; +import { GuacPagerComponent } from '../../../list/components/guac-pager/guac-pager.component'; +import { DataSourceBuilderService } from '../../../list/services/data-source-builder.service'; +import { SortService } from '../../../list/services/sort.service'; +import { DataSource } from '../../../list/types/DataSource'; +import { SortOrder } from '../../../list/types/SortOrder'; +import { ManageableUser } from '../../../manage/types/ManageableUser'; +import { DataSourceService } from '../../../rest/service/data-source-service.service'; +import { PermissionService } from '../../../rest/service/permission.service'; +import { RequestService } from '../../../rest/service/request.service'; +import { UserService } from '../../../rest/service/user.service'; +import { PermissionSet } from '../../../rest/types/PermissionSet'; +import { User } from '../../../rest/types/User'; + +/** + * A component for managing all users in the system. + */ +@Component({ + selector: 'guac-settings-users', + templateUrl: './guac-settings-users.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class GuacSettingsUsersComponent implements OnInit { + + /** + * Reference to the instance of the pager component. + */ + @ViewChild(GuacPagerComponent, { static: true }) pager!: GuacPagerComponent; + + /** + * Reference to the instance of the filter component. + */ + @ViewChild(GuacFilterComponent, { static: true }) filter!: GuacFilterComponent; + + /** + * Identifier of the current user + */ + private currentUsername: string | null = this.authenticationService.getCurrentUsername(); + + /** + * The identifiers of all data sources accessible by the current + * user. + */ + private readonly dataSources: string[] = this.authenticationService.getAvailableDataSources(); + + /** + * TODO: document + */ + dataSourceView: DataSource | null = null; + + /** + * All visible users, along with their corresponding data sources. + */ + manageableUsers: ManageableUser[] | null = null; + + /** + * The name of the new user to create, if any, when user creation + * is requested via newUser(). + */ + newUsername = ''; + + /** + * Map of data source identifiers to all permissions associated + * with the current user within that data source, or null if the + * user's permissions have not yet been loaded. + */ + permissions: Record | null = null; + + /** + * Array of all user properties that are filterable. + */ + readonly filteredUserProperties: string[] = [ + 'user.attributes["guac-full-name"]', + 'user.attributes["guac-organization"]', + 'user.lastActive', + 'user.username' + ]; + + /** + * The date format for use for the last active date. + */ + dateFormat: string | null = null; + + /** + * SortOrder instance which stores the sort order of the listed + * users. + */ + private readonly initialOrder: SortOrder = new SortOrder([ + 'user.username', + '-user.lastActive', + 'user.attributes["guac-organization"]', + 'user.attributes["guac-full-name"]' + ]); + + /** + * Observable of the current SortOrder instance which stores the sort order of the listed + * users. The value is updated by the GuacSortOrderDirective. + */ + order: BehaviorSubject = new BehaviorSubject(this.initialOrder); + + /** + * Inject required services and initialize fields. + */ + constructor(private authenticationService: AuthenticationService, + private dataSourceService: DataSourceService, + private permissionService: PermissionService, + private requestService: RequestService, + private userService: UserService, + private translocoService: TranslocoService, + private sortService: SortService, + private dataSourceBuilderService: DataSourceBuilderService, + private router: Router) { + + + // Get session date format + this.translocoService.selectTranslate('SETTINGS_USERS.FORMAT_DATE') + .pipe(take(1)) + .subscribe((retrievedDateFormat: string) => { + // Store received date format + this.dateFormat = retrievedDateFormat; + }); + + } + + ngOnInit(): void { + + // Build the data source for the users list entries. + this.dataSourceView = this.dataSourceBuilderService.getBuilder() + .source([]) + .filter(this.filter.searchStringChange, this.filteredUserProperties) + .sort(this.order) + .paginate(this.pager.page) + .build(); + + // Retrieve current permissions + this.retrieveCurrentPermissions(); + } + + /** + * Retrieves the current permissions. + * @private + */ + private retrieveCurrentPermissions(): void { + + this.dataSourceService.apply( + (ds: string, username: string) => this.permissionService.getEffectivePermissions(ds, username), + this.dataSources, + this.currentUsername + ) + .then(permissions => { + + // Store retrieved permissions + this.permissions = permissions; + + // Return to home if there's nothing to do here + if (!this.canManageUsers()) { + this.router.navigate(['/']); + return; + } + + let userPromise: Promise>>; + + // If users can be created, list all readable users + if (this.canCreateUsers()) + userPromise = this.dataSourceService.apply((ds: string) => this.userService.getUsers(ds), this.dataSources); + + // Otherwise, list only updatable/deletable users + else + userPromise = this.dataSourceService.apply(this.userService.getUsers, this.dataSources, [ + PermissionSet.ObjectPermissionType.UPDATE, + PermissionSet.ObjectPermissionType.DELETE + ]); + + userPromise.then((allUsers) => { + + const addedUsers: Record = {}; + const manageableUsers: ManageableUser[] = []; + + // For each user in each data source + this.dataSources.forEach(dataSource => { + for (const username in allUsers[dataSource]) { + const user = allUsers[dataSource][username]; + + // Do not add the same user twice + if (addedUsers[user.username]) + return; + + // Link to default creation data source if we cannot manage this user + if (!PermissionSet.hasSystemPermission(permissions[dataSource], PermissionSet.SystemPermissionType.ADMINISTER) + && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.UPDATE, user.username) + && !PermissionSet.hasUserPermission(permissions[dataSource], PermissionSet.ObjectPermissionType.DELETE, user.username)) + dataSource = this.getDefaultDataSource() || ''; + + // Add user to overall list + addedUsers[user.username] = user; + manageableUsers.push(new ManageableUser({ + dataSource: dataSource, + user : user + })); + + } + + }); + + this.manageableUsers = manageableUsers; + this.dataSourceView?.updateSource(this.manageableUsers); + + }, this.requestService.PROMISE_DIE); + + }, this.requestService.PROMISE_DIE); + } + + /** + * Returns whether critical data has completed being loaded. + * + * @returns + * true if enough data has been loaded for the user interface + * to be useful, false otherwise. + */ + isLoaded(): boolean { + + return this.dateFormat !== null + && this.manageableUsers !== null + && this.permissions !== null + && this.dataSourceView !== null; + + } + + /** + * Returns the identifier of the data source that should be used by + * default when creating a new user. + * + * @return + * The identifier of the data source that should be used by + * default when creating a new user, or null if user creation + * is not allowed. + */ + getDefaultDataSource(): string | null { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return null; + + // For each data source + const dataSources = _.keys(this.permissions).sort(); + for (let i = 0; i < dataSources.length; i++) { + + // Retrieve corresponding permission set + const dataSource = dataSources[i]; + const permissionSet = this.permissions[dataSource]; + + // Can create users if administrator or have explicit permission + if (PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.ADMINISTER) + || PermissionSet.hasSystemPermission(permissionSet, PermissionSet.SystemPermissionType.CREATE_USER)) + return dataSource; + + } + + // No data sources allow user creation + return null; + + } + + /** + * Returns whether the current user can create new users within at + * least one data source. + * + * @return + * true if the current user can create new users within at + * least one data source, false otherwise. + */ + canCreateUsers(): boolean { + return this.getDefaultDataSource() !== null; + } + + /** + * Returns whether the current user can create new users or make + * changes to existing users within at least one data source. The + * user management interface as a whole is useless if this function + * returns false. + * + * @return + * true if the current user can create new users or make + * changes to existing users within at least one data source, + * false otherwise. + */ + canManageUsers(): boolean { + + // Abort if permissions have not yet loaded + if (!this.permissions) + return false; + + // Creating users counts as management + if (this.canCreateUsers()) + return true; + + // For each data source + for (const dataSource in this.permissions) { + + // Retrieve corresponding permission set + const permissionSet = this.permissions[dataSource]; + + // Can manage users if granted explicit update or delete + if (PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.UPDATE) + || PermissionSet.hasUserPermission(permissionSet, PermissionSet.ObjectPermissionType.DELETE)) + return true; + + } + + // No data sources allow management of users + return false; + + } + + /** + * Track a user by their username. It is used in Angular's *ngFor + * directive to optimize performance. + * + * @param index The index of the current item in the iterable. + * @param manageableUser The current item being iterated. + * + * @returns The username of the manageable user. + */ + trackByUsername(index: number, manageableUser: ManageableUser): string { + return manageableUser.user.username; + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.html new file mode 100644 index 0000000000..7b59c78879 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.html @@ -0,0 +1,24 @@ + + + + {{ 'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION_GROUP' | transloco }} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.ts new file mode 100644 index 0000000000..6703bc2ab4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection-group/new-connection-group.component.ts @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a link to create a new connection group in a specific + * location within the list of accessible connections and groups. + */ +@Component({ + selector: 'guac-new-connection-group', + templateUrl: './new-connection-group.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class NewConnectionGroupComponent { + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.html new file mode 100644 index 0000000000..04f2987c58 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.html @@ -0,0 +1,24 @@ + + + + {{ 'SETTINGS_CONNECTIONS.ACTION_NEW_CONNECTION' | transloco }} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.ts new file mode 100644 index 0000000000..9c691579f4 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-connection/new-connection.component.ts @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a link to create a new connection in a specific + * location within the list of accessible connections and groups. + */ +@Component({ + selector: 'guac-new-connection', + templateUrl: './new-connection.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class NewConnectionComponent { + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.html new file mode 100644 index 0000000000..4caad913e6 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.html @@ -0,0 +1,24 @@ + + + + {{ 'SETTINGS_CONNECTIONS.ACTION_NEW_SHARING_PROFILE' | transloco }} + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.ts new file mode 100644 index 0000000000..a39f1eddd3 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/new-sharing-profile/new-sharing-profile.component.ts @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a link to create a new sharing profile for a specific + * connection within the list of accessible connections and groups. + */ +@Component({ + selector: 'guac-new-sharing-profile', + templateUrl: './new-sharing-profile.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class NewSharingProfileComponent { + @Input({ required: true }) item!: GroupListItem; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.html new file mode 100644 index 0000000000..e12c054597 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.html @@ -0,0 +1,35 @@ + + +
      + +
      +

      {{ 'SETTINGS.SECTION_HEADER_SETTINGS' | transloco }}

      + +
      + + +
      + +
      + + + + +
      diff --git a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.ts similarity index 52% rename from guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.ts index 9961d292fa..335de5c0c6 100644 --- a/guacamole/src/main/frontend/src/app/settings/controllers/settingsController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/settings/settings.component.ts @@ -17,36 +17,41 @@ * under the License. */ +import { Component, OnInit, ViewEncapsulation } from '@angular/core'; +import { UserPageService } from '../../../manage/services/user-page.service'; +import { PageDefinition } from '../../../navigation/types/PageDefinition'; + /** - * The controller for the general settings page. + * The component for the general settings page. */ -angular.module('settings').controller('settingsController', ['$scope', '$injector', - function settingsController($scope, $injector) { - - // Required services - var $routeParams = $injector.get('$routeParams'); - var userPageService = $injector.get('userPageService'); +@Component({ + selector: 'guac-settings', + templateUrl: './settings.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class SettingsComponent implements OnInit { /** * The array of settings pages available to the current user, or null if * not yet known. - * - * @type Page[] */ - $scope.settingsPages = null; + settingsPages: PageDefinition[] | null = null; /** - * The currently-selected settings tab. This may be 'users', 'userGroups', - * 'connections', 'history', 'preferences', or 'sessions'. - * - * @type String + * Inject required services. */ - $scope.activeTab = $routeParams.tab; + constructor(private userPageService: UserPageService) { + } - // Retrieve settings pages - userPageService.getSettingsPages() - .then(function settingsPagesRetrieved(pages) { - $scope.settingsPages = pages; - }); + /** + * Retrieve settings pages. + */ + ngOnInit(): void { + this.userPageService.getSettingsPages() + .subscribe(pages => { + this.settingsPages = pages; + }); + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.html new file mode 100644 index 0000000000..25bf157850 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.html @@ -0,0 +1,29 @@ + + + + + +
      + + + {{ item.name }} + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.ts new file mode 100644 index 0000000000..f8166422ce --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/components/sharing-profile/sharing-profile.component.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, Input, ViewEncapsulation } from '@angular/core'; +import { GroupListItem } from '../../../group-list/types/GroupListItem'; + +/** + * A component which displays a single sharing profile entry within the + * list of accessible connections and groups. + */ +@Component({ + selector: 'guac-sharing-profile', + templateUrl: './sharing-profile.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class SharingProfileComponent { + + /** + * TODO + */ + @Input({ required: true }) item!: GroupListItem; + +} diff --git a/guacamole/src/main/frontend/src/app/settings/services/csvService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/csv.service.ts similarity index 79% rename from guacamole/src/main/frontend/src/app/settings/services/csvService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/csv.service.ts index e68cf15e43..6dc66b2212 100644 --- a/guacamole/src/main/frontend/src/app/settings/services/csvService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/csv.service.ts @@ -17,12 +17,60 @@ * under the License. */ +import { Injectable } from '@angular/core'; + /** * A service for generating downloadable CSV links given arbitrary data. */ -angular.module('settings').factory('csvService', [function csvService() { +@Injectable({ + providedIn: 'root' +}) +export class CsvService { - var service = {}; + /** + * Creates a new Blob containing properly-formatted CSV generated from the + * given array of records, where each entry in the provided array is an + * array of arbitrary fields. + * + * @param records + * An array of all records making up the desired CSV. + * + * @returns + * A new Blob containing each provided record in CSV format. + */ + toBlob(records: any[][]): Blob { + return new Blob([this.encodeCSV(records)], { type: 'text/csv' }); + } + + /** + * Encodes an entire array of records as properly-formatted CSV, where each + * entry in the provided array is an array of arbitrary fields. + * + * @param records + * An array of all records making up the desired CSV. + * + * @return + * An entire CSV containing each provided record, separated by CR+LF + * line terminators. + */ + encodeCSV(records: any[][]): string { + return records.map(record => this.encodeRecord(record)).join('\r\n'); + } + + /** + * Encodes each of the provided values for inclusion in a CSV file as + * fields within the same record (in the manner specified by + * encodeField()), separated by commas. + * + * @param fields + * An array of arbitrary values which make up the record. + * + * @return + * A CSV record containing the each value in the given array. + */ + private encodeRecord(fields: any[]): string { + return fields.map(field => this.encodeField(field)).join(','); + } /** * Encodes an arbitrary value for inclusion in a CSV file as an individual @@ -32,14 +80,14 @@ angular.module('settings').factory('csvService', [function csvService() { * value itself includes double quotes, those quotes will be properly * escaped. * - * @param {*} field + * @param field * The arbitrary value to encode. * - * @return {String} + * @return * The provided value, coerced to a string and properly escaped for * CSV. */ - var encodeField = function encodeField(field) { + private encodeField(field: any): string { // Coerce field to string if (field === null || field === undefined) @@ -54,53 +102,6 @@ angular.module('settings').factory('csvService', [function csvService() { // Enclose all other fields in quotes, escaping any quotes therein return '"' + field.replace(/"/g, '""') + '"'; - }; - - /** - * Encodes each of the provided values for inclusion in a CSV file as - * fields within the same record (in the manner specified by - * encodeField()), separated by commas. - * - * @param {*[]} fields - * An array of arbitrary values which make up the record. - * - * @return {String} - * A CSV record containing the each value in the given array. - */ - var encodeRecord = function encodeRecord(fields) { - return fields.map(encodeField).join(','); - }; - - /** - * Encodes an entire array of records as properly-formatted CSV, where each - * entry in the provided array is an array of arbitrary fields. - * - * @param {Array.<*[]>} records - * An array of all records making up the desired CSV. - * - * @return {String} - * An entire CSV containing each provided record, separated by CR+LF - * line terminators. - */ - var encodeCSV = function encodeCSV(records) { - return records.map(encodeRecord).join('\r\n'); - }; - - /** - * Creates a new Blob containing properly-formatted CSV generated from the - * given array of records, where each entry in the provided array is an - * array of arbitrary fields. - * - * @param {Array.<*[]>} records - * An array of all records making up the desired CSV. - * - * @returns {Blob} - * A new Blob containing each provided record in CSV format. - */ - service.toBlob = function toBlob(records) { - return new Blob([ encodeCSV(records) ], { type : 'text/csv' }); - }; - - return service; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/preference.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/preference.service.ts new file mode 100644 index 0000000000..0760233c89 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/services/preference.service.ts @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; +import { NavigationEnd, Router } from '@angular/router'; +import { getBrowserLang } from '@ngneat/transloco'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { filter } from 'rxjs'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { DEFAULT_LANGUAGE } from '../../locale/service/translation.service'; +import { LocalStorageService } from '../../storage/local-storage.service'; +import { Preferences } from '../types/Preferences'; + +/** + * The storage key of Guacamole preferences within local storage. + */ +const GUAC_PREFERENCES_STORAGE_KEY = 'GUAC_PREFERENCES'; + +/** + * A service for setting and retrieving browser-local preferences. Preferences + * may be any JSON-serializable type. + */ +@Injectable({ + providedIn: 'root' +}) +export class PreferenceService { + + /** + * All currently-set preferences, as name/value pairs. Each property name + * corresponds to the name of a preference. + */ + preferences: Preferences; + + /** + * All valid input method type names. + */ + inputMethods = { + + /** + * No input method is used. Keyboard events are generated from a + * physical keyboard. + * + * @constant + */ + NONE: 'none', + + /** + * Keyboard events will be generated from the Guacamole on-screen + * keyboard. + * + * @constant + */ + OSK: 'osk', + + /** + * Keyboard events will be generated by inferring the keys necessary to + * produce typed text from an IME (Input Method Editor) such as the + * native on-screen keyboard of a mobile device. + * + * @constant + */ + TEXT: 'text' + + }; + + /** + * Inject required services. + */ + constructor(private localStorageService: LocalStorageService, + private guacEventService: GuacEventService, + private router: Router) { + + this.preferences = { + emulateAbsoluteMouse : true, + inputMethod : this.inputMethods.NONE, + language : this.getDefaultLanguageKey(), + numberOfRecentConnections: 6, + showRecentConnections : true, + timezone : this.getDetectedTimezone() + }; + + // Get stored preferences from localStorage + const storedPreferences = this.localStorageService.getItem(GUAC_PREFERENCES_STORAGE_KEY); + if (storedPreferences) + this.preferences = { ...this.preferences, ...storedPreferences }; + + // Persist settings when window is unloaded + window.addEventListener('unload', this.save); + + + // Persist settings upon navigation + this.router.events.pipe(filter((event) => event instanceof NavigationEnd)) + .subscribe(() => this.save()); + + + // Persist settings upon logout + this.guacEventService.on('guacLogout') + .subscribe(() => { + this.save(); + }); + } + + /** + * Returns the key of the language currently in use within the browser. + * This is not necessarily the user's desired language, but is rather the + * language user by the browser's interface. + * + * @returns + * The key of the language currently in use within the browser. + */ + getDefaultLanguageKey(): string { + + // Pull browser language, falling back to English + return getBrowserLang() || DEFAULT_LANGUAGE; + + } + + /** + * Return the timezone detected for the current browser session. + * + * @returns + * The name of the currently-detected timezone in IANA zone key + * format (Olson time zone database). + */ + getDetectedTimezone(): string { + return Intl.DateTimeFormat().resolvedOptions().timeZone; + } + + /** + * Persists the current values of all preferences, if possible. + */ + save() { + this.localStorageService.setItem(GUAC_PREFERENCES_STORAGE_KEY, this.preferences); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/settings.module.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/settings.module.ts new file mode 100644 index 0000000000..ac7dc42414 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/settings.module.ts @@ -0,0 +1,104 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { CommonModule, NgOptimizedImage } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { RouterLink, RouterOutlet } from '@angular/router'; +import { TranslocoModule } from '@ngneat/transloco'; +import { ClientLibModule } from 'guacamole-frontend-lib'; +import { FormModule } from '../form/form.module'; +import { GroupListModule } from '../group-list/group-list.module'; +import { IndexModule } from '../index/index.module'; +import { ListModule } from '../list/list.module'; +import { NavigationModule } from '../navigation/navigation.module'; +import { PlayerModule } from '../player/player.module'; +import { ConnectionGroupComponent } from './components/connection-group/connection-group.component'; +import { + ConnectionHistoryPlayerComponent +} from './components/connection-history-player/connection-history-player.component'; +import { ConnectionComponent } from './components/connection/connection.component'; +import { + GuacSettingsConnectionHistoryComponent +} from './components/guac-settings-connection-history/guac-settings-connection-history.component'; +import { + GuacSettingsConnectionsComponent +} from './components/guac-settings-connections/guac-settings-connections.component'; +import { + GuacSettingsPreferencesComponent +} from './components/guac-settings-preferences/guac-settings-preferences.component'; +import { GuacSettingsSessionsComponent } from './components/guac-settings-sessions/guac-settings-sessions.component'; +import { + GuacSettingsUserGroupsComponent +} from './components/guac-settings-user-groups/guac-settings-user-groups.component'; +import { GuacSettingsUsersComponent } from './components/guac-settings-users/guac-settings-users.component'; +import { NewConnectionGroupComponent } from './components/new-connection-group/new-connection-group.component'; +import { NewConnectionComponent } from './components/new-connection/new-connection.component'; +import { NewSharingProfileComponent } from './components/new-sharing-profile/new-sharing-profile.component'; +import { SettingsComponent } from './components/settings/settings.component'; +import { SharingProfileComponent } from './components/sharing-profile/sharing-profile.component'; + +/** + * The module for manipulation of general settings. This is distinct from the + * "manage" module, which deals only with administrator-level system management. + */ +@NgModule({ + declarations: [ + GuacSettingsUsersComponent, + SettingsComponent, + GuacSettingsPreferencesComponent, + GuacSettingsUserGroupsComponent, + GuacSettingsConnectionsComponent, + ConnectionComponent, + ConnectionGroupComponent, + NewConnectionComponent, + NewConnectionGroupComponent, + NewSharingProfileComponent, + GuacSettingsConnectionHistoryComponent, + GuacSettingsSessionsComponent, + ConnectionHistoryPlayerComponent, + SharingProfileComponent + ], + imports : [ + CommonModule, + TranslocoModule, + ListModule, + RouterLink, + NavigationModule, + RouterOutlet, + FormModule, + NgOptimizedImage, + FormsModule, + IndexModule, + GroupListModule, + ClientLibModule, + PlayerModule, + ], + exports : [ + GuacSettingsUsersComponent, + SettingsComponent, + GuacSettingsPreferencesComponent, + GuacSettingsUserGroupsComponent, + GuacSettingsConnectionsComponent, + GuacSettingsConnectionHistoryComponent, + GuacSettingsSessionsComponent + ] +}) +export class SettingsModule { +} diff --git a/guacamole/src/main/frontend/src/app/settings/styles/buttons.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/buttons.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/buttons.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/buttons.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/connection-list.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/connection-list.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/connection-list.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/connection-list.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/history-player.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history-player.css similarity index 99% rename from guacamole/src/main/frontend/src/app/settings/styles/history-player.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history-player.css index 55458dd511..65250fd69b 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/history-player.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history-player.css @@ -186,3 +186,4 @@ /* #888888 color at 75% opacity */ fill: rgba(135, 135, 135, 0.75); } + diff --git a/guacamole/src/main/frontend/src/app/settings/styles/history.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history.css similarity index 98% rename from guacamole/src/main/frontend/src/app/settings/styles/history.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history.css index d5c9dac00d..0650c618aa 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/history.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/history.css @@ -85,5 +85,5 @@ background-position: center; background-repeat: no-repeat; background-image: url('images/action-icons/guac-play-link.svg'); - vertical-align: middle; + vertical-align: middle; } diff --git a/guacamole/src/main/frontend/src/app/settings/styles/input-method.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/input-method.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/input-method.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/input-method.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/mouse-mode.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/mouse-mode.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/mouse-mode.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/mouse-mode.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/preferences.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/preferences.css similarity index 94% rename from guacamole/src/main/frontend/src/app/settings/styles/preferences.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/preferences.css index dbb2330a85..86f1749a6c 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/preferences.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/preferences.css @@ -23,10 +23,14 @@ border-left: 3px solid rgba(0,0,0,0.125); } -.preferences .form .fields .labeled-field { +.preferences .form .fields guac-form-field { display: table-row; } +.preferences .form .fields .labeled-field { + display: contents; +} + .preferences .form .fields .field-header, .preferences .form .fields .form-field { display: table-cell; diff --git a/guacamole/src/main/frontend/src/app/settings/styles/sessions.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/sessions.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/sessions.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/sessions.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/settings.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/settings.css similarity index 100% rename from guacamole/src/main/frontend/src/app/settings/styles/settings.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/settings.css diff --git a/guacamole/src/main/frontend/src/app/settings/styles/user-group-list.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-group-list.css similarity index 99% rename from guacamole/src/main/frontend/src/app/settings/styles/user-group-list.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-group-list.css index 6f29de70fa..d53a2d5789 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/user-group-list.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-group-list.css @@ -39,4 +39,4 @@ .settings.user-groups table.user-group-list tr.user-group td.user-group-name { padding: 0; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/settings/styles/user-list.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-list.css similarity index 99% rename from guacamole/src/main/frontend/src/app/settings/styles/user-list.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-list.css index e9631a9509..f0fda1fe8c 100644 --- a/guacamole/src/main/frontend/src/app/settings/styles/user-list.css +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/styles/user-list.css @@ -44,4 +44,4 @@ .settings.users table.user-list tr.user td.username { padding: 0; -} \ No newline at end of file +} diff --git a/guacamole/src/main/frontend/src/app/settings/types/ActiveConnectionWrapper.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ActiveConnectionWrapper.ts similarity index 50% rename from guacamole/src/main/frontend/src/app/settings/types/ActiveConnectionWrapper.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ActiveConnectionWrapper.ts index 18e9032c1b..86016921c6 100644 --- a/guacamole/src/main/frontend/src/app/settings/types/ActiveConnectionWrapper.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ActiveConnectionWrapper.ts @@ -17,61 +17,54 @@ * under the License. */ +import { ActiveConnection } from '../../rest/types/ActiveConnection'; +import { Optional } from '../../util/utility-types'; + /** - * A service for defining the ActiveConnectionWrapper class. + * Wrapper for ActiveConnection which adds display-specific + * properties, such as a checked option. */ -angular.module('settings').factory('ActiveConnectionWrapper', [ - function defineActiveConnectionWrapper() { +export class ActiveConnectionWrapper { + + /** + * The identifier of the data source associated with the + * ActiveConnection wrapped by this ActiveConnectionWrapper. + */ + dataSource: string; + + /** + * The display name of this connection. + */ + name?: string; + + /** + * The date and time this session began, pre-formatted for display. + */ + startDate: string; + + /** + * The wrapped ActiveConnection. + */ + activeConnection: ActiveConnection; + + /** + * A flag indicating that the active connection has been selected. + */ + checked: boolean; /** - * Wrapper for ActiveConnection which adds display-specific - * properties, such as a checked option. - * - * @constructor - * @param {ActiveConnectionWrapper|Object} template + * Creates a new ActiveConnectionWrapper. This constructor initializes the properties of the + * new ActiveConnectionWrapper with the corresponding properties of the given template. + * + * @param template * The object whose properties should be copied within the new * ActiveConnectionWrapper. */ - var ActiveConnectionWrapper = function ActiveConnectionWrapper(template) { - - /** - * The identifier of the data source associated with the - * ActiveConnection wrapped by this ActiveConnectionWrapper. - * - * @type String - */ + constructor(template: Optional) { this.dataSource = template.dataSource; - - /** - * The display name of this connection. - * - * @type String - */ this.name = template.name; - - /** - * The date and time this session began, pre-formatted for display. - * - * @type String - */ this.startDate = template.startDate; - - /** - * The wrapped ActiveConnection. - * - * @type ActiveConnection - */ this.activeConnection = template.activeConnection; - - /** - * A flag indicating that the active connection has been selected. - * - * @type Boolean - */ this.checked = template.checked || false; - - }; - - return ActiveConnectionWrapper; - -}]); \ No newline at end of file + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.ts new file mode 100644 index 0000000000..acbda7ce7d --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/ConnectionHistoryEntryWrapper.ts @@ -0,0 +1,154 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { translate } from '@ngneat/transloco'; +import _ from 'lodash'; +import { ActivityLog } from '../../rest/types/ActivityLog'; +import { ConnectionHistoryEntry } from '../../rest/types/ConnectionHistoryEntry'; + +declare namespace ConnectionHistoryEntryWrapper { + type Log = typeof ConnectionHistoryEntryWrapper.Log.prototype; +} + +/** + * Wrapper for ConnectionHistoryEntry which adds display-specific + * properties, such as a duration. + */ +export class ConnectionHistoryEntryWrapper { + + /** + * The data source associated with the wrapped history record. + */ + dataSource: string; + + /** + * The wrapped ConnectionHistoryEntry. + */ + entry: ConnectionHistoryEntry; + + /** + * The total amount of time the connection associated with the wrapped + * history record was open, in seconds. + */ + duration: number; + + /** + * An object providing value and unit properties, denoting the duration + * and its corresponding units. + */ + readableDuration: ConnectionHistoryEntry.Duration | null = null; + + /** + * The string to display as the duration of this history entry. If a + * duration is available, its value and unit will be exposed to any + * given translation string as the VALUE and UNIT substitution + * variables respectively. + */ + readableDurationText: string; + + /** + * The graphical session recording associated with this history entry, + * if any. If no session recordings are associated with the entry, this + * will be null. If there are multiple session recordings, this will be + * the first such recording. + */ + sessionRecording: ConnectionHistoryEntryWrapper.Log | null = null; + + /** + * Creates a new ConnectionHistoryEntryWrapper. This constructor initializes the properties of the + * new ConnectionHistoryEntryWrapper with the corresponding properties of the given template. + * + * @param historyEntry + * The ConnectionHistoryEntry that should be wrapped. + */ + constructor(dataSource: string, historyEntry: ConnectionHistoryEntry) { + this.dataSource = dataSource; + this.entry = historyEntry; + this.duration = historyEntry.endDate! - historyEntry.startDate!; + + // Set the duration if the necessary information is present + if (historyEntry.endDate && historyEntry.startDate) + this.readableDuration = new ConnectionHistoryEntry.Duration(this.duration); + + this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.TEXT_HISTORY_DURATION'; + + // Inform user if end date is not known + if (!historyEntry.endDate) + this.readableDurationText = 'SETTINGS_CONNECTION_HISTORY.INFO_CONNECTION_DURATION_UNKNOWN'; + + this.sessionRecording = this.getSessionRecording(); + } + + private getSessionRecording(): ConnectionHistoryEntryWrapper.Log | null { + + const identifier = this.entry.identifier; + if (!identifier) + return null; + + const name: string | undefined = _.findKey(this.entry.logs, log => log.type === ActivityLog.Type.GUACAMOLE_SESSION_RECORDING); + if (!name) + return null; + + const log: ActivityLog = (this.entry.logs as Record)[name]; + + const description = log.description && log.description.key ? translate(log.description.key, log.description.variables) : ''; + + return new ConnectionHistoryEntryWrapper.Log({ + + url: '/settings/' + encodeURIComponent(this.dataSource) + + '/recording/' + encodeURIComponent(identifier) + + '/' + encodeURIComponent(name), + + description + + }); + + } + + /** + * Representation of the ActivityLog of a ConnectionHistoryEntry which adds + * display-specific properties, such as a URL for viewing the log. + */ + static Log = class { + + /** + * The relative URL for a session recording player that loads the + * session recording represented by this log. + */ + url: string; + + /** + * A promise that resolves with a human-readable description of the log. + */ + description: Promise; + + /** + * Creates a new ConnectionHistoryEntryWrapper.Log. This constructor initializes the properties of the + * new Log with the corresponding properties of the given template. + * + * @param template + * The object whose properties should be copied within the new + * ConnectionHistoryEntryWrapper.Log. + */ + constructor(template: ConnectionHistoryEntryWrapper.Log | any = {}) { + this.url = template.url; + this.description = template.description; + } + }; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/Preferences.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/Preferences.ts new file mode 100644 index 0000000000..b83f2a1fa8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/settings/types/Preferences.ts @@ -0,0 +1,57 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Preferences, as name/value pairs. Each property name + * corresponds to the name of a preference. + */ +export interface Preferences { + /** + * Whether translation of touch to mouse events should emulate an + * absolute pointer device, or a relative pointer device. + */ + emulateAbsoluteMouse: boolean; + + /** + * The default input method. This may be any of the values defined + * within preferenceService.inputMethods. + */ + inputMethod: string; + + /** + * The key of the desired display language. + */ + language: string; + + /** + * The number of recent connections to display. + */ + numberOfRecentConnections: number; + + /** + * Whether or not to show the "Recent Connections" section. + */ + showRecentConnections: boolean; + + /** + * The timezone set by the user, in IANA zone key format (Olson time + * zone database). + */ + timezone: string; +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.spec.ts new file mode 100644 index 0000000000..d95a49b871 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.spec.ts @@ -0,0 +1,35 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; + +import { LocalStorageService } from './local-storage.service'; + +describe('LocalStorageService', () => { + let service: LocalStorageService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LocalStorageService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/frontend/src/app/storage/services/localStorageService.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.ts similarity index 73% rename from guacamole/src/main/frontend/src/app/storage/services/localStorageService.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.ts index a8c68460ee..15e63d1a95 100644 --- a/guacamole/src/main/frontend/src/app/storage/services/localStorageService.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/local-storage.service.ts @@ -17,30 +17,26 @@ * under the License. */ +import { Injectable } from '@angular/core'; + /** * Service for setting, removing, and retrieving localStorage keys. If access * to localStorage is disabled, or the browser does not support localStorage, * key values are temporarily stored in memory instead. If necessary, the same * functionality is also available at the localStorageServiceProvider level. */ -angular.module('storage').provider('localStorageService', [function localStorageServiceProvider() { - - /** - * Reference to this provider. - * - * @type localStorageServiceProvider - */ - var provider = this; +@Injectable({ + providedIn: 'root' +}) +export class LocalStorageService { /** * Internal cache of key/value pairs stored within localStorage, updated * lazily as keys are retrieved, updated, or removed. If localStorage is * not actually available, then this cache will be the sole storage * location for these key/value pairs. - * - * @type Object. */ - var storedItems = {}; + storedItems: Record = {}; /** * Stores the given value within localStorage under the given key. If access @@ -49,28 +45,26 @@ angular.module('storage').provider('localStorageService', [function localStorage * remaining retrievable via getItem() until the browser tab/window is * closed. * - * @param {String} key + * @param key * The arbitrary, unique key under which the value should be stored. * - * @param {Object} value + * @param value * The object to store under the given key. This may be any object that * can be serialized as JSON, and will automatically be serialized as * JSON prior to storage. */ - provider.setItem = function setItem(key, value) { - + setItem(key: string, value: any): void { // Store given value internally - var data = JSON.stringify(value); - storedItems[key] = data; + const data = JSON.stringify(value); + (this.storedItems)[key] = data; // Additionally store value within localStorage if allowed try { if (window.localStorage) localStorage.setItem(key, data); + } catch (ignore) { } - catch (ignore) {} - - }; + } /** * Removes the item having the given key from localStorage. If access to @@ -78,22 +72,22 @@ angular.module('storage').provider('localStorageService', [function localStorage * removed solely from internal, in-memory storage. If no such item exists, * this function has no effect. * - * @param {String} key + * @param key * The arbitrary, unique key of the item to remove from localStorage. */ - provider.removeItem = function removeItem(key) { + removeItem(key: string) { // Evict key from internal storage - delete storedItems[key]; + delete this.storedItems[key]; // Remove key from localStorage if allowed try { if (window.localStorage) localStorage.removeItem(key); + } catch (ignore) { } - catch (ignore) {} - }; + } /** * Retrieves the value currently stored within localStorage for the item @@ -102,44 +96,34 @@ angular.module('storage').provider('localStorageService', [function localStorage * internal, in-memory storage. The retrieved value is automatically * deserialized from JSON prior to being returned. * - * @param {String} key + * @param key * The arbitrary, unique key of the item to retrieve from localStorage. * - * @returns {Object} + * @returns * The value stored within localStorage under the given key, * automatically deserialized from JSON, or null if no such item is * present. */ - provider.getItem = function getItem(key) { + getItem(key: string): object | null { // Attempt to refresh internal storage from localStorage try { - if (window.localStorage) - storedItems[key] = localStorage.getItem(key); + if (window.localStorage) { + const data = localStorage.getItem(key); + if (data) + this.storedItems[key] = data; + } + } catch (ignore) { } - catch (ignore) {} // Pull and parse value from internal storage, if present - var data = storedItems[key]; + const data = this.storedItems[key]; if (data) return JSON.parse(data); // No value defined for given key return null; - }; - - // Factory method required by provider - this.$get = ['$injector', function localStorageServiceFactory($injector) { - - // Pass through all get/set/remove calls to the provider - // implementations of the same - return { - setItem : provider.setItem, - removeItem : provider.removeItem, - getItem : provider.getItem - }; - - }]; + } -}]); +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.spec.ts new file mode 100644 index 0000000000..624670c41e --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.spec.ts @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { SessionStorageFactory } from './session-storage-factory.service'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +describe('SessionStorageFactoryService', () => { + let service: SessionStorageFactory; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [], + providers: [provideHttpClient(withInterceptorsFromDi()), provideHttpClientTesting()] +}); + service = TestBed.inject(SessionStorageFactory); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/frontend/src/app/storage/services/sessionStorageFactory.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.ts similarity index 57% rename from guacamole/src/main/frontend/src/app/storage/services/sessionStorageFactory.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.ts index da53cdfe97..dd929a5097 100644 --- a/guacamole/src/main/frontend/src/app/storage/services/sessionStorageFactory.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/session-storage-factory.service.ts @@ -17,70 +17,76 @@ * under the License. */ +import { Injectable } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import _ from 'lodash'; +import { AuthenticationService } from '../auth/service/authentication.service'; +import { GuacFrontendEventArguments } from '../events/types/GuacFrontendEventArguments'; +import { isDefined } from '../util/is-defined'; + /** - * Factory for session-local storage. Creating session-local storage creates a - * getter/setter with semantics tied to the user's session. If a user is logged - * in, the storage is consistent. If the user logs out, the storage will not - * persist new values, and attempts to retrieve the existing value will result - * only in the default value. + * A getter/setter which returns or sets the current value of the new + * session-local storage. */ -angular.module('storage').factory('sessionStorageFactory', ['$injector', function sessionStorageFactory($injector) { +export type SessionStorageEntry = (newValue?: T) => T; - // Required services - var $rootScope = $injector.get('$rootScope'); - var authenticationService = $injector.get('authenticationService'); +@Injectable({ + providedIn: 'root' +}) +export class SessionStorageFactory { - var service = {}; + /** + * Inject required services. + */ + constructor(private authenticationService: AuthenticationService, + private readonly guacEventService: GuacEventService) { + } /** * Creates session-local storage that uses the provided default value or * getter to obtain new values as necessary. Beware that if the default is * an object, the resulting getter provide deep copies for new values. * - * @param {Function|*} [template] + * @param template * The default value for new users, or a getter which returns a newly- * created default value. * - * @param {Function} [destructor] + * @param destructor * Function which will be called just before the stored value is * destroyed on logout, if a value is stored. * - * @returns {Function} + * @returns * A getter/setter which returns or sets the current value of the new * session-local storage. Newly-set values will only persist of the * user is actually logged in. */ - service.create = function create(template, destructor) { + create(template: (() => T) | T, destructor?: (value: T | undefined) => void): SessionStorageEntry { /** * Whether new values may be stored and retrieved. - * - * @type Boolean */ - var enabled = !!authenticationService.getCurrentToken(); + let enabled = !!this.authenticationService.getCurrentToken(); /** * Getter which returns the default value for this storage. - * - * @type Function */ - var getter; + let getter: () => T; // If getter provided, use that if (typeof template === 'function') - getter = template; + getter = template as () => T; - // Otherwise, create and maintain a deep copy (automatically cached to + // Otherwise, create and maintain a deep copy (automatically cached to // avoid "infdig" errors) else { - var cached = angular.copy(template); + let cached = _.cloneDeep(template); getter = function getIndependentCopy() { // Reset to template only if changed externally, such that // session storage values can be safely used in scope watches // even if not logged in if (!_.isEqual(cached, template)) - cached = angular.copy(template); + cached = _.cloneDeep(template); return cached; @@ -90,53 +96,54 @@ angular.module('storage').factory('sessionStorageFactory', ['$injector', functio /** * The current value of this storage, or undefined if not yet set. */ - var value = undefined; + let value: T | undefined = undefined; // Reset value and allow storage when the user is logged in - $rootScope.$on('guacLogin', function userLoggedIn() { - enabled = true; - value = undefined; - }); + this.guacEventService.on('guacLogin') + .subscribe(() => { + enabled = true; + value = undefined; + }); // Reset value and disallow storage when the user is logged out - $rootScope.$on('guacLogout', function userLoggedOut() { + this.guacEventService.on('guacLogout') + .subscribe(() => { - // Call destructor before storage is teared down - if (angular.isDefined(value) && destructor) - destructor(value); + // Call destructor before storage is torn down + if (isDefined(value) && destructor) + destructor(value); - // Destroy storage - enabled = false; - value = undefined; + // Destroy storage + enabled = false; + value = undefined; - }); + }); // Return getter/setter for value - return function sessionLocalGetterSetter(newValue) { + return function sessionLocalGetterSetter(newValue?: T) { // Only actually store/retrieve values if enabled if (enabled) { // Set value if provided - if (angular.isDefined(newValue)) + if (isDefined(newValue)) value = newValue; // Obtain new value if unset - if (!angular.isDefined(value)) + if (!isDefined(value)) value = getter(); // Return current value - return value; + return value as T; } // Otherwise, just pretend to store/retrieve - return angular.isDefined(newValue) ? newValue : getter(); + return (isDefined(newValue) ? newValue : getter()) as T; }; - }; + } - return service; -}]); +} diff --git a/guacamole/src/main/frontend/src/app/storage/storageModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/storage.module.ts similarity index 80% rename from guacamole/src/main/frontend/src/app/storage/storageModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/storage.module.ts index 219ffe06c2..13ed45c09e 100644 --- a/guacamole/src/main/frontend/src/app/storage/storageModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/storage/storage.module.ts @@ -17,9 +17,18 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + + /** * Module which provides generic storage services. */ -angular.module('storage', [ - 'auth' -]); +@NgModule({ + declarations: [], + imports : [ + CommonModule + ] +}) +export class StorageModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.html new file mode 100644 index 0000000000..991f716537 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.html @@ -0,0 +1,22 @@ + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.spec.ts new file mode 100644 index 0000000000..e1b7acbbfb --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.spec.ts @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslocoTestingModule } from '@ngneat/transloco'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; +import { KeyComponent } from './key.component'; + +describe('KeyComponent', () => { + let component: KeyComponent; + let fixture: ComponentFixture; + let guacEventService: GuacEventService; + + beforeEach(() => { + TestBed.configureTestingModule({ + declarations: [KeyComponent], + imports : [ + TranslocoTestingModule.forRoot({}) + ], + }); + fixture = TestBed.createComponent(KeyComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + guacEventService = TestBed.inject(GuacEventService); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should toggle pressed state for sticky keys', () => { + spyOn(component.pressedChange, 'emit'); + + component.sticky = true; + component.pressed = false; + fixture.detectChanges(); + + component.updateKey(new MouseEvent('click')); + + expect(component.pressed).toBeTrue(); + expect(component.pressedChange.emit).toHaveBeenCalledWith(true); + }); + + it('should not toggle pressed state for non-sticky keys', () => { + spyOn(component.pressedChange, 'emit'); + + component.sticky = false; + fixture.detectChanges(); + + component.updateKey(new MouseEvent('click')); + + expect(component.pressed).toBeFalse(); + expect(component.pressedChange.emit).not.toHaveBeenCalled(); + }); + + it('should broadcast keydown and keyup events for non-sticky keys', () => { + spyOn(guacEventService, 'broadcast'); + + component.sticky = false; + component.keysym = 65; // Example keysym + fixture.detectChanges(); + + component.updateKey(new MouseEvent('click')); + + expect(guacEventService.broadcast).toHaveBeenCalledWith('guacSyntheticKeydown', { keysym: 65 }); + expect(guacEventService.broadcast).toHaveBeenCalledWith('guacSyntheticKeyup', { keysym: 65 }); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.ts new file mode 100644 index 0000000000..281f285fe0 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-key/key.component.ts @@ -0,0 +1,110 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Component, EventEmitter, Input, OnChanges, Output, SimpleChanges, ViewEncapsulation } from '@angular/core'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; + +/** + * A component which displays a button that controls the pressed state of a + * single keyboard key. + */ +@Component({ + selector: 'guac-key', + templateUrl: './key.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class KeyComponent implements OnChanges { + + /** + * The text to display within the key. This will be run through the + * translation filter prior to display. + */ + @Input({ required: true }) text!: string; + + /** + * The keysym to send within keyup and keydown events when this key + * is pressed or released. + */ + @Input({ required: true }) keysym!: number; + + /** + * Whether this key is sticky. Sticky keys toggle their pressed + * state with each click. + * + * @default false + */ + @Input() sticky = false; + + /** + * Whether this key is currently pressed. + * + * @default false + */ + @Input() pressed = false; + @Output() pressedChange = new EventEmitter(); + + constructor(private guacEventService: GuacEventService) { + } + + /** + * Presses and releases this key, sending the corresponding keydown + * and keyup events. In the case of sticky keys, the pressed state + * is toggled, and only a single keydown/keyup event will be sent, + * depending on the current state. + * + * @param event + * The mouse event which resulted in this function being + * invoked. + */ + updateKey(event: MouseEvent) { + // If sticky, toggle pressed state + if (this.sticky) { + this.pressed = !this.pressed; + this.pressedChange.emit(this.pressed); + } + + // For all non-sticky keys, press and release key immediately + else { + this.guacEventService.broadcast('guacSyntheticKeydown', { keysym: this.keysym }); + this.guacEventService.broadcast('guacSyntheticKeyup', { keysym: this.keysym }); + } + + // Prevent loss of focus due to interaction with buttons + event.preventDefault(); + } + + ngOnChanges(changes: SimpleChanges): void { + // Send keyup/keydown when pressed state is altered + if (changes['pressed']) { + + const isPressed = changes['pressed'].currentValue; + const wasPressed = changes['pressed'].previousValue; + + // If the key is pressed now, send keydown + if (isPressed) + this.guacEventService.broadcast('guacSyntheticKeydown', { keysym: this.keysym }); + + // If the key was pressed, but is not pressed any longer, send keyup + else if (wasPressed) + this.guacEventService.broadcast('guacSyntheticKeyup', { keysym: this.keysym }); + } + } +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.html new file mode 100644 index 0000000000..11b3cb2182 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.html @@ -0,0 +1,35 @@ + + +
      + + +
      +
      +
      {{ text }}
      +
      +
      +
      + + + + +
      + +
      diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.ts new file mode 100644 index 0000000000..86bd450197 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/guac-text-input/text-input.component.ts @@ -0,0 +1,330 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { AfterViewInit, Component, DestroyRef, ElementRef, ViewChild, ViewEncapsulation } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { GuacEventService } from 'guacamole-frontend-lib'; +import { GuacFrontendEventArguments } from '../../events/types/GuacFrontendEventArguments'; + +/** + * The number of characters to include on either side of text input + * content, to allow the user room to use backspace and delete. + */ +const TEXT_INPUT_PADDING = 4; + +/** + * The Unicode codepoint of the character to use for padding on + * either side of text input content. + */ +const TEXT_INPUT_PADDING_CODEPOINT = 0x200B; + +/** + * Keys which should be allowed through to the client when in text + * input mode, providing corresponding key events are received. + * Keys in this set will be allowed through to the server. + */ +const ALLOWED_KEYS: Record = { + 0xFE03: true, /* AltGr */ + 0xFF08: true, /* Backspace */ + 0xFF09: true, /* Tab */ + 0xFF0D: true, /* Enter */ + 0xFF1B: true, /* Escape */ + 0xFF50: true, /* Home */ + 0xFF51: true, /* Left */ + 0xFF52: true, /* Up */ + 0xFF53: true, /* Right */ + 0xFF54: true, /* Down */ + 0xFF57: true, /* End */ + 0xFF64: true, /* Insert */ + 0xFFBE: true, /* F1 */ + 0xFFBF: true, /* F2 */ + 0xFFC0: true, /* F3 */ + 0xFFC1: true, /* F4 */ + 0xFFC2: true, /* F5 */ + 0xFFC3: true, /* F6 */ + 0xFFC4: true, /* F7 */ + 0xFFC5: true, /* F8 */ + 0xFFC6: true, /* F9 */ + 0xFFC7: true, /* F10 */ + 0xFFC8: true, /* F11 */ + 0xFFC9: true, /* F12 */ + 0xFFE1: true, /* Left shift */ + 0xFFE2: true, /* Right shift */ + 0xFFE3: true, /* Left ctrl */ + 0xFFE4: true, /* Right ctrl */ + 0xFFE9: true, /* Left alt */ + 0xFFEA: true, /* Right alt */ + 0xFFFF: true /* Delete */ +}; + +/** + * A component which displays the Guacamole text input method. + */ +@Component({ + selector: 'guac-text-input', + templateUrl: './text-input.component.html', + encapsulation: ViewEncapsulation.None, + standalone: false +}) +export class TextInputComponent implements AfterViewInit { + + /** + * Recently-sent text, ordered from oldest to most recent. + */ + sentText: string[] = []; + + /** + * Whether the "Alt" key is currently pressed within the text input + * interface. + */ + altPressed = false; + + /** + * Whether the "Ctrl" key is currently pressed within the text + * input interface. + */ + ctrlPressed = false; + + /** + * ElementRef to the text area input target. + */ + @ViewChild('target') targetRef!: ElementRef; + + /** + * The text area input target. + */ + target!: HTMLTextAreaElement; + + /** + * Whether the text input target currently has focus. Setting this + * attribute has no effect, but any bound property will be updated + * as focus is gained or lost. + */ + hasFocus = false; + + /** + * Whether composition is currently active within the text input + * target element, such as when an IME is in use. + */ + composingText = false; + + /** + * Inject required services. + */ + constructor(private guacEventService: GuacEventService, + private destroyRef: DestroyRef) { + } + + ngAfterViewInit(): void { + this.target = this.targetRef.nativeElement; + + this.target.onfocus = () => { + this.hasFocus = true; + this.resetTextInputTarget(TEXT_INPUT_PADDING); + }; + this.target.onblur = () => { + this.hasFocus = false; + }; + + this.target.addEventListener('input', (e: Event) => { + // Ignore input events during text composition + if (this.composingText) + return; + + let i; + const content = this.target.value; + const expectedLength = TEXT_INPUT_PADDING * 2; + + // If content removed, update + if (content.length < expectedLength) { + + // Calculate number of backspaces and send + const backspaceCount = TEXT_INPUT_PADDING - this.target.selectionStart; + for (i = 0; i < backspaceCount; i++) + this.sendKeysym(0xFF08); + + // Calculate number of deletes and send + const deleteCount = expectedLength - content.length - backspaceCount; + for (i = 0; i < deleteCount; i++) + this.sendKeysym(0xFFFF); + + } else + this.sendString(content); + + // Reset content + this.resetTextInputTarget(TEXT_INPUT_PADDING); + e.preventDefault(); + + }, false); + + // Do not allow event target contents to be selected during input + this.target.addEventListener('selectstart', function (e) { + e.preventDefault(); + }, false); + + // If the text input UI has focus, prevent keydown events + this.guacEventService.on('guacBeforeKeydown') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym }) => { + // filterKeydown + if (this.hasFocus && !ALLOWED_KEYS[keysym]) + event.preventDefault(); + }); + + // If the text input UI has focus, prevent keyup events + this.guacEventService.on('guacBeforeKeyup') + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(({ event, keysym }) => { + // filterKeyup + if (this.hasFocus && !ALLOWED_KEYS[keysym]) + event.preventDefault(); + }); + + // Attempt to focus initially + this.target.focus(); + } + + /** + * Translates a given Unicode codepoint into the corresponding X11 + * keysym. + * + * @param codepoint + * The Unicode codepoint to translate. + * + * @returns + * The X11 keysym that corresponds to the given Unicode + * codepoint, or null if no such keysym exists. + */ + keysymFromCodepoint(codepoint: number): number | null { + + // Keysyms for control characters + if (codepoint <= 0x1F || (codepoint >= 0x7F && codepoint <= 0x9F)) + return 0xFF00 | codepoint; + + // Keysyms for ASCII chars + if (codepoint >= 0x0000 && codepoint <= 0x00FF) + return codepoint; + + // Keysyms for Unicode + if (codepoint >= 0x0100 && codepoint <= 0x10FFFF) + return 0x01000000 | codepoint; + + return null; + + } + + /** + * Presses and releases the key corresponding to the given keysym, + * as if typed by the user. + * + * @param keysym The keysym of the key to send. + */ + sendKeysym(keysym: number): void { + this.guacEventService.broadcast('guacSyntheticKeydown', { keysym }); + this.guacEventService.broadcast('guacSyntheticKeyup', { keysym }); + } + + /** + * Presses and releases the key having the keysym corresponding to + * the Unicode codepoint given, as if typed by the user. + * + * @param codepoint + * The Unicode codepoint of the key to send. + */ + sendCodepoint(codepoint: number): void { + + if (codepoint === 10) { + this.sendKeysym(0xFF0D); + this.releaseStickyKeys(); + return; + } + + const keysym = this.keysymFromCodepoint(codepoint); + if (keysym) { + this.sendKeysym(keysym); + this.releaseStickyKeys(); + } + } + + /** + * Translates each character within the given string to keysyms and + * sends each, in order, as if typed by the user. + * + * @param content + * The string to send. + */ + sendString(content: string): void { + + let sentText = ''; + + // Send each codepoint within the string + for (let i = 0; i < content.length; i++) { + const codepoint = content.charCodeAt(i); + if (codepoint !== TEXT_INPUT_PADDING_CODEPOINT) { + sentText += String.fromCharCode(codepoint); + this.sendCodepoint(codepoint); + } + } + + // Display the text that was sent + this.sentText.push(sentText); + + // Remove text after one second + setTimeout(() => { + this.sentText.shift(); + }, 1000); + + } + + /** + * Releases all currently-held sticky keys within the text input UI. + */ + releaseStickyKeys(): void { + + // Reset all sticky keys + this.altPressed = false; + this.ctrlPressed = false; + + } + + /** + * Removes all content from the text input target, replacing it + * with the given number of padding characters. Padding of the + * requested size is added on both sides of the cursor, thus the + * overall number of characters added will be twice the number + * specified. + * + * @param padding + * The number of characters to pad the text area with. + */ + resetTextInputTarget(padding: number): void { + + const paddingChar = String.fromCharCode(TEXT_INPUT_PADDING_CODEPOINT); + + // Pad text area with an arbitrary, non-typable character (so there is something + // to delete with backspace or del), and position cursor in middle. + this.target.value = new Array(padding * 2 + 1).join(paddingChar); + this.target.setSelectionRange(padding, padding); + + } + + trackByIndex(index: number): number { + return index; + } +} diff --git a/guacamole/src/main/frontend/src/app/textInput/styles/textInput.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/styles/textInput.css similarity index 100% rename from guacamole/src/main/frontend/src/app/textInput/styles/textInput.css rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/styles/textInput.css diff --git a/guacamole/src/main/frontend/src/app/textInput/textInputModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/text-input.module.ts similarity index 61% rename from guacamole/src/main/frontend/src/app/textInput/textInputModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/text-input.module.ts index 7fd3445270..f858c3353a 100644 --- a/guacamole/src/main/frontend/src/app/textInput/textInputModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/text-input/text-input.module.ts @@ -17,7 +17,29 @@ * under the License. */ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { TranslocoModule } from '@ngneat/transloco'; +import { KeyComponent } from './guac-key/key.component'; +import { TextInputComponent } from './guac-text-input/text-input.component'; + + /** * Module for displaying the Guacamole text input method. */ -angular.module('textInput', []); +@NgModule({ + declarations: [ + TextInputComponent, + KeyComponent + ], + imports : [ + CommonModule, + TranslocoModule, + ], + exports : [ + TextInputComponent, + KeyComponent + ] +}) +export class TextInputModule { +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/ApplicationState.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/ApplicationState.ts new file mode 100644 index 0000000000..0761e37782 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/ApplicationState.ts @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Possible overall states of the client side of the web application. + */ +export enum ApplicationState { + + /** + * A non-interactive authentication attempt failed. + */ + AUTOMATIC_LOGIN_REJECTED = 'automaticLoginRejected', + + /** + * The application has fully loaded but is awaiting credentials from + * the user before proceeding. + */ + AWAITING_CREDENTIALS = 'awaitingCredentials', + + /** + * A fatal error has occurred that will prevent the client side of the + * application from functioning properly. + */ + FATAL_ERROR = 'fatalError', + + /** + * The application has just started within the user's browser and has + * not yet settled into any specific state. + */ + LOADING = 'loading', + + /** + * The user has manually logged out. + */ + LOGGED_OUT = 'loggedOut', + + /** + * The application has fully loaded and the user has logged in + */ + READY = 'ready' + + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/interceptor.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/interceptor.service.ts new file mode 100644 index 0000000000..76602c38ad --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/interceptor.service.ts @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { HttpContextToken, HttpRequest } from '@angular/common/http'; +import { Injectable } from '@angular/core'; + +/** + * HTTP context token that can be used to skip all interceptors when issuing a + * request. + */ +export const SKIP_ALL_INTERCEPTORS: HttpContextToken = new HttpContextToken(() => false); + +/** + * HTTP context token that can be used to skip the {@link ErrorHandlingInterceptor} when issuing a request. + */ +export const SKIP_ERROR_HANDLING_INTERCEPTOR: HttpContextToken = new HttpContextToken(() => false); + +/** + * HTTP context token that can be used to skip the {@link AuthenticationInterceptor} when issuing a request. + */ +export const SKIP_AUTHENTICATION_INTERCEPTOR: HttpContextToken = new HttpContextToken(() => false); + +/** + * HTTP context token that can be used to set the authentication token to pass with the "Guacamole-Token" header. + * If omitted, and the user is logged in, the user's current authentication token will be used. + */ +export const AUTHENTICATION_TOKEN: HttpContextToken = new HttpContextToken(() => null); + + +/** + * Service that can be used by interceptors to determine whether they should + * skip processing for a given request. + */ +@Injectable({ + providedIn: 'root' +}) +export class InterceptorService { + + /** + * Returns whether the given request should skip all interceptors. + * + * @param request + * The request to check. + * + * @returns + * true if the request should skip all interceptors, false otherwise. + */ + skipAllInterceptors(request: HttpRequest): boolean { + return request.context.get(SKIP_ALL_INTERCEPTORS); + } + + /** + * Returns whether the given request should skip the {@link ErrorHandlingInterceptor}. + * + * @param request + * The request to check. + * + * @returns + * true if the request should skip all interceptors, false otherwise. + */ + skipErrorHandlingInterceptor(request: HttpRequest): boolean { + return request.context.get(SKIP_ERROR_HANDLING_INTERCEPTOR) || this.skipAllInterceptors(request); + } + + /** + * Returns whether the given request should skip the {@link AuthenticationInterceptor}. + * + * @param request + * The request to check. + * + * @returns + * true if the request should skip all interceptors, false otherwise. + */ + skipAuthenticationInterceptor(request: HttpRequest): boolean { + return request.context.get(SKIP_AUTHENTICATION_INTERCEPTOR) || this.skipAllInterceptors(request); + } + + + /** + * Returns the authentication token to pass with the "Guacamole-Token" header. + * + * @param request + * The request to check. + * + * @returns + * The authentication token if set, null otherwise. + */ + getAuthenticationToken(request: HttpRequest): string | null { + return request.context.get(AUTHENTICATION_TOKEN); + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-array.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-array.ts new file mode 100644 index 0000000000..78269de756 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-array.ts @@ -0,0 +1,28 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * Determines if a reference is an `Array`. + * + * @param value Reference to check. + * @returns True if `value` is an `Array`. + */ +export function isArray(value: T | T[]): value is T[] { + return Array.isArray(value) || value instanceof Array; +} diff --git a/guacamole/src/main/frontend/src/app/element/elementModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-defined.ts similarity index 78% rename from guacamole/src/main/frontend/src/app/element/elementModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-defined.ts index 0b564fb597..17832cc4c7 100644 --- a/guacamole/src/main/frontend/src/app/element/elementModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/is-defined.ts @@ -18,7 +18,14 @@ */ /** - * Module for manipulating element state, such as focus or scroll position, as - * well as handling browser events. + * Determines if a reference is defined. + * + * @param value + * Reference to check. + * + * @returns + * True if `value` is defined. */ -angular.module('element', []); +export function isDefined(value: any): boolean { + return typeof value !== 'undefined'; +} diff --git a/guacamole/src/main/frontend/src/app/form/controllers/selectFieldController.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.spec.ts similarity index 69% rename from guacamole/src/main/frontend/src/app/form/controllers/selectFieldController.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.spec.ts index e780378451..d87599cc3a 100644 --- a/guacamole/src/main/frontend/src/app/form/controllers/selectFieldController.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.spec.ts @@ -17,17 +17,19 @@ * under the License. */ +import { TestBed } from '@angular/core/testing'; -/** - * Controller for select fields. - */ -angular.module('form').controller('selectFieldController', ['$scope', '$injector', - function selectFieldController($scope, $injector) { +import { LogService } from './log.service'; + +describe('LogService', () => { + let service: LogService; - // Interpret undefined/null as empty string - $scope.$watch('model', function setModel(model) { - if (!model && model !== '') - $scope.model = ''; + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(LogService); }); -}]); + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.ts new file mode 100644 index 0000000000..ad43756ace --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/log.service.ts @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Injectable } from '@angular/core'; + +@Injectable({ + providedIn: 'root' +}) +export class LogService { + + info(message: any, ...optionalParams: any[]) { + console.log(message, optionalParams); + } + + error(message: any, ...optionalParams: any[]) { + console.error(message, optionalParams); + } + + warn(message: any, ...optionalParams: any[]) { + console.warn(message, optionalParams); + } + + debug(message: any, ...optionalParams: any[]) { + console.debug(message, optionalParams); + } +} diff --git a/guacamole/src/main/frontend/src/app/home/homeModule.js b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/noop.ts similarity index 90% rename from guacamole/src/main/frontend/src/app/home/homeModule.js rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/noop.ts index 5d930dae9c..8abf65dcec 100644 --- a/guacamole/src/main/frontend/src/app/home/homeModule.js +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/noop.ts @@ -17,4 +17,5 @@ * under the License. */ -angular.module('home', ['client', 'groupList', 'history', 'navigation', 'rest']); +export const NOOP = () => { +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.spec.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.spec.ts new file mode 100644 index 0000000000..f95c5cce21 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.spec.ts @@ -0,0 +1,45 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestBed } from '@angular/core/testing'; +import { SortService } from '../list/services/sort.service'; +import { OrderByPipe } from './order-by.pipe'; + +describe('OrderByPipe', () => { + let pipe: OrderByPipe; + let sortService: SortService; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [SortService] + }); + + sortService = TestBed.inject(SortService); + pipe = new OrderByPipe(sortService); + }); + + it('create an instance', () => { + expect(pipe).toBeTruthy(); + }); + + it('orders by property', () => { + const result = pipe.transform([{ name: 'b' }, { name: 'a' }], 'name'); + expect(result).toEqual([{ name: 'a' }, { name: 'b' }]); + }); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.ts new file mode 100644 index 0000000000..6b9fd09d1c --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/order-by.pipe.ts @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { Pipe, PipeTransform } from '@angular/core'; +import { SortService } from '../list/services/sort.service'; + +/** + * Pipe that sorts a collection by the given predicates. + */ +@Pipe({ + name : 'orderBy', + standalone: true +}) +export class OrderByPipe implements PipeTransform { + + /** + * Inject required service. + */ + constructor(private sortService: SortService) { + } + + /** + * @see SortService.orderByPredicate + */ + transform(collection: T[] | null | undefined, ...predicates: string[]): T[] { + + return this.sortService.orderByPredicate(collection, predicates); + + } + +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/test-helper.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/test-helper.ts new file mode 100644 index 0000000000..78eb457836 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/test-helper.ts @@ -0,0 +1,30 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { TestScheduler } from 'rxjs/testing'; + +/** + * Provides a TestScheduler that can be used for testing RxJS observables ("marble tests"). + */ +export const getTestScheduler = () => new TestScheduler((actual, expected) => { + // asserting the two objects are equal - required + // for TestScheduler assertions to work via your test framework + // e.g. using chai. + expect(actual).toEqual(expected); +}); diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/utility-types.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/utility-types.ts new file mode 100644 index 0000000000..84d0d352c8 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/app/util/utility-types.ts @@ -0,0 +1,37 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/** + * From T make a set of properties by key K become optional + */ +export type Optional = Omit & Partial> + +/** + * From T make a set of properties by key K become non-nullable + */ +export type NonNullableProperties = { + [P in keyof T]: P extends K ? NonNullable : T[P]; +}; + +/** + * From T make all properties non-nullable + */ +export type NonNullableAllProperties = { + [P in keyof T]: NonNullable; +}; diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/bootstrap.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/bootstrap.ts new file mode 100644 index 0000000000..9966db5900 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/bootstrap.ts @@ -0,0 +1,29 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { ViewEncapsulation } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; + + +platformBrowserDynamic().bootstrapModule(AppModule, { + defaultEncapsulation: ViewEncapsulation.None +}) + .catch(err => console.error(err)); diff --git a/guacamole/src/main/frontend/src/fonts/carlito/Carlito-Bold.woff b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Bold.woff similarity index 100% rename from guacamole/src/main/frontend/src/fonts/carlito/Carlito-Bold.woff rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Bold.woff diff --git a/guacamole/src/main/frontend/src/fonts/carlito/Carlito-Italic.woff b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Italic.woff similarity index 100% rename from guacamole/src/main/frontend/src/fonts/carlito/Carlito-Italic.woff rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Italic.woff diff --git a/guacamole/src/main/frontend/src/fonts/carlito/Carlito-Regular.woff b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Regular.woff similarity index 100% rename from guacamole/src/main/frontend/src/fonts/carlito/Carlito-Regular.woff rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/Carlito-Regular.woff diff --git a/guacamole/src/main/frontend/src/fonts/carlito/LICENSE b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/LICENSE similarity index 100% rename from guacamole/src/main/frontend/src/fonts/carlito/LICENSE rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/fonts/carlito/LICENSE diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-back.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-back.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-back.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-back.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-config-dark.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-config-dark.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-config-dark.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-config-dark.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-config.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-config.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-config.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-config.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-file-import.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-file-import.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-file-import.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-file-import.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-first-page.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-first-page.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-first-page.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-first-page.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-group-add.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-group-add.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-group-add.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-group-add.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-hide-pass.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-hide-pass.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-hide-pass.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-hide-pass.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-home-dark.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-home-dark.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-home-dark.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-home-dark.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-home.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-home.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-home.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-home.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-key.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-key.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-key.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-key.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-last-page.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-last-page.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-last-page.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-last-page.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-logout-dark.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-logout-dark.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-logout-dark.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-logout-dark.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-logout.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-logout.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-logout.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-logout.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-monitor-add.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-monitor-add.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-monitor-add.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-next-page.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-next-page.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-next-page.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-next-page.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-pause.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-pause.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-pause.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-pause.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-play-link.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-play-link.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-play-link.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-play-link.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-play.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-play.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-play.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-play.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-prev-page.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-prev-page.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-prev-page.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-prev-page.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-show-pass.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-show-pass.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-show-pass.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-show-pass.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-user-add.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-user-add.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-user-add.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-user-add.svg diff --git a/guacamole/src/main/frontend/src/images/action-icons/guac-user-group-add.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-user-group-add.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/action-icons/guac-user-group-add.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/action-icons/guac-user-group-add.svg diff --git a/guacamole/src/main/frontend/src/images/arrows/down.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/down.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/arrows/down.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/down.svg diff --git a/guacamole/src/main/frontend/src/images/arrows/left.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/left.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/arrows/left.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/left.svg diff --git a/guacamole/src/main/frontend/src/images/arrows/right.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/right.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/arrows/right.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/right.svg diff --git a/guacamole/src/main/frontend/src/images/arrows/up.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/up.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/arrows/up.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/arrows/up.svg diff --git a/guacamole/src/main/frontend/src/images/checker.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/checker.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/checker.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/checker.svg diff --git a/guacamole/src/main/frontend/src/images/checkmark.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/checkmark.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/checkmark.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/checkmark.svg diff --git a/guacamole/src/main/frontend/src/images/circle-arrows.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/circle-arrows.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/circle-arrows.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/circle-arrows.svg diff --git a/guacamole/src/main/frontend/src/images/cog.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/cog.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/cog.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/cog.svg diff --git a/guacamole/src/main/frontend/src/images/drive.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/drive.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/drive.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/drive.svg diff --git a/guacamole/src/main/frontend/src/images/file.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/file.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/file.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/file.svg diff --git a/guacamole/src/main/frontend/src/images/folder-closed.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-closed.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/folder-closed.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-closed.svg diff --git a/guacamole/src/main/frontend/src/images/folder-open.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-open.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/folder-open.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-open.svg diff --git a/guacamole/src/main/frontend/src/images/folder-up.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-up.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/folder-up.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/folder-up.svg diff --git a/guacamole/src/main/frontend/src/images/fullscreen.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/fullscreen.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/fullscreen.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/fullscreen.svg diff --git a/guacamole/src/main/frontend/src/images/group-icons/guac-closed.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/group-icons/guac-closed.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/group-icons/guac-closed.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/group-icons/guac-closed.svg diff --git a/guacamole/src/main/frontend/src/images/group-icons/guac-open.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/group-icons/guac-open.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/group-icons/guac-open.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/group-icons/guac-open.svg diff --git a/guacamole/src/main/frontend/src/images/guac-tricolor.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/guac-tricolor.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/guac-tricolor.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/guac-tricolor.svg diff --git a/guacamole/src/main/frontend/src/images/lock.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/lock.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/lock.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/lock.svg diff --git a/guacamole/src/main/frontend/src/images/logo-144.png b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-144.png similarity index 100% rename from guacamole/src/main/frontend/src/images/logo-144.png rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-144.png diff --git a/guacamole/src/main/frontend/src/images/logo-192.png b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-192.png similarity index 100% rename from guacamole/src/main/frontend/src/images/logo-192.png rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-192.png diff --git a/guacamole/src/main/frontend/src/images/logo-512.png b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-512.png similarity index 100% rename from guacamole/src/main/frontend/src/images/logo-512.png rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-512.png diff --git a/guacamole/src/main/frontend/src/images/logo-64.png b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-64.png similarity index 100% rename from guacamole/src/main/frontend/src/images/logo-64.png rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-64.png diff --git a/guacamole/src/main/frontend/src/images/logo-vector.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-vector.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/logo-vector.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/logo-vector.svg diff --git a/guacamole/src/main/frontend/src/images/magnifier.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/magnifier.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/magnifier.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/magnifier.svg diff --git a/guacamole/src/main/frontend/src/images/mouse/blank.cur b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/blank.cur similarity index 100% rename from guacamole/src/main/frontend/src/images/mouse/blank.cur rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/blank.cur diff --git a/guacamole/src/main/frontend/src/images/mouse/blank.gif b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/blank.gif similarity index 100% rename from guacamole/src/main/frontend/src/images/mouse/blank.gif rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/blank.gif diff --git a/guacamole/src/main/frontend/src/images/mouse/dot.gif b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/dot.gif similarity index 100% rename from guacamole/src/main/frontend/src/images/mouse/dot.gif rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/mouse/dot.gif diff --git a/guacamole/src/main/frontend/src/images/plus.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/plus.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/plus.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/plus.svg diff --git a/guacamole/src/main/frontend/src/images/progress.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/progress.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/progress.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/progress.svg diff --git a/guacamole/src/main/frontend/src/images/protocol-icons/guac-monitor.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-monitor.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/protocol-icons/guac-monitor.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-monitor.svg diff --git a/guacamole/src/main/frontend/src/images/protocol-icons/guac-plug.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-plug.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/protocol-icons/guac-plug.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-plug.svg diff --git a/guacamole/src/main/frontend/src/images/protocol-icons/guac-text.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-text.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/protocol-icons/guac-text.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/protocol-icons/guac-text.svg diff --git a/guacamole/src/main/frontend/src/images/question.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/question.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/question.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/question.svg diff --git a/guacamole/src/main/frontend/src/images/settings/tablet-keys.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/tablet-keys.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/settings/tablet-keys.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/tablet-keys.svg diff --git a/guacamole/src/main/frontend/src/images/settings/touchpad.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/touchpad.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/settings/touchpad.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/touchpad.svg diff --git a/guacamole/src/main/frontend/src/images/settings/touchscreen.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/touchscreen.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/settings/touchscreen.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/touchscreen.svg diff --git a/guacamole/src/main/frontend/src/images/settings/zoom-in.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/zoom-in.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/settings/zoom-in.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/zoom-in.svg diff --git a/guacamole/src/main/frontend/src/images/settings/zoom-out.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/zoom-out.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/settings/zoom-out.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/settings/zoom-out.svg diff --git a/guacamole/src/main/frontend/src/images/share-white.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/share-white.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/share-white.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/share-white.svg diff --git a/guacamole/src/main/frontend/src/images/share.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/share.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/share.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/share.svg diff --git a/guacamole/src/main/frontend/src/images/user-icons/guac-user-group.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user-group.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/user-icons/guac-user-group.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user-group.svg diff --git a/guacamole/src/main/frontend/src/images/user-icons/guac-user-white.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user-white.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/user-icons/guac-user-white.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user-white.svg diff --git a/guacamole/src/main/frontend/src/images/user-icons/guac-user.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/user-icons/guac-user.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/user-icons/guac-user.svg diff --git a/guacamole/src/main/frontend/src/images/warning-white.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/warning-white.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/warning-white.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/warning-white.svg diff --git a/guacamole/src/main/frontend/src/images/warning.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/warning.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/warning.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/warning.svg diff --git a/guacamole/src/main/frontend/src/images/x-black.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x-black.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/x-black.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x-black.svg diff --git a/guacamole/src/main/frontend/src/images/x-red.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x-red.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/x-red.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x-red.svg diff --git a/guacamole/src/main/frontend/src/images/x.svg b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x.svg similarity index 100% rename from guacamole/src/main/frontend/src/images/x.svg rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/images/x.svg diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/index.html b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/index.html new file mode 100644 index 0000000000..dad634d530 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/index.html @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/main.ts b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/main.ts new file mode 100644 index 0000000000..660a9ee450 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/main.ts @@ -0,0 +1,25 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +import { initFederation } from '@angular-architects/native-federation'; + +initFederation('native-federation-manifest.json') + .catch(err => console.error(err)) + .then(_ => import('./bootstrap')) + .catch(err => console.error(err)); diff --git a/guacamole/src/main/frontend/src/manifest.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/manifest.json similarity index 100% rename from guacamole/src/main/frontend/src/manifest.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/manifest.json diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/styles.css b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/styles.css new file mode 100644 index 0000000000..620671f4ba --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/styles.css @@ -0,0 +1,112 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +/* You can add global styles to this file, and also import other style files */ + +@import "app/client/styles/client.css"; +@import "app/client/styles/connection-select-menu.css"; +@import "app/client/styles/connection-warning.css"; +@import "app/client/styles/display.css"; +@import "app/client/styles/file-browser.css"; +@import "app/client/styles/file-transfer-dialog.css"; +@import "app/client/styles/filesystem-menu.css"; +@import "app/client/styles/guac-menu.css"; +@import "app/client/styles/keyboard.css"; +@import "app/client/styles/menu.css"; +@import "app/client/styles/notification.css"; +@import "app/client/styles/share-menu.css"; +@import "app/client/styles/thumbnail-display.css"; +@import "app/client/styles/tiled-client-grid.css"; +@import "app/client/styles/transfer-manager.css"; +@import "app/client/styles/transfer.css"; +@import "app/client/styles/viewport.css"; +@import "app/client/styles/zoom.css"; + +@import "app/clipboard/styles/clipboard.css"; + +@import "app/form/styles/form-field.css"; +@import "app/form/styles/form.css"; +@import "app/form/styles/redirect-field.css"; +@import "app/form/styles/terminal-color-scheme-field.css"; + +@import "app/home/styles/home.css"; + +@import "app/import/styles/help.css"; +@import "app/import/styles/import.css"; + +@import "app/index/styles/animation.css"; +@import "app/index/styles/automatic-login-rejected.css"; +@import "app/index/styles/buttons.css"; +@import "app/index/styles/cloak.css"; +@import "app/index/styles/fatal-page-error.css"; +@import "app/index/styles/font-carlito.css"; +@import "app/index/styles/headers.css"; +@import "app/index/styles/input.css"; +@import "app/index/styles/lists.css"; +@import "app/index/styles/loading.css"; +@import "app/index/styles/logged-out.css"; +@import "app/index/styles/other-connections.css"; +@import "app/index/styles/sorted-tables.css"; +@import "app/index/styles/status.css"; +@import "app/index/styles/ui.css"; + +@import "app/list/styles/filter.css"; +@import "app/list/styles/pager.css"; +@import "app/list/styles/user-item.css"; +@import "app/login/styles/animation.css"; +@import "app/login/styles/dialog.css"; +@import "app/login/styles/input.css"; +@import "app/login/styles/login.css"; + +@import "app/manage/styles/attributes.css"; +@import "app/manage/styles/connection-parameter.css"; +@import "app/manage/styles/forms.css"; +@import "app/manage/styles/locationChooser.css"; +@import "app/manage/styles/manage-user-group.css"; +@import "app/manage/styles/manage-user.css"; +@import "app/manage/styles/related-objects.css"; + +@import "app/navigation/styles/menu.css"; +@import "app/navigation/styles/tabs.css"; +@import "app/navigation/styles/user-menu.css"; + +@import "app/notification/styles/modal.css"; +@import "app/notification/styles/notification.css"; + +@import "app/player/styles/player.css"; +@import "app/player/styles/playerDisplay.css"; +@import "app/player/styles/progressIndicator.css"; +@import "app/player/styles/seek.css"; +@import "app/player/styles/textView.css"; + +@import "app/settings/styles/buttons.css"; +@import "app/settings/styles/connection-list.css"; +@import "app/settings/styles/history-player.css"; +@import "app/settings/styles/history.css"; +@import "app/settings/styles/input-method.css"; +@import "app/settings/styles/mouse-mode.css"; +@import "app/settings/styles/preferences.css"; +@import "app/settings/styles/sessions.css"; +@import "app/settings/styles/settings.css"; +@import "app/settings/styles/user-group-list.css"; +@import "app/settings/styles/user-list.css"; + +@import "app/text-input/styles/textInput.css"; + +@import '@simonwep/pickr/dist/themes/monolith.min.css'; diff --git a/guacamole/src/main/frontend/src/translations/ca.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ca.json similarity index 98% rename from guacamole/src/main/frontend/src/translations/ca.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ca.json index 224702e63e..cd15751c40 100644 --- a/guacamole/src/main/frontend/src/translations/ca.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ca.json @@ -1,7 +1,7 @@ { - + "NAME" : "Catalan", - + "APP" : { "ACTION_ACKNOWLEDGE" : "D'acord", @@ -39,7 +39,7 @@ "ERROR_PASSWORD_BLANK" : "La vostra contrasenya no pot estar en blanc.", "ERROR_PASSWORD_MISMATCH" : "Les contrasenyes proporcionades no coincideixen.", "ERROR_SINGLE_FILE_ONLY" : "Penjeu un sol fitxer cada vegada.", - + "FIELD_HEADER_PASSWORD" : "Contrasenya:", "FIELD_HEADER_PASSWORD_AGAIN" : "Torneu a escriure la contrasenya:", @@ -118,7 +118,7 @@ "INFO_CONNECTION_SHARED" : "Aquesta connexió es comparteix ara.", "INFO_NO_FILE_TRANSFERS" : "No hi ha transferències de fitxers.", "INFO_USER_COUNT" : "{USERNAME}{COUNT, plural, one{} other{ (#)}}", - + "NAME_INPUT_METHOD_NONE" : "Cap", "NAME_INPUT_METHOD_OSK" : "Teclat a la pantalla", "NAME_INPUT_METHOD_TEXT" : "Entrada de text", @@ -217,7 +217,7 @@ "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connexió importada} other{connexions importades}} correctament.", - + "SECTION_HEADER_CONNECTION_IMPORT" : "Importa una connexió", "SECTION_HEADER_CSV" : "Format CSV", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Format de fitxer d'importació de connexions", @@ -250,7 +250,7 @@ "HOME" : { "INFO_NO_RECENT_CONNECTIONS" : "No hi ha connexions recents.", - + "PASSWORD_CHANGED" : "S'ha canviat la contrasenya.", "SECTION_HEADER_ALL_CONNECTIONS" : "Totes les connexions", @@ -296,7 +296,7 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Servidor Remot", "TEXT_CONFIRM_DELETE" : "No es poden restaurar les connexions un cop s'hagin suprimit. Esteu segur que voleu suprimir aquesta connexió?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, @@ -367,13 +367,13 @@ "DIALOG_HEADER_CONFIRM_DELETE" : "Suprimeix el grup", - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Desactivat:", "FIELD_HEADER_USER_GROUP_NAME" : "Nom del grup:", @@ -382,16 +382,16 @@ "HELP_NO_MEMBER_USERS" : "Actualment aquest grup no conté cap usuari. Amplieu aquesta secció per afegir-ne usuaris.", "INFO_READ_ONLY" : "Ho sentim, però no es pot editar aquest grup.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "No hi ha usuaris disponibles.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Edició de grup", "SECTION_HEADER_MEMBER_USERS" : "Usuaris membres", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Grups membres", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Grups pare", "TEXT_CONFIRM_DELETE" : "No es poden restaurar els grups després que se'ls hagi suprimit. Esteu segur que voleu suprimir aquest grup?" @@ -407,7 +407,7 @@ "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{coincidència} other{coincidències}}", "INFO_SEEK_IN_PROGRESS" : "Saltant a la posició demanada. Espereu...", - "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER" + "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@" }, @@ -880,7 +880,7 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Hora d'inici", "TABLE_HEADER_SESSION_USERNAME" : "Nom d'usuari", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, @@ -905,23 +905,23 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirmar nova contrasenya:", "FIELD_HEADER_TIMEZONE" : "Fus horari:", "FIELD_HEADER_USERNAME" : "Nom d'usuari:", - + "HELP_DEFAULT_INPUT_METHOD" : "El mètode d'introducció predeterminat determina com Guacamole rep els esdeveniments del teclat. Canviar aquesta configuració pot ser necessària quan s'utilitza un dispositiu mòbil o quan s'escriu a través d'un IME. Aquesta configuració es pot substituir de manera per connexió dins del menú Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "El mode d'emulació predeterminat del ratolí determina com es comportarà el ratolí remot en les noves connexions pel que fa als tocs. Aquesta configuració es pot substituir de manera per connexió al menú Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Les opcions que apareixen a continuació estan relacionades amb la configuració regional de l'usuari i tindran un impacte sobre la visualització de diverses parts de la interfície.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Si voleu canviar la vostra contrasenya, introduïu la vostra contrasenya actual i la contrasenya desitjada a continuació, i feu clic a \"Actualitza la contrasenya\". El canvi entrarà en vigor immediatament.", "INFO_PASSWORD_CHANGED" : "S'ha canviat la contrasenya.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "S'han guardat els canvis a la configuració.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Mètode d'introducció per defecte", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Mode d'emulació del ratolí predeterminat", @@ -957,22 +957,22 @@ }, "SETTINGS_SESSIONS" : { - + "ACTION_DELETE" : "Tanca sessions", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Tancar sessions", "HELP_SESSIONS" : "Aquesta pàgina s'omple amb les connexions actualment actives. Les connexions enumerades i la possibilitat de tancar-les depèn del nivell d'accés. Si voleu tancar una o més sessions, marqueu la casella que hi ha al costat de les sessions i feu clic a \"Tanca sessions\". Tancant una sessió es desconnectarà immediatament l'usuari de la connexió associada.", - + "INFO_NO_SESSIONS" : "No hi ha sessions actives", "SECTION_HEADER_SESSIONS" : "Sessions actives", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nom de la connexió", "TABLE_HEADER_SESSION_REMOTEHOST" : "Amfitrió remot", "TABLE_HEADER_SESSION_STARTDATE" : "Actiu des de", "TABLE_HEADER_SESSION_USERNAME" : "Nom d'usuari", - + "TEXT_CONFIRM_DELETE" : "Esteu segur que voleu matar totes les sessions seleccionades? Els usuaris que utilitzin aquestes sessions seran desconnectats immediatament." }, diff --git a/guacamole/src/main/frontend/src/translations/cs.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/cs.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/cs.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/cs.json index a465f71b51..75027f0df9 100644 --- a/guacamole/src/main/frontend/src/translations/cs.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/cs.json @@ -1,5 +1,5 @@ { - + "NAME" : "Čeština", "APP" : { @@ -40,7 +40,7 @@ "ERROR_PASSWORD_BLANK" : "Heslo nesmí být prázdné.", "ERROR_PASSWORD_MISMATCH" : "Hesla nesouhlasí.", "ERROR_SINGLE_FILE_ONLY" : "Nahrajte prosím vždy pouze jeden soubor", - + "FIELD_HEADER_PASSWORD" : "Heslo:", "FIELD_HEADER_PASSWORD_AGAIN" : "Heslo znovu:", @@ -59,18 +59,18 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_CLEAR_CLIENT_MESSAGES" : "Vyčistit", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Vyčistit", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Odpojit", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Znovu připojit", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "Kliknutím zobrazíte obsah schránky.", "ACTION_UPLOAD_FILES" : "Nahrát soubory", @@ -117,7 +117,7 @@ "ERROR_UPLOAD_31D" : "V současné době se přenáší příliš mnoho souborů. Počkejte prosím na dokončení probíhajících přenosů a akci opakujte.", "ERROR_UPLOAD_DEFAULT" : "V rámci serveru Guacamole došlo k interní chybě a připojení bylo ukončeno. Pokud problém přetrvává, informujte prosím správce systému nebo zkontrolujte systémové protokoly.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Zde se zobrazí text zkopírovaný / oříznutý v Guacamole. Změny níže uvedeného textu ovlivní vzdálenou schránku.", "HELP_INPUT_METHOD_NONE" : "Není použita žádná metoda vstupu. Vstup z klávesnice je přijímán z připojené fyzické klávesnice.", @@ -149,7 +149,7 @@ "SECTION_HEADER_DISPLAY" : "Zobrazení", "SECTION_HEADER_FILE_TRANSFERS" : "Přenos souborů", "SECTION_HEADER_INPUT_METHOD" : "Metoda vstupu", - + "SECTION_HEADER_MOUSE_MODE" : "Mód emulace myši", "TEXT_ANONYMOUS_USER_JOINED" : "K připojení se připojil anonymní uživatel.", @@ -171,9 +171,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Skrýt", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Zobrazit", "FIELD_HEADER_BACKGROUND" : "Pozadí", @@ -187,15 +187,15 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_BROWSE" : "Vyhledejte soubor", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR:@", "ACTION_VIEW_FORMAT_HELP" : "Zobrazit tipy formátů", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_IMPORT_CONNECTIONS" : "Importovat připojení", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "DIALOG_HEADER_SUCCESS" : "Úspěch", "ERROR_AMBIGUOUS_CSV_HEADER" : "Nejednoznačné záhlaví CSV \"{HEADER}\", může být buď atribut nebo parametr připojení", @@ -223,7 +223,7 @@ "ERROR_REQUIRED_NAME_FILE" : "V zadaném souboru nebyl nalezen žádný název připojení", "ERROR_REQUIRED_PROTOCOL_FILE" : "V zadaném souboru nebyl nalezen žádný protokol připojení", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FIELD_HEADER_EXISTING_CONNECTION_MODE" : "Nahradit/aktualizovat stávající připojení", "FIELD_HEADER_EXISTING_PERMISSION_MODE" : "Obnovit oprávnění", @@ -245,7 +245,7 @@ "HELP_YAML_EXAMPLE" : "---\n - name: prip1\n protocol: vnc\n parameters:\n username: alice\n password: heslo1\n hostname: prip1.web.com\n group: ROOT\n users:\n - guac uživatel 1\n - guac uživatel 2\n groups:\n - Uživatelé Připojení 1\n attributes:\n guacd-encryption: none\n - name: prip2\n protocol: rdp\n parameters:\n username: bob\n password: heslo2\n hostname: prip2.web.com\n group: ROOT/Nadřazená skupina\n users:\n - guac uživatel 1\n attributes:\n guacd-encryption: none\n - name: prip3\n protocol: ssh\n parameters:\n username: karolina\n password: heslo3\n hostname: prip3.web.com\n group: ROOT/Nadřazená skupina/Podřízená skupina\n users:\n - guac uživatel 2\n - guac uživatel 3\n - name: prip4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{připojení} other{připojení}} úspěšně importováno.", - + "SECTION_HEADER_CONNECTION_IMPORT" : "Import připojení", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Formát souboru importu připojení", "SECTION_HEADER_CSV" : "Formát CSV", @@ -275,12 +275,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Žádná nedávná spojení.", - + "PASSWORD_CHANGED" : "Heslo bylo změněno.", "SECTION_HEADER_ALL_CONNECTIONS" : "Všechna spojení", @@ -296,11 +296,11 @@ "LOGIN" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Neplatné přihlašovací jméno", @@ -311,20 +311,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat spojení", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Oblast:", "FIELD_HEADER_NAME" : "Jméno:", "FIELD_HEADER_PROTOCOL" : "Protokol:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Nyní aktivní", @@ -340,20 +340,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Vzdálený host", "TEXT_CONFIRM_DELETE" : "Spojení nemůže být obnoveno poté, co je smazáno. Opravdu chcete smazat toto spojení?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat skupinu spojení", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Lokace:", "FIELD_HEADER_NAME" : "Jméno:", @@ -370,14 +370,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Odstranit profil sdílení", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Jméno:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Primární spojení:", @@ -391,16 +391,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat uživatele", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Spravovat systém:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Změnit vlastní heslo:", @@ -409,11 +409,11 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Vytvořit nové spojení:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Vytvořit nové skupiny připojení:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Vytvořit nový sdílený profil:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Tento uživatel momentálně nepatří do žádné skupiny. Rozbalte tuto sekci, abyste mohli přidát skupiny.", @@ -433,41 +433,41 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Smazat skupinu", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_NAME" : "Jméno skupiny:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Tato skupina momentálně nepatří do žádné skupiny. Rozbalením této sekce ji přidáte do skupiny.", "HELP_NO_MEMBER_USER_GROUPS" : "Tato skupina v současné době neobsahuje žádné skupiny. Rozbalením této sekce ji přidáte do skupiny.", "HELP_NO_MEMBER_USERS" : "Tato skupina v současné době neobsahuje žádné uživatele. Rozbalením této sekce přidáte uživatele.", "INFO_READ_ONLY" : "Je nám líto, ale tuto skupinu nelze upravovat.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Žádní uživatelé nejsou k dispozici.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Upravit skupinu", "SECTION_HEADER_MEMBER_USERS" : "Členský uživatel", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Členské skupiny", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Rodičovské skupiny", "TEXT_CONFIRM_DELETE" : "Skupiny nelze po jejich odstranění obnovit. Chcete opravdu smazat tuto skupinu?" @@ -476,9 +476,9 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "INFO_LOADING_RECORDING" : "Vaše nahrávka se nyní načítá. Čekejte prosím...", "INFO_SEEK_IN_PROGRESS" : "Hledání požadované pozice. Čekejte prosím..." @@ -958,15 +958,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Zde jsou uvedeny záznamy o historii připojení a lze je třídit kliknutím na záhlaví sloupců. Chcete-li vyhledat konkrétní záznamy, zadejte řetězec filtrů a klikněte na tlačítko Hledat. Zobrazí se pouze záznamy, které odpovídají zadanému řetězci filtrů.", @@ -980,25 +980,25 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Doba spuštění", "TABLE_HEADER_SESSION_USERNAME" : "Uživatelské jméno", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_NEW_CONNECTION" : "Nové připojení", "ACTION_NEW_CONNECTION_GROUP" : "Nový skupina", "ACTION_NEW_SHARING_PROFILE" : "Nový sdílený profil", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Toto připojení můžete spravovat klepnutím nebo klepnutím na níže uvedené připojení. V závislosti na vaší úrovni přístupu lze přidávat a mazat připojení a měnit jejich vlastnosti (protokol, název hostitele, port atd.).", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Připojení" @@ -1006,15 +1006,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Jazyk zobrazení:", "FIELD_HEADER_PASSWORD" : "Heslo:", @@ -1023,23 +1023,23 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Potvrďte nové heslo:", "FIELD_HEADER_TIMEZONE" : "Časové pásmo:", "FIELD_HEADER_USERNAME" : "Uživatelské jméno:", - + "HELP_DEFAULT_INPUT_METHOD" : "Výchozí metoda vstupu určuje, jak Guacamole přijímá události klávesnice. Změna tohoto nastavení může být nezbytná při používání mobilního zařízení nebo při psaní přes IME. Toto nastavení může být přepsáno na základě připojení v rámci nabídky Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Výchozí režim emulace myši určuje, jak se bude vzdálená myš chovat v nových spojeních s ohledem na dotyky. Toto nastavení může být přepsáno na základě připojení v rámci nabídky Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Níže uvedené možnosti se vztahují k místu uživatele a ovlivní způsob zobrazení různých částí rozhraní.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Pokud chcete změnit heslo, zadejte své aktuální heslo a níže požadované nové heslo a klikněte na tlačítko „Aktualizovat heslo“. Změna se projeví okamžitě.", "INFO_PASSWORD_CHANGED" : "Heslo bylo změněno.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Uživatelská nastavení uložena.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Výchozí metoda vstupu", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Výchozí mód emulace myši", @@ -1049,14 +1049,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nový uživatel", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Chcete-li spravovat daného uživatele, klepněte na něj nebo klepněte na něj. V závislosti na úrovni přístupu mohou být uživatelé přidáváni a mazáni a jejich hesla mohou být změněna.", @@ -1071,14 +1071,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Nová skupina", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Chcete-li tuto skupinu spravovat, klepněte na ni nebo klepněte na ni. V závislosti na úrovni přístupu lze skupiny přidávat a mazat a jejich členské uživatele a skupiny lze měnit.", @@ -1089,29 +1089,29 @@ }, "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Ukončit sezení", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Ukončit sezení", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Tato stránka bude naplněna aktuálně aktivními připojeními. Uvedená připojení a schopnost zabít tato připojení závisí na úrovni přístupu. Pokud chcete zabít jednu nebo více relací, zaškrtněte políčko vedle těchto relací a klepněte na tlačítko \"Zabít relace\". Zabití relace okamžitě odpojí uživatele od přidruženého připojení.", - + "INFO_NO_SESSIONS" : "Žádné aktivní sezení", "SECTION_HEADER_SESSIONS" : "Aktivní sezení", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Název připojení", "TABLE_HEADER_SESSION_REMOTEHOST" : "Vzdálený host", "TABLE_HEADER_SESSION_STARTDATE" : "Aktivní od", "TABLE_HEADER_SESSION_USERNAME" : "Uživatelské jméno", - + "TEXT_CONFIRM_DELETE" : "Jste si jisti, že chcete ukončit vybrané připojení? Uživatelé využívající toto připojení budou okamžitě odpojeni." }, @@ -1127,15 +1127,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/de.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/de.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/de.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/de.json index 2047bb7aa1..487df9d1b2 100644 --- a/guacamole/src/main/frontend/src/translations/de.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/de.json @@ -1,7 +1,7 @@ { - + "NAME" : "Deutsch", - + "APP" : { "ACTION_ACKNOWLEDGE" : "OK", @@ -39,7 +39,7 @@ "ERROR_PASSWORD_BLANK" : "Das Passwort darf nicht leer sein.", "ERROR_PASSWORD_MISMATCH" : "Die Passwörter stimmen nicht überein.", "ERROR_SINGLE_FILE_ONLY" : "Bitte nur eine Datei zeitgleich hochladen", - + "FIELD_HEADER_PASSWORD" : "Passwort:", "FIELD_HEADER_PASSWORD_AGAIN" : "Wiederhole Passwort:", @@ -58,19 +58,19 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Entferne abgeschlossene Übertragungen", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Trennen", "ACTION_FULLSCREEN" : "Vollbild", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Neu verbinden", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "Klicken um Zwischenablage anzuzeigen.", "ACTION_UPLOAD_FILES" : "Dateien hochladen", @@ -117,7 +117,7 @@ "ERROR_UPLOAD_31D" : "Die maximale Anzahl gleichzeitiger Dateiübertragungen erreicht. Bitte warten Sie bis eine oder mehrere laufende Dateiübertragungen abgeschlossen sind und versuche Sie es dann erneut.", "ERROR_UPLOAD_DEFAULT" : "Die Verbindung wurde aufgrund eines internen Fehlers im Guacamole Server beendet. Sollte dieses Problem weiterhin bestehen informieren Sie Ihren Systemadministrator oder überprüfen Sie die Protokolle.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Kopierter oder ausgeschnittener Text aus Guacamole wird hier angezeigt. Änderungen am Text werden direkt auf die entfernte Zwischenablage angewandt.", "HELP_INPUT_METHOD_NONE" : "Keine Eingabemethode in Verwendung. Tastatureingaben werden von der Hardwaretastatur akzeptiert.", @@ -171,9 +171,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Ausblenden", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Anzeigen", "FIELD_HEADER_BACKGROUND" : "Hintergrund", @@ -187,15 +187,15 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_BROWSE" : "Datei auswählen", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR:@", "ACTION_VIEW_FORMAT_HELP" : "Formatierungshinweise anzeigen", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_IMPORT_CONNECTIONS" : "Verbindungen importieren", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "DIALOG_HEADER_SUCCESS" : "Erfolgreich", "ERROR_AMBIGUOUS_CSV_HEADER" : "Mehrdeutige CSV Kopfzeile \"{HEADER}\" könnte ein Verbindungsattribute oder Parameter sein", @@ -223,7 +223,7 @@ "ERROR_REQUIRED_NAME_FILE" : "Kein Verbindungsnamen gefunden in der verwendeten Datei", "ERROR_REQUIRED_PROTOCOL_FILE" : "Kein Verbindungsprotokoll gefunden in der verwendeten Datei", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FIELD_HEADER_EXISTING_CONNECTION_MODE" : "Vorhandene Verbindungen ersetzen/aktualisieren", "FIELD_HEADER_EXISTING_PERMISSION_MODE" : "Berechtigungen zurücksetzen", @@ -245,7 +245,7 @@ "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connection} other{connections}} erfolgreich importiert.", - + "SECTION_HEADER_CONNECTION_IMPORT" : "Verbindungsimport", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Dateiformat für Verbindungsimport", "SECTION_HEADER_CSV" : "CSV Format", @@ -275,12 +275,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Keine aktiven Verbindungen.", - + "PASSWORD_CHANGED" : "Passwort geändert.", "SECTION_HEADER_ALL_CONNECTIONS" : "Alle Verbindungen", @@ -296,11 +296,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Anmeldungefehler", @@ -311,20 +311,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Verbindung löschen", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Standort:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PROTOCOL" : "Protokoll:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Aktivieren", @@ -340,20 +340,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Entfernter Host", "TEXT_CONFIRM_DELETE" : "Dieser Löschvorgang ist unumkehrbar. Soll diese Verbindung wirklich gelöscht werden?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Lösche Verbindungsgruppe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Standort:", "FIELD_HEADER_NAME" : "Name:", @@ -370,14 +370,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Verteil-Profil löschen", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Primäre Verbindung:", @@ -391,16 +391,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Lösche Benutzer", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administration:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Eigenes Passwort ändern:", @@ -409,12 +409,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Neue Verbindung erstellen:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Neue Verbindungsgruppe erstellen:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Neues Verteil-Profil erstellen:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Login deaktiviert:", "FIELD_HEADER_USERNAME" : "Benutzername:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Dieser Benutzer gehört aktuell keiner Benutzergruppe an. Erweitern Sie diesen Abschnitt, um eine neue Benutzergruppe zu erstellen.", @@ -434,42 +434,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Benutzergrupperuppe löschen", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Deaktiviert:", "FIELD_HEADER_USER_GROUP_NAME" : "Name der Benutzergruppe:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Diese Benutzergruppe gehört keiner Benutzergruppe an. Erweitern Sie diesen Abschnitt um eine neue Benutzergruppe hinzuzufügen.", "HELP_NO_MEMBER_USER_GROUPS" : "Dieser Benutzergruppe gehört keine Benutzergruppe an. Erweitern Sie diesen Abschnitt um eine neue Benutzergruppe hinzuzufügen.", "HELP_NO_MEMBER_USERS" : "Dieser Benutzergruppe gehört kein Benutzer an. Erweitern Sie diesen Abschnitt um einen neuen Benutzer hinzuzufügen.", "INFO_READ_ONLY" : "Diese Benutzergruppe kann nicht bearbeitet werden.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Keine Benutzer verfügbar.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Gruppe bearbeiten", "SECTION_HEADER_MEMBER_USERS" : "Mitglieder einer Benutzergruppe", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Benutzergruppen", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Übergeordnete Benutzergruppen", "TEXT_CONFIRM_DELETE" : "Dieser Löschvorgang ist unumkehrbar. Möchten Sie diese Benutzergruppe wirklich löschen?" @@ -478,9 +478,9 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "ACTION_SHOW_KEY_LOG" : "Tastenanschlag-Protokoll", "INFO_LOADING_RECORDING" : "Aufnahme wird geladen. Bitte warten...", @@ -488,7 +488,7 @@ "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Match} other{Matches}}", "INFO_SEEK_IN_PROGRESS" : "Spule zu gewünschten Stelle. Bitte warten...", - "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER" + "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@" }, @@ -949,15 +949,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Die letzten Verbindungen werden hier historisch aufgelistet und können durch Klicken auf die Spaltenüberschriften sortiert werden. Zum Aufsuchen von bestimmten Datensätzen, geben Sie eine Filterzeichenfolge ein und klicken Sie auf \"Suchen\". Nur Datensätze, die die vorgesehenen Filterzeichenfolge entsprechen, werden aufgelistet.", @@ -971,25 +971,25 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Sitzung aktiv seit", "TABLE_HEADER_SESSION_USERNAME" : "Benutzername", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_NEW_CONNECTION" : "Neue Verbindung", "ACTION_NEW_CONNECTION_GROUP" : "Neue Verbindungsgruppe", "ACTION_NEW_SHARING_PROFILE" : "Neues Verteil-Profil", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Klicken Sie auf eine Verbindung um diese zu verwalten. Abhängig von Ihrer Zugriffsebene können Verbindungen hinzugefügt, gelöscht oder Parameter (Protokoll, Hostname, Port, etc.) geändert werden.", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Verbindungen" @@ -997,15 +997,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Anzeigesprache:", "FIELD_HEADER_PASSWORD" : "Passwort:", @@ -1014,23 +1014,23 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Passwort wiederholen:", "FIELD_HEADER_TIMEZONE" : "Zeitzone:", "FIELD_HEADER_USERNAME" : "Benutzername:", - + "HELP_DEFAULT_INPUT_METHOD" : "Die Standardeingabemethode bestimmt wie Tastaturereignisse an Guacamole weitergeleitet werden. Eine Änderung dieser Einstellung kann erforderlich sein, wenn ein mobiles Gerät verwendet wird oder bei der Eingabe durch einen IME. Dieses Verhalten kann im Menü innerhalb der Guacamole Verbindung geändert werden.", "HELP_DEFAULT_MOUSE_MODE" : "Der Standard Mausemulationsmodus bestimmt wie sich die entfernte Maus bei Touchpad Berührungen verhält. Dieses Verhalten kann im Menü innerhalb der Guacamole Verbindung geändert werden.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Um die Spracheinstellungen von Guacamole zu ändern, wählen Sie eine der verfügbaren Sprachen.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Wenn Sie das Passwort ändern wollen, geben Sie das aktuelle und das gewünschte Passwort ein und klicken Sie auf \"Ändere Passwort\". Die Änderung wird sofort wirksam.", "INFO_PASSWORD_CHANGED" : "Passwort geändert.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Benutzereinstellungen gespeichert.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Standard Eingabemethode", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Standard Mausemulationsmodus", @@ -1040,14 +1040,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Neuer Benutzer", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Klicken Sie auf einen der unten stehenden Benutzer, um diesen zu modifizieren. Je nach Berechtigungsstufe können Sie Benutzer hinzufügen, bearbeiten, löschen oder die Passwörter ändern.", @@ -1062,14 +1062,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Neue Gruppe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Klicken Sie auf eine der unten stehenden Gruppen, um diese zu modifizieren. Je nach Berechtigungsstufe können Sie Gruppen und deren Mitglieder hinzufügen, bearbeiten oder löschen.", @@ -1080,29 +1080,29 @@ }, "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Beende Sitzung", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Beende Sitzung", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Diese Seite wird mit derzeit aktiven Verbindungen gefüllt. Die aufgelisteten Verbindungen und die Möglichkeit, diese Verbindungen zu beenden, hängen von Ihrer Zugriffsebene ab. Wenn Sie eine oder mehrere Sitzungen beenden wollen, wählen Sie diese Sitzung durch Aktivierung der nebenstehende Box und klicken auf \"Beende Sitzung\". Beendung einer Sitzung trennt den Benutzer von dessen Verbindung unverzüglich.", - + "INFO_NO_SESSIONS" : "Keine aktiven Sitzungen", "SECTION_HEADER_SESSIONS" : "Aktive Sitzungen", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Verbindungsname", "TABLE_HEADER_SESSION_REMOTEHOST" : "Entfernter Host", "TABLE_HEADER_SESSION_STARTDATE" : "Sitzung aktiv seit", "TABLE_HEADER_SESSION_USERNAME" : "Benutzername", - + "TEXT_CONFIRM_DELETE" : "Sind Sie sicher, dass alle ausgewählten Sitzungen beendet werden sollen? Die Benutzer dieser Sitzungen werden unverzüglich getrennt." }, @@ -1118,15 +1118,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/en.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/en.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/en.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/en.json index b79f6184e4..c1e7cd645d 100644 --- a/guacamole/src/main/frontend/src/translations/en.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/en.json @@ -1,7 +1,7 @@ { - + "NAME" : "English", - + "APP" : { "NAME" : "Apache Guacamole", @@ -42,7 +42,7 @@ "ERROR_PASSWORD_BLANK" : "Your password cannot be blank.", "ERROR_PASSWORD_MISMATCH" : "The provided passwords do not match.", "ERROR_SINGLE_FILE_ONLY" : "Please upload only a single file at a time", - + "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_AGAIN" : "Re-enter Password:", "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "Allow writing to existing recording file:", @@ -63,19 +63,19 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR:@", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Disconnect", "ACTION_FULLSCREEN" : "Fullscreen", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Reconnect", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "Click to view clipboard contents.", "ACTION_UPLOAD_FILES" : "Upload Files", @@ -122,7 +122,7 @@ "ERROR_UPLOAD_31D" : "Too many files are currently being transferred. Please wait for existing transfers to complete, and then try again.", "ERROR_UPLOAD_DEFAULT" : "An internal error has occurred within the Guacamole server, and the connection has been terminated. If the problem persists, please notify your system administrator, or check your system logs.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Text copied/cut within Guacamole will appear here. Changes to the text below will affect the remote clipboard.", "HELP_INPUT_METHOD_NONE" : "No input method is used. Keyboard input is accepted from a connected, physical keyboard.", @@ -154,7 +154,7 @@ "SECTION_HEADER_DISPLAY" : "Display", "SECTION_HEADER_FILE_TRANSFERS" : "File Transfers", "SECTION_HEADER_INPUT_METHOD" : "Input method", - + "SECTION_HEADER_MOUSE_MODE" : "Mouse emulation mode", "TEXT_ANONYMOUS_USER_JOINED" : "An anonymous user has joined the connection.", @@ -176,9 +176,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Hide", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Show", "FIELD_HEADER_BACKGROUND" : "Background", @@ -192,15 +192,15 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_BROWSE" : "Browse for File", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR:@", "ACTION_VIEW_FORMAT_HELP" : "View Format Tips", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_IMPORT_CONNECTIONS" : "Import Connections", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "DIALOG_HEADER_SUCCESS" : "Success", "ERROR_AMBIGUOUS_CSV_HEADER" : "Ambiguous CSV Header \"{HEADER}\" could be either a connection attribute or parameter", @@ -228,7 +228,7 @@ "ERROR_REQUIRED_NAME_FILE" : "No connection name found in the provided file", "ERROR_REQUIRED_PROTOCOL_FILE" : "No connection protocol found in the provided file", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FIELD_HEADER_EXISTING_CONNECTION_MODE" : "Replace/Update existing connections", "FIELD_HEADER_EXISTING_PERMISSION_MODE" : "Reset permissions", @@ -250,7 +250,7 @@ "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connection} other{connections}} imported successfully.", - + "SECTION_HEADER_CONNECTION_IMPORT" : "Connection Import", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Connection Import File Format", "SECTION_HEADER_CSV" : "CSV Format", @@ -280,12 +280,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "No recent connections.", - + "PASSWORD_CHANGED" : "Password changed.", "SECTION_HEADER_ALL_CONNECTIONS" : "All Connections", @@ -301,11 +301,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Invalid Login", @@ -316,20 +316,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PROTOCOL" : "Protocol:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Active Now", @@ -345,20 +345,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Remote Host", "TEXT_CONFIRM_DELETE" : "Connections cannot be restored after they have been deleted. Are you sure you want to delete this connection?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Location:", "FIELD_HEADER_NAME" : "Name:", @@ -375,14 +375,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Sharing Profile", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Name:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Primary Connection:", @@ -396,16 +396,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete User", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administer system:", "FIELD_HEADER_AUDIT_SYSTEM" : "Audit system:", @@ -415,12 +415,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Create new connections:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Create new connection groups:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Create new sharing profiles:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Login disabled:", "FIELD_HEADER_USERNAME" : "Username:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "This user does not currently belong to any groups. Expand this section to add groups.", @@ -440,43 +440,43 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Group", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_AUDIT_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_AUDIT_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_AUDIT_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_AUDIT_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Disabled:", "FIELD_HEADER_USER_GROUP_NAME" : "Group name:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "This group does not currently belong to any groups. Expand this section to add groups.", "HELP_NO_MEMBER_USER_GROUPS" : "This group does not currently contain any groups. Expand this section to add groups.", "HELP_NO_MEMBER_USERS" : "This group does not currently contain any users. Expand this section to add users.", "INFO_READ_ONLY" : "Sorry, but this group cannot be edited.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "No users available.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Edit Group", "SECTION_HEADER_MEMBER_USERS" : "Member Users", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Member Groups", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Parent Groups", "TEXT_CONFIRM_DELETE" : "Groups cannot be restored after they have been deleted. Are you sure you want to delete this group?" @@ -485,9 +485,9 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "ACTION_SHOW_KEY_LOG" : "Keystroke Log", "INFO_FRAME_EVENTS_LEGEND" : "On-screen Activity", @@ -497,7 +497,7 @@ "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Match} other{Matches}}", "INFO_SEEK_IN_PROGRESS" : "Seeking to the requested position. Please wait...", - "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER" + "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@" }, @@ -522,7 +522,7 @@ "FIELD_HEADER_POD" : "Pod name:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:", @@ -531,7 +531,7 @@ "FIELD_HEADER_SCROLLBACK" : "Maximum scrollback size:", "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_USE_SSL" : "Use SSL/TLS", "FIELD_OPTION_BACKSPACE_EMPTY" : "", @@ -626,7 +626,7 @@ "FIELD_HEADER_PRECONNECTION_BLOB" : "Preconnection BLOB (VM ID):", "FIELD_HEADER_PRECONNECTION_ID" : "RDP source ID:", "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:", "FIELD_HEADER_RECORDING_EXCLUDE_TOUCH" : "Exclude touch events:", @@ -753,7 +753,7 @@ "FIELD_HEADER_PUBLIC_KEY" : "Public key:", "FIELD_HEADER_SCROLLBACK" : "Maximum scrollback size:", "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:", @@ -768,7 +768,7 @@ "FIELD_HEADER_TIMEZONE" : "Time zone ($TZ):", "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Broadcast address for WoL packet:", "FIELD_HEADER_WOL_MAC_ADDR" : "MAC address of the remote host:", "FIELD_HEADER_WOL_SEND_PACKET" : "Send WoL packet:", @@ -843,7 +843,7 @@ "FIELD_HEADER_PASSWORD_REGEX" : "Password regular expression:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:", @@ -854,7 +854,7 @@ "FIELD_HEADER_TIMEOUT" : "Connection timeout:", "FIELD_HEADER_TYPESCRIPT_NAME" : "Typescript name:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Typescript path:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Broadcast address for WoL packet:", "FIELD_HEADER_WOL_MAC_ADDR" : "MAC address of the remote host:", "FIELD_HEADER_WOL_SEND_PACKET" : "Send WoL packet:", @@ -932,7 +932,7 @@ "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_QUALITY_LEVEL" : "Display quality:", "FIELD_HEADER_READ_ONLY" : "Read-only:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclude mouse:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclude graphics/streams:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Include key events:", @@ -1021,15 +1021,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "History records for past connections are listed here and can be sorted by clicking the column headers. To search for specific records, enter a filter string and click \"Search\". Only records which match the provided filter string will be listed.", @@ -1043,25 +1043,25 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Start time", "TABLE_HEADER_SESSION_USERNAME" : "Username", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_NEW_CONNECTION" : "New Connection", "ACTION_NEW_CONNECTION_GROUP" : "New Group", "ACTION_NEW_SHARING_PROFILE" : "New Sharing Profile", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Click or tap on a connection below to manage that connection. Depending on your access level, connections can be added and deleted, and their properties (protocol, hostname, port, etc.) can be changed.", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Connections" @@ -1069,15 +1069,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Display language:", "FIELD_HEADER_NUMBER_RECENT_CONNECTIONS" : "Number of Recent Connections to show:", @@ -1088,24 +1088,24 @@ "FIELD_HEADER_SHOW_RECENT_CONNECTIONS" : "Display Recent Connections:", "FIELD_HEADER_TIMEZONE" : "Timezone:", "FIELD_HEADER_USERNAME" : "Username:", - + "HELP_APPEARANCE" : "Here you can enable or disable whether the \"Recent Connections\" section is shown on the home screen, and adjust the number of recent connections included.", "HELP_DEFAULT_INPUT_METHOD" : "The default input method determines how keyboard events are received by Guacamole. Changing this setting may be necessary when using a mobile device, or when typing through an IME. This setting can be overridden on a per-connection basis within the Guacamole menu.", "HELP_DEFAULT_MOUSE_MODE" : "The default mouse emulation mode determines how the remote mouse will behave in new connections with respect to touches. This setting can be overridden on a per-connection basis within the Guacamole menu.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Options below are related to the locale of the user and will impact how various parts of the interface are displayed.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "If you wish to change your password, enter your current password and the desired new password below, and click \"Update Password\". The change will take effect immediately.", "INFO_PASSWORD_CHANGED" : "Password changed.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "User settings saved.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_APPEARANCE" : "Appearance", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Default Input Method", @@ -1116,14 +1116,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "New User", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Click or tap on a user below to manage that user. Depending on your access level, users can be added and deleted, and their passwords can be changed.", @@ -1138,14 +1138,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "New Group", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Click or tap on a group below to manage that group. Depending on your access level, groups can be added and deleted, and their member users and groups can be changed.", @@ -1156,29 +1156,29 @@ }, "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Kill Sessions", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Kill Sessions", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "This page will be populated with currently-active connections. The connections listed and the ability to kill those connections is dependent upon your access level. If you wish to kill one or more sessions, check the box next to those sessions and click \"Kill Sessions\". Killing a session will immediately disconnect the user from the associated connection.", - + "INFO_NO_SESSIONS" : "No active sessions", "SECTION_HEADER_SESSIONS" : "Active Sessions", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Connection name", "TABLE_HEADER_SESSION_REMOTEHOST" : "Remote host", "TABLE_HEADER_SESSION_STARTDATE" : "Active since", "TABLE_HEADER_SESSION_USERNAME" : "Username", - + "TEXT_CONFIRM_DELETE" : "Are you sure you want to kill all selected sessions? The users using these sessions will be immediately disconnected." }, @@ -1194,15 +1194,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/es.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/es.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/es.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/es.json index c4f8dbe909..51991179c2 100644 --- a/guacamole/src/main/frontend/src/translations/es.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/es.json @@ -49,17 +49,17 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Limpiar", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Disconnect", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Reconectar", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_UPLOAD_FILES" : "Subir ficheros", "DIALOG_HEADER_CONNECTING" : "Conectando", @@ -105,7 +105,7 @@ "ERROR_UPLOAD_31D" : "Se estan transfiriendo muchos ficheros actualmente. Por favor espere a que finalicen las transferencias de fichero existentes e intente de nuevo.", "ERROR_UPLOAD_DEFAULT" : "Ha ocurrido un error interno en el servidor Guacamole y la conexión ha finalizado. Si el problema persiste, por favor notifíquelo al administrador o verifique los registros del sistema.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Aquí aparecerá el texto copiado/cortado en Guacamole. Los cambios en el texto de abajo afectaran al portapapeles remoto.", "HELP_INPUT_METHOD_NONE" : "No se está usando un método de entrada. La entrada de teclado se acepta desde un teclado físico conectado.", @@ -151,9 +151,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Ocultar", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Mostrar", "FIELD_HEADER_BACKGROUND" : "Fondo", @@ -181,9 +181,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Sin conexiones recientes.", @@ -202,11 +202,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Inicio de sesión inválido", @@ -217,20 +217,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Borrar conexión", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Ubicación:", "FIELD_HEADER_NAME" : "Nombre:", "FIELD_HEADER_PROTOCOL" : "Protocolo:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Activo Ahora", @@ -246,20 +246,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Host Remoto", "TEXT_CONFIRM_DELETE" : "Las conexiones no se pueden restaurar despues de haberlas eliminado. ¿Está seguro que quiere eliminar esta conexión?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Borrar Grupo de conexiones", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Ubicación:", "FIELD_HEADER_NAME" : "Nombre:", @@ -276,14 +276,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Eliminar perfil de compartir", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Nombre:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Conexión primaria:", @@ -297,16 +297,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Eliminar Usuario", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administrar sistema:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Cambiar contraseña:", @@ -315,12 +315,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Crear nuevas conexiones:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Crear nuevos grupos de conexión:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Crear nuevos perfiles de compartir:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Inicio de sesión deshabilitado:", "FIELD_HEADER_USERNAME" : "Nombre de usuario:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Este usuario no pertenece actualmente a ningún grupo. Expanda esta sección para agregar grupos.", @@ -340,42 +340,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Eliminar grupo", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Deshabilitado:", "FIELD_HEADER_USER_GROUP_NAME" : "Nombre del grupo:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Este grupo no pertenece actualmente a ningún grupo. Expanda esta sección para agregar grupos.", "HELP_NO_MEMBER_USER_GROUPS" : "Este grupo no contiene actualmente ningun grupo. Expanda esta sección para agregar grupos.", "HELP_NO_MEMBER_USERS" : "Este grupo no contiene actualmente ningun usuario. Expanda esta sección para agregar usuarios.", "INFO_READ_ONLY" : "Lo siento, este grupo no se puede editar.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "No hay usuarios disponibles.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Editar grupo", "SECTION_HEADER_MEMBER_USERS" : "Usuarios asociados", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Grupos asociados", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Grupo principal", "TEXT_CONFIRM_DELETE" : "Los grupos no se pueden restaurar despues de haberlos eliminado. ¿Esta seguro de querer eliminar este grupo?" @@ -842,14 +842,14 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Aquí se detalla el historial de las últimas conexiones y se pueden ordernar haciendo clic en los encabezados de la columna. Para buscar un registro específico, introduzca la cadena de texto a filtrar y haga clic en \"Buscar\". Solo se listaran los registros que coincidan con el filtro introducido.", @@ -862,24 +862,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Activo Desde", "TABLE_HEADER_SESSION_USERNAME" : "Usuario", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Nueva Conexión", "ACTION_NEW_CONNECTION_GROUP" : "Nuevo Grupo", "ACTION_NEW_SHARING_PROFILE" : "Nuevo perfil de compartir", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Haga clic o toque en una de las conexiones de abajo para gestionar esa conexión. Dependiendo de su nivel de acceso, podrá añadir/borrar conexiones y cambiar sus propiedades (Protocolo, Nombre de Host, Puerto, etc.) .", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Conexiones" @@ -887,14 +887,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Idioma para mostrar:", "FIELD_HEADER_PASSWORD" : "Contraseña:", @@ -906,19 +906,19 @@ "HELP_DEFAULT_INPUT_METHOD" : "El método de entrada por defecto determina como se reciben en Guacamole los eventos de teclado. Es posible que sea necesario cambiar esta configuración cuando se usa un dispositivo móvil, o cuando se escribe a través de un método de entrada. Esta configuración se puede cambiar tambien en cada conexión desde el menú de Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "El modo de emulación de ratón por defecto determina como se comportará el ratón remoto en nuevas conexiones con respecto a los toques. Esta configuración se puede cambiar tambien en cada conexión desde el menú de Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Las siguientes opciones están relacionadas con la configuración regional del usuario e impactarán en cómo se muestran varias partes de la interfaz.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Si quiere cambiar su contraseña, introduzca abajo su contraseña actual y la nueva contraseña deseada y haga clic en \"Actualizar Contraseña\". El cambio será efectivo inmediatamente.", "INFO_PASSWORD_CHANGED" : "Contraseña Cambiada.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Método de entrada por defecto", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Modo de emulación de ratón por Defecto", @@ -928,14 +928,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nuevo Usuario", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Haga Clic o toque un usuario de la lista inferior para gestionar ese usuario. Dependiendo de su nivel de acceso, podrá añadir/borrar usuarios y cambiar sus contraseñas.", @@ -950,14 +950,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Nuevo Grupo", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Haga clic o toque un grupo de la lista inferior para gestionar ese grupo. Dependiendo de su nivel de acceso, podrá agregar/borrar grupos y cambiar los miembros y grupos del mismo.", @@ -970,16 +970,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Finalizar Sesiones", "DIALOG_HEADER_CONFIRM_DELETE" : "Finalizar Sesiones", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "En esta página se mostrarán las conexiones que estan activas actualmente. Las conexiones enumeradas y la capacidad de eliminar esas conexiones dependen de su nivel de acceso. Si quiere finalizar una o mas sesiones, marque la casilla correspondiente a esa/s sesión/es y haga clic en \"Finalizar Sesiones\". Si finaliza una sesión desconectará inmediatamente al usuario de la conexión asociada.", @@ -1007,15 +1007,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/fr.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/fr.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/fr.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/fr.json index b8915c631d..b0b00ceee5 100644 --- a/guacamole/src/main/frontend/src/translations/fr.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/fr.json @@ -60,19 +60,19 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR:@", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Déconnecter", "ACTION_FULLSCREEN" : "Plein écran", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Reconnecter", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "Cliquez pour afficher le contenu du presse-papiers.", "ACTION_UPLOAD_FILES" : "Envoyer Fichiers", @@ -119,7 +119,7 @@ "ERROR_UPLOAD_31D" : "Trop de fichiers sont actuellement transférés. Merci d'attendre que les transferts en cours soient terminés et de réessayer plus tard.", "ERROR_UPLOAD_DEFAULT" : "Une erreur interne est apparue dans le serveur Guacamole et la connexion a été fermée. Si le problème persiste, merci de notifier l'administrateur ou de regarder les journaux système.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Texte copié/coupé dans Guacamole apparaîtra ici. Changer le texte ci-dessous affectera le presse-papiers distant.", "HELP_INPUT_METHOD_NONE" : "Aucune méthode de saisie utilisée. Clavier accepté depuis un clavier physique connecté.", @@ -173,9 +173,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Masquer", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Afficher", "FIELD_HEADER_BACKGROUND" : "Fond", @@ -189,17 +189,17 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_BROWSE" : "Parcourir les fichiers", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR:@", "ACTION_VIEW_FORMAT_HELP" : "Voir les conseils de format", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_IMPORT_CONNECTIONS" : "Importer les connexions", - - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "DIALOG_HEADER_SUCCESS" : "Succès", - + "ERROR_AMBIGUOUS_CSV_HEADER" : "L'en-tête CSV ambigu \"{HEADER}\" peut être soit un attribut de connexion, soit un paramètre", "ERROR_AMBIGUOUS_PARENT_GROUP" : "Le groupe et le parentIdentifier peuvent ne pas être spécifiés en même temps", "ERROR_ARRAY_REQUIRED" : "Le fichier fourni doit contenir une liste de connexions", @@ -224,12 +224,12 @@ "ERROR_REQUIRED_PROTOCOL_CONNECTION" : "Le protocole de connexion est requis", "ERROR_REQUIRED_NAME_FILE" : "Aucun nom de connexion trouvé dans le fichier fourni", "ERROR_REQUIRED_PROTOCOL_FILE" : "Aucun protocole de connexion trouvé dans le fichier fourni", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + "FIELD_HEADER_EXISTING_CONNECTION_MODE" : "Remplacer/Mettre à jour les connexions existantes", "FIELD_HEADER_EXISTING_PERMISSION_MODE" : "Réinitialiser les permissions", - + "HELP_CSV_DESCRIPTION" : "Un fichier CSV d'importation de connexion a un enregistrement de connexion par ligne. Chaque colonne spécifie un champ de connexion. Au minimum, le nom de la connexion et le protocole doivent être spécifiés.", "HELP_CSV_EXAMPLE" : "name,protocol,username,password,hostname,group,users,groups,guacd-encryption (attribute)\nconn1,vnc,alice,pass1,conn1.web.com,ROOT,guac user 1;guac user 2,Connection 1 Users,none\nconn2,rdp,bob,pass2,conn2.web.com,ROOT/Parent Group,guac user 1,,ssl\nconn3,ssh,carol,pass3,conn3.web.com,ROOT/Parent Group/Child Group,guac user 2;guac user 3,,\nconn4,kubernetes,,,,,,,", "HELP_CSV_MORE_DETAILS" : "L'en-tête CSV de chaque ligne spécifie le champ de connexion. L'identifiant de groupe de connexion dans lequel la connexion doit être importée peut être directement spécifié avec \"parentIdentifier\", ou le chemin vers le groupe parent peut être spécifié en utilisant \"group\" comme indiqué ci-dessous. Dans la plupart des cas, il ne devrait pas y avoir de conflit entre les champs, mais si nécessaire, un suffixe \" (attribute)\" ou \" (parameter)\" peut être ajouté pour dissocier. Les listes d'identifiants d'utilisateur ou de groupe d'utilisateurs doivent être séparées par des points-virgules.¹", @@ -245,22 +245,22 @@ "HELP_UPLOAD_FILE_TYPES" : "CSV, JSON ou YAML", "HELP_YAML_DESCRIPTION" : "Un fichier YAML d'importation de connexion est une liste d'objets de connexion avec exactement la même structure que le format JSON.", "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n username: alice\n password: pass1\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n username: bob\n password: pass2\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n username: carol\n password: pass3\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", - + "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} {NUMBER, plural, one{connexion} other{connexions}} importée(s) avec succès.", - + "SECTION_HEADER_CONNECTION_IMPORT" : "Importation de connexion", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "Format de fichier d'importation de connexion", "SECTION_HEADER_CSV" : "Format CSV", "SECTION_HEADER_JSON" : "Format JSON", "SECTION_HEADER_YAML" : "Format YAML", - + "TABLE_HEADER_ERRORS" : "Erreurs", "TABLE_HEADER_GROUP" : "Groupe", "TABLE_HEADER_NAME" : "Nom", "TABLE_HEADER_PROTOCOL" : "Protocole", "TABLE_HEADER_ROW_NUMBER" : "Ligne #" }, - + "DATA_SOURCE_DEFAULT" : { "NAME" : "Standard (XML)" }, @@ -277,9 +277,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Pas de connexion récente.", @@ -298,11 +298,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Identifiant Incorrect", @@ -313,11 +313,11 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Connexion", "DIALOG_HEADER_ERROR" : "Erreur", @@ -326,7 +326,7 @@ "FIELD_HEADER_NAME" : "Nom:", "FIELD_HEADER_PROTOCOL" : "Protocole:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Active", @@ -342,17 +342,17 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Hôte distant", "TEXT_CONFIRM_DELETE" : "Les connexions ne pourront être restaurées une fois supprimées. Êtes-vous certains de vouloir supprimer cette connexion ?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Groupe de Connexion", "DIALOG_HEADER_ERROR" : "Erreur", @@ -372,14 +372,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer le Profil de Partage", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Nom:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Connexion Primaire:", @@ -393,16 +393,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Utilisateur", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administration du système:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Modifier son propre mot de passe:", @@ -411,12 +411,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Créer de nouvelles connexions:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Créer de nouveaux groupes de connexion:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Créer de nouveaux profils de partage:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Connexion désactivée:", "FIELD_HEADER_USERNAME" : "Identifiant:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Cet utilisateur n'appartient à aucun groupe. Développer cette section pour ajouter des groupes..", @@ -436,42 +436,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Supprimer Groupe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Désactivé:", "FIELD_HEADER_USER_GROUP_NAME" : "Nom Groupe:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Ce groupe n'appartient actuellement à aucun groupe. Développez cette section pour ajouter des groupes.", "HELP_NO_MEMBER_USER_GROUPS" : "Ce groupe n'appartient actuellement à aucun groupe. Développez cette section pour ajouter des groupes.", "HELP_NO_MEMBER_USERS" : "Ce groupe ne contient actuellement aucun utilisateur. Développez cette section pour ajouter des utilisateurs.", "INFO_READ_ONLY" : "Désolé, mais ce groupe ne peut pas être modifié.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Pas d'utilisateur disponible.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Modifier Groupe", "SECTION_HEADER_MEMBER_USERS" : "Utilisateurs Membre", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Groupes Membre", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Groupes Parent", "TEXT_CONFIRM_DELETE" : "Les groupes ne peuvent pas être restaurés après leur suppression. Êtes-vous certains de vouloir supprimer ce groupe ?" @@ -480,21 +480,21 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "ACTION_SHOW_KEY_LOG" : "Journal des frappes", - + "INFO_FRAME_EVENTS_LEGEND" : "Activité à l'écran", "INFO_KEY_EVENTS_LEGEND" : "Activité du clavier", "INFO_LOADING_RECORDING" : "Votre enregistrement est en cours de chargement. Veuillez patienter...", "INFO_NO_KEY_LOG" : "Journal des frappes non disponible", "INFO_NUMBER_OF_RESULTS" : "{RESULTS} {RESULTS, plural, one{Résultat} other{Résultats}}", "INFO_SEEK_IN_PROGRESS" : "Recherche de la position demandée. Veuillez patienter...", - - "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER" - - }, + + "FIELD_PLACEHOLDER_TEXT_BATCH_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@" + + }, "PROTOCOL_KUBERNETES" : { @@ -517,7 +517,7 @@ "FIELD_HEADER_POD" : "Nom du pod:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclure la souris:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclure les graphiques/flux:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Inclure les évenements clavier:", @@ -526,7 +526,7 @@ "FIELD_HEADER_SCROLLBACK" : "Taille maximum du défilement arrière:", "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Chemin Typescript:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_USE_SSL" : "Utiliser SSL/TLS", "FIELD_OPTION_BACKSPACE_EMPTY" : "", @@ -621,7 +621,7 @@ "FIELD_HEADER_PRECONNECTION_BLOB" : "Préconnexion BLOB (VM ID):", "FIELD_HEADER_PRECONNECTION_ID" : "Source RDP ID:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclure la souris:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclure les graphiques/flux:", "FIELD_HEADER_RECORDING_EXCLUDE_TOUCH" : "Exclure les événements tactiles:", @@ -748,7 +748,7 @@ "FIELD_HEADER_PUBLIC_KEY" : "Clé publique:", "FIELD_HEADER_SCROLLBACK" : "Taille maximum du défilement arrière:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclure la souris:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclure les graphiques/flux:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Inclure les événements clavier:", @@ -763,7 +763,7 @@ "FIELD_HEADER_TIMEZONE" : "Fuseau horaire ($TZ):", "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Chemin Typescript:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:", "FIELD_HEADER_WOL_MAC_ADDR" : "Adresse MAC de l'hôte distant:", "FIELD_HEADER_WOL_SEND_PACKET" : "Envoi de paquets WoL:", @@ -838,7 +838,7 @@ "FIELD_HEADER_PASSWORD_REGEX" : "Expression régulière Mot de passe:", "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclure la souris:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclure les graphiques/flux:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Inclure les événements clavier:", @@ -849,7 +849,7 @@ "FIELD_HEADER_TIMEOUT" : "Délai d'expiration de la connexion:", "FIELD_HEADER_TYPESCRIPT_NAME" : "Nom Typescript:", "FIELD_HEADER_TYPESCRIPT_PATH" : "Chemin Typescript:", - "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING", + "FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING" : "@:APP.FIELD_HEADER_TYPESCRIPT_WRITE_EXISTING:@", "FIELD_HEADER_WOL_BROADCAST_ADDR" : "Adresse de diffusion pour les paquets WoL:", "FIELD_HEADER_WOL_MAC_ADDR" : "Adresse MAC de l'hôte distant:", "FIELD_HEADER_WOL_SEND_PACKET" : "Envoi de paquets WoL:", @@ -927,7 +927,7 @@ "FIELD_HEADER_PORT" : "Port:", "FIELD_HEADER_QUALITY_LEVEL" : "Qualité d'affichage:", "FIELD_HEADER_READ_ONLY" : "Lecture seule:", - "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING", + "FIELD_HEADER_RECORDING_WRITE_EXISTING" : "@:APP.FIELD_HEADER_RECORDING_WRITE_EXISTING:@", "FIELD_HEADER_RECORDING_EXCLUDE_MOUSE" : "Exclure la souris:", "FIELD_HEADER_RECORDING_EXCLUDE_OUTPUT" : "Exclure les graphiques/flux:", "FIELD_HEADER_RECORDING_INCLUDE_KEYS" : "Inclure les événements clavier:", @@ -992,15 +992,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "L'historique des dernières connexions est répertorié ici et peut être trié en cliquant sur l'en-tête des colonnes. Pour rechercher des enregistrements spécifiques, entrez un filtre et cliquez sur \"Rechercher\". Seuls les enregistrements correspondant au filtre renseigné seront listés.", @@ -1014,25 +1014,25 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Heure de début", "TABLE_HEADER_SESSION_USERNAME" : "Identifiant", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_NEW_CONNECTION" : "Nouvelle Connexion", "ACTION_NEW_CONNECTION_GROUP" : "Nouveau Groupe", "ACTION_NEW_SHARING_PROFILE" : "Nouveau Profil de Partage", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Cliquer ou appuyer sur une connexion en dessous pour la gérer. Selon vos permissions, les connexions peuvent être ajoutées, supprimées, leur propriétés (protocole, nom d'hôte, port, etc) changées.", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Connexions" @@ -1040,15 +1040,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_PASSWORD_BLANK" : "Votre mot de passe ne peut être vide.", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Langue affichée:", "FIELD_HEADER_NUMBER_RECENT_CONNECTIONS" : "Nombre de connexions récentes à afficher:", @@ -1063,20 +1063,20 @@ "HELP_APPEARANCE" : "Vous pouvez ici activer ou désactiver les connexions récentes sur la page d'accueil de Guacamole, et ajuster le nombre de connexions récentes qui seront affichées.", "HELP_DEFAULT_INPUT_METHOD" : "La méthode de saisie par défaut détermine comment les événements clavier sont reçus par Guacamole. Modifier ce paramètre peut être nécessaire lors de l'utilisation d'un appareil mobile. Ce paramètre peut être remplacé pour chaque connexion dans le menu de Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Le mode d'émulation de la souris par défaut détermine le comportement de la souris distante dans de nouvelles connexions par rapport aux touches. Ce paramètre peut être remplacé pour chaque connexion dans le menu Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Les options ci-dessous sont liées à la localisation de l'utilisateur et auront un impact sur l'affichage de différentes parties de l'interface.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Si vous souhaitez changer votre mot de passe, saisissez votre mot de passe actuel ainsi que le nouveau mot de passe souhaité ci-dessous, puis cliquez sur \"Mettre à jour Mot de passe\". Le changement prendra effet immédiatement.", "INFO_PASSWORD_CHANGED" : "Mot de passe modifié.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Paramètres utilisateur enregistrés.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_APPEARANCE" : "Apparence", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Méthode de saisie par défaut", @@ -1087,14 +1087,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nouvel Utilisateur", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Cliquez ou appuyez sur un utilisateur en dessous pour le gérer. Selon vos permissions, les utilisateurs peuvent être ajoutés, supprimés et leur mot de passe changé.", @@ -1109,14 +1109,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Nouveau Groupe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Cliquez ou appuyez sur un groupe ci-dessous pour gérer ce groupe. En fonction de votre niveau d'accès, des groupes peuvent être ajoutés et supprimés, ainsi que leurs utilisateurs et groupes membres.", @@ -1128,16 +1128,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Fermer Sessions", "DIALOG_HEADER_CONFIRM_DELETE" : "Fermer Sessions", "DIALOG_HEADER_ERROR" : "Erreur", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Cette page sera remplie avec des connexions actuellement actives. Les connexions répertoriées et la possibilité de supprimer ces connexions dépendent de votre niveau d'accès. Si vous souhaitez en fermer une ou plusieurs, sélectionnez les et cliquez sur \"Fermer Sessions\". La fermeture d'une session déconnectera immédiatement l'utilisateur.", @@ -1165,15 +1165,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/it.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/it.json similarity index 91% rename from guacamole/src/main/frontend/src/translations/it.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/it.json index 2b4be1b3f1..1d4bcfa956 100644 --- a/guacamole/src/main/frontend/src/translations/it.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/it.json @@ -1,7 +1,7 @@ { - + "NAME" : "Italiano", - + "APP" : { "ACTION_ACKNOWLEDGE" : "OK", @@ -26,7 +26,7 @@ "ERROR_PASSWORD_BLANK" : "La password non può essere vuota.", "ERROR_PASSWORD_MISMATCH" : "Le password inserite sono diverse!", - + "FIELD_HEADER_PASSWORD" : "Password:", "FIELD_HEADER_PASSWORD_AGAIN" : "Re-inserisci la password:", @@ -40,14 +40,14 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Pulisci i trasferimenti completati", "ACTION_DISCONNECT" : "Disconnetti", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Riconnetti", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", "ACTION_UPLOAD_FILES" : "Carica un file", "DIALOG_HEADER_CONNECTING" : "Connessione in corso", @@ -87,7 +87,7 @@ "ERROR_UPLOAD_31D" : "Ci sono troppi file in coda per il trasferimento. Attendi che siano completati i trasferimenti in atto e riprova.", "ERROR_UPLOAD_DEFAULT" : "Si è verificato un errore sul server e la connessione è stata chiusa. Riprova o contatta il tuo amministratore di sistema.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Il testo copiato/tagliato appare qui. I cambiamenti effettuati al testo qui sotto saranno riportati negli appunti remoti.", "HELP_INPUT_METHOD_NONE" : "Non c'è nessun metodo di immissione. L'input da tastiera è accettato da una tastiera fisica connessa.", @@ -137,12 +137,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Nessuna connessione recente.", - + "PASSWORD_CHANGED" : "Password modificata.", "SECTION_HEADER_ALL_CONNECTIONS" : "Tutte le Connessioni", @@ -152,11 +152,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Nome utente e/o password errati.", @@ -167,20 +167,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina connessione", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Posizione:", "FIELD_HEADER_NAME" : "Nome:", "FIELD_HEADER_PROTOCOL" : "Protocollo:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Attiva adesso", @@ -200,13 +200,13 @@ "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina Gruppo di Connesisoni", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Posizione:", "FIELD_HEADER_NAME" : "Nome:", @@ -223,13 +223,13 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Nome:", @@ -239,27 +239,27 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Elimina utente", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Amministartore di sistema:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Cambia la tua password:", "FIELD_HEADER_CREATE_NEW_USERS" : "Crea un utente:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Crea una connessione:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Crea un ruppo di connessioni:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USERNAME" : "Nome utente:", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "SECTION_HEADER_CONNECTIONS" : "Connessioni", "SECTION_HEADER_EDIT_USER" : "Modifica Utente", @@ -268,7 +268,7 @@ "TEXT_CONFIRM_DELETE" : "L'utente non può essere ripristinato dopo l'eliminazione. Sei sicuro di volere eliminare l'utente?" }, - + "PROTOCOL_RDP" : { "FIELD_HEADER_COLOR_DEPTH" : "Profondità di colore:", @@ -305,10 +305,10 @@ "FIELD_HEADER_SFTP_PASSWORD" : "Password:", "FIELD_HEADER_SFTP_PORT" : "Porta:", "FIELD_HEADER_SFTP_PRIVATE_KEY" : "Chiave privata:", - "FIELD_HEADER_SFTP_USERNAME" : "Nome utente:", + "FIELD_HEADER_SFTP_USERNAME" : "Nome utente:", "FIELD_HEADER_STATIC_CHANNELS" : "Nomi dei canali statici:", "FIELD_HEADER_USERNAME" : "Nome utente:", - "FIELD_HEADER_WIDTH" : "Larghezza:", + "FIELD_HEADER_WIDTH" : "Larghezza:", "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", @@ -471,17 +471,17 @@ "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Nuova Connessione", "ACTION_NEW_CONNECTION_GROUP" : "Nuovo Gruppo", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Fai click o tap sulla connessione qui sotto per gestire quella connessione. In base al tuo livello di accesso, le connessioni possono essere create, eliminate, e le relative proprietà (protocol, hostname, port, etc.) possono essere cambiate.", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Connessioni" @@ -489,9 +489,9 @@ "SETTINGS_CONNECTION_HISTORY" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", @@ -504,14 +504,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Lingua dell'interfaccia:", "FIELD_HEADER_PASSWORD" : "Password:", @@ -519,22 +519,22 @@ "FIELD_HEADER_PASSWORD_NEW" : "Nuova Password:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Conferma Nuova Password:", "FIELD_HEADER_USERNAME" : "Nome utente", - + "HELP_DEFAULT_INPUT_METHOD" : "Il metodo di input predefinito determina come gli eventi della tastiera vengono ricevuti da Guacamole. La modifica di questa impostazione potrebbe essere necessaria quando si utilizza un dispositivo mobile o quando si digita un IME. Questa impostazione può essere ignorata in base alla connessione all'interno del menu Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "La modalità di emulazione del mouse predefinita determina come si comporterà il mouse remoto nelle nuove connessioni rispetto ai tocchi. Questa impostazione può essere ignorata in base alla connessione all'interno del menu Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LANGUAGE" : "Seleziona una lingua diversa di seguito per cambiare la lingua di tutto il testo all'interno di Guacamole. Le scelte disponibili dipenderanno dalle lingue installate.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Se desideri cambiare la tua password, inserisci la tua password attuale e sotto scrivi quella che desideri come nuova password, clicca \"Modifica Password\". La modifica avrà effetto immediato.", "INFO_PASSWORD_CHANGED" : "Password Modificata.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Metodo di immissione predefinito", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Modalità di emulazione del mouse predefinita", @@ -544,14 +544,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nuovo utente", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Fare clic o toccare un utente di seguito per gestire quell'utente. A seconda del livello di accesso, gli utenti possono essere aggiunti ed eliminati e le loro password possono essere modificate.", @@ -560,44 +560,44 @@ "TABLE_HEADER_USERNAME" : "Nome utente" }, - + "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Termina Sessione", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Termina Sessione", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Questa pagina verrà popolata con connessioni attualmente attive. Le connessioni elencate e la possibilità di terminare tali connessioni dipende dal tuo livello di accesso. Se desideri terminare una o più sessioni, seleziona la casella accanto a quelle sessioni e fai clic su \"Termina sessione \". Terminando una sessione si disconnetterà immediatamente l'utente dalla connessione associata.", - + "INFO_NO_SESSIONS" : "Nessuna sessione attiva", "SECTION_HEADER_SESSIONS" : "Sessioni Attive", - + "TABLE_HEADER_SESSION_USERNAME" : "Nome utente", "TABLE_HEADER_SESSION_STARTDATE" : "Attivo da", "TABLE_HEADER_SESSION_REMOTEHOST" : "Host remoto", "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome della connessione", - + "TEXT_CONFIRM_DELETE" : "Sei sicuro di voler terminare la sessione selezionata? L'utente che sta utilizzando questa sessione sarà immediatamente disconnesso." }, "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@" } diff --git a/guacamole/src/main/frontend/src/translations/ja.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ja.json similarity index 92% rename from guacamole/src/main/frontend/src/translations/ja.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ja.json index 73a55e099e..296a3a6bd6 100644 --- a/guacamole/src/main/frontend/src/translations/ja.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ja.json @@ -1,7 +1,7 @@ { - + "NAME" : "日本語", - + "APP" : { "ACTION_CANCEL" : "キャンセル", @@ -32,7 +32,7 @@ "ERROR_PASSWORD_BLANK" : "パスワードが入力されていません。", "ERROR_PASSWORD_MISMATCH" : "パスワードが一致しません。", - + "FIELD_HEADER_PASSWORD" : "パスワード:", "FIELD_HEADER_PASSWORD_AGAIN" : "パスワード確認:", @@ -46,15 +46,15 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "クリア", "ACTION_DISCONNECT" : "切断", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "再接続", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "クリックをしてコピー/カットされたテキストが表示されます。", "ACTION_UPLOAD_FILES" : "ファイルアップロード", @@ -146,12 +146,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "最近の接続情報はありません。", - + "PASSWORD_CHANGED" : "パスワードが変更されました。", "SECTION_HEADER_ALL_CONNECTIONS" : "全ての接続情報", @@ -167,11 +167,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "不正なログインです。", @@ -182,20 +182,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "接続の削除", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "ロケーション:", "FIELD_HEADER_NAME" : "名前:", "FIELD_HEADER_PROTOCOL" : "プロトコル:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "アクティブにする", @@ -211,20 +211,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "接続元", "TEXT_CONFIRM_DELETE" : "削除した接続は元に戻せません。この接続を削除してもよろしいですか?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "接続グループの削除", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "ロケーション:", "FIELD_HEADER_NAME" : "名前:", @@ -241,14 +241,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "共有プロファイルの削除", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "名前:", "FIELD_HEADER_PRIMARY_CONNECTION" : "プライマリ接続:", @@ -262,16 +262,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "ユーザ削除", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "システム管理者:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "自身のパスワードの変更:", @@ -280,12 +280,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "接続の作成:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "接続グループの作成:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "共有プロファイルの作成:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "ログインの無効化:", "FIELD_HEADER_USERNAME" : "ユーザ名:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "このユーザーは現在どのグループにも属していません。このセクションを展開してグループを追加してください。", @@ -305,48 +305,48 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "グループの削除", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "グループの無効化:", "FIELD_HEADER_USER_GROUP_NAME" : "グループ名:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "このグループは現在どのグループにも属していません。グループを追加するにはこのセクションを展開してください。", "HELP_NO_MEMBER_USER_GROUPS" : "このグループには現在グループが含まれていません。このセクションを展開してグループを追加してください。", "HELP_NO_MEMBER_USERS" : "このグループには現在ユーザーが含まれていません。ユーザーを追加するにはこのセクションを展開してください。", "INFO_READ_ONLY" : "このグループは編集できません", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "利用可能なユーザがいません。", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "グループ編集", "SECTION_HEADER_MEMBER_USERS" : "メンバーユーザ", "SECTION_HEADER_MEMBER_USER_GROUPS" : "メンバーグループ", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "親グループ", "TEXT_CONFIRM_DELETE" : "削除したグループは復元できません。このグループを削除してもよろしいですか?" }, - + "PROTOCOL_KUBERNETES" : { "FIELD_HEADER_BACKSPACE" : "Backspaceキーの送信:", @@ -670,12 +670,12 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "過去の接続履歴はここに表示されています。列の見出しをクリックしてソートすることができます。特定のレコードを検索するにはフィルタに検索キーワードを入力して、検索ボタンをクリックしてください。", @@ -688,24 +688,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "開始時間", "TABLE_HEADER_SESSION_USERNAME" : "ユーザ名", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "接続の追加", "ACTION_NEW_CONNECTION_GROUP" : "グループの追加", "ACTION_NEW_SHARING_PROFILE" : "共有プロファイルの追加", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "接続をクリックまたはタップすることで、管理画面が表示されます。権限に応じて接続のプロパティが変更できます。", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "接続" @@ -713,14 +713,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "表示言語:", "FIELD_HEADER_PASSWORD" : "パスワード:", @@ -729,22 +729,22 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "新しいパスワード(確認):", "FIELD_HEADER_TIMEZONE" : "タイムゾーン:", "FIELD_HEADER_USERNAME" : "ユーザ名:", - + "HELP_DEFAULT_INPUT_METHOD" : "デフォルトの入力メソッドは、Guacamoleがどのようにキーボード入力を受け取るかを設定します。この設定の変更は、モバイルデバイスまたはIMEを通して入力を行う際に必要です。", "HELP_DEFAULT_MOUSE_MODE" : "デフォルトのマウスエミュレーションモードは、タッチに関して新しい接続でリモートマウスがどのように動作するかを決定します。この設定は、Guacamoleメニュー内で接続ごとに上書きすることができます。", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Guacamoleの言語を変更するには、下の言語を選択してください。選択可能な言語は、インストールされている言語によって異なります。", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "パスワードを変更する場合は、下に現在のパスワードと新しいパスワードを入力して、[パスワードの更新]をクリックしてください。変更はすぐに有効になります。", "INFO_PASSWORD_CHANGED" : "パスワードが変更されました。", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "デフォルトの入力方法", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "デフォルトのマウスエミュレーションモード", @@ -754,14 +754,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "ユーザ追加", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "ユーザをクリックまたはタップすることで、ユーザを管理できます。権限に応じてユーザ情報の変更を行うことができます。", @@ -776,14 +776,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "グループ追加", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "グループをクリックまたはタップすることで、グループを管理できます。権限に応じてグループ情報の変更を行うことができます。", @@ -794,28 +794,28 @@ }, "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "強制切断", - + "DIALOG_HEADER_CONFIRM_DELETE" : "セッションの強制切断", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Guacamoleのアクティブなセッションが全て表示されています。 もしセッションを強制切断したい場合、 チェックボックスにチェックを入れて、強制切断ボタンをクリックしてください。", - + "INFO_NO_SESSIONS" : "アクティブセッションはありません", "SECTION_HEADER_SESSIONS" : "アクティブセッション", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "接続名", "TABLE_HEADER_SESSION_REMOTEHOST" : "接続元", "TABLE_HEADER_SESSION_USERNAME" : "ユーザ名", - + "TEXT_CONFIRM_DELETE" : "選択したすべてのセッションを強制終了しますか?これらのセッションを使用しているユーザーは直ちに切断されます。" }, @@ -831,15 +831,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/ko.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ko.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/ko.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ko.json index e5c4da5353..a4fd39a70b 100644 --- a/guacamole/src/main/frontend/src/translations/ko.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ko.json @@ -50,17 +50,17 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "지우기", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "연결 해제", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "다시 연결", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_UPLOAD_FILES" : "파일 업로드", "DIALOG_HEADER_CONNECTING" : "연결 중", @@ -77,7 +77,7 @@ "ERROR_CLIENT_20B" : "원격 데스크톱 서버가 강제로 연결을 끊었습니다. 원치 않거나 예기치 않은 경우, 시스템 관리자에게 문의하거나 시스템 로그를 확인하십시오.", "ERROR_CLIENT_301" : "로그인이 실패했습니다. 다시 연결한 다음 다시 시도하십시오", "ERROR_CLIENT_303" : "원격 데스크톱 서버가 이 연결에 대한 액세스를 거부했습니다. 액세스 권한이 필요하면, 시스템 관리자에게 계정 액세스 권한을 부여하도록 요청하거나 시스템 설정을 확인하십시오.", - "ERROR_CLIENT_308" : "브라우저에서 연결이 끊긴 것처럼 보일 정도로 오랫동안 응답이 없었기 때문에 Guacamole 서버가 연결을 닫았습니다. 이는 보통 불안정한 무선 신호나 단지 네트워크의 느린 속도같은 네트워크 문제로 일어납니다. 네트워크 상태를 확인 후에 다시 시도 하십시오.", + "ERROR_CLIENT_308" : "브라우저에서 연결이 끊긴 것처럼 보일 정도로 오랫동안 응답이 없었기 때문에 Guacamole 서버가 연결을 닫았습니다. 이는 보통 불안정한 무선 신호나 단지 네트워크의 느린 속도같은 네트워크 문제로 일어납니다. 네트워크 상태를 확인 후에 다시 시도 하십시오.", "ERROR_CLIENT_31D" : "개별 사용자의 동시 연결 사용 제한을 초과했기 때문에 Guacamole 서버가 이 연결에 대한 액세스를 거부했습니다. 하나 이상의 연결을 닫고 다시 시도 하십시오.", "ERROR_CLIENT_DEFAULT" : "Guacamole 서버 내에서 내부 오류가 발생해서 연결이 종료되었습니다. 문제가 지속되면, 시스템 관리자에게 문의하거나 시스템 로그를 확인하십시오.", @@ -94,7 +94,7 @@ "ERROR_TUNNEL_31D" : "개별 사용자의 동시 연결 사용 제한을 초과했기 때문에 Guacamole server가 이 연결에 대한 액세스를 거부하고 있습니다. 하나 이상의 연결을 닫고 다시 시도하십시오.", "ERROR_TUNNEL_DEFAULT" : "Guacamole 서버 내에서 내부 오류가 발생해서 연결이 종료되었습니다. 문제가 지속되면, 시스템 관리자에게 문의하거나 시스템 로그를 확인하십시오.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "ERROR_UPLOAD_100" : "파일 전송이 지원되지 않거나 활성화되지 않았습니다. 시스템 관리자에게 문의하거나 시스템 로그를 확인하십시오.", "ERROR_UPLOAD_201" : "현재 너무 많은 파일이 전송되고 있습니다. 기존 전송이 완료될 때까지 기다린 후 다시 시도하십시오.", @@ -149,9 +149,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "숨기기", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "표시", "FIELD_HEADER_BACKGROUND" : "배경색", @@ -179,9 +179,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "최근 연결이 없습니다.", @@ -200,11 +200,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "잘못된 로그인", @@ -215,20 +215,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "연결 삭제", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "위치:", "FIELD_HEADER_NAME" : "이름:", "FIELD_HEADER_PROTOCOL" : "프로토콜:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "현재 활성화", @@ -243,20 +243,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "원격 호스트", "TEXT_CONFIRM_DELETE" : "연결을 삭제한 후에는 복원할 수 없습니다. 이 연결을 삭제하겠습니까?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "연결 그룹 삭제", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "위치:", "FIELD_HEADER_NAME" : "이름:", @@ -273,14 +273,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "공유 프로필 삭제", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "이름:", "FIELD_HEADER_PRIMARY_CONNECTION" : "기본 연결:", @@ -294,16 +294,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "사용자 삭제", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "관리자 시스템", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "자신의 패스워드 변경:", @@ -312,12 +312,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "새로운 연결 생성", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "새로운 연결 그룹 생성:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "새로운 공유 프로파일 생성", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "로그인 비활성화:", "FIELD_HEADER_USERNAME" : "사용자 이름:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "이 사용자는 현재 어떤 그룹에도 속하지 않습니다. 그룹을 추가하려면이 섹션을 확장하십시오.", @@ -337,42 +337,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "DIALOG_HEADER_CONFIRM_DELETE" : "그룹 삭제", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "비활성화:", "FIELD_HEADER_USER_GROUP_NAME" : "그룹 이름:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "이 그룹은 현재 어떤 그룹에도 소속되지 않았습니다. 이 섹션을 확장하여 그룹을 추가하십시오.", "HELP_NO_MEMBER_USER_GROUPS" : "이 그룹은 현재 어떤 그룹도 포함하고 있지 않습니다. 이 섹션을 확장하여 그룹을 추가하십시오.", "HELP_NO_MEMBER_USERS" : "이 그룹은 어떤 사용자도 포함 하고 있지 않습니다. 이 섹션을 확장하여 사용자를 추가하십시오. ", "INFO_READ_ONLY" : "이 그룹은 편집할 수 없습니다.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "사용가능한 사용자가 없습니다.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "그룹 편집", "SECTION_HEADER_MEMBER_USERS" : "맴버 사용자", "SECTION_HEADER_MEMBER_USER_GROUPS" : "맴버 그룹", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "상위 그룹", "TEXT_CONFIRM_DELETE" : "그룹을 삭제한 후에는 복원 할 수 없습니다. 이 그룹을 삭제 하시겠습니까?" @@ -817,14 +817,14 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "과거 연결 기록은 여기에 나열되며 열 헤더 (column header)를 클릭하여 정렬할 수 있습니다. 특정 기록을 검색하려면, 필터 문자열을 입력하고 \"검색\"을 클릭하십시오. 제공된 필터 문자열과 일치하는 기록만 나열됩니다.", @@ -837,24 +837,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "시작 시간", "TABLE_HEADER_SESSION_USERNAME" : "사용자 이름", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "새 연결", "ACTION_NEW_CONNECTION_GROUP" : "새 그룹", "ACTION_NEW_SHARING_PROFILE" : "새 공유 프로필", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "아래 연결을 클릭하거나 탭하여 해당 연결을 관리합니다. 엑세스 레벨에 따라 연결을 추가하고 삭제할 수 있으며, 속성 (프로토콜, 호스트 이름, 포트 등)을 변경할 수 있습니다.", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "연결" @@ -862,14 +862,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "표시 언어:", "FIELD_HEADER_PASSWORD" : "패스워드:", @@ -881,19 +881,19 @@ "HELP_DEFAULT_INPUT_METHOD" : "기본 입력 방법은 키보드 이벤트가 Guacamole에 의해 수신되는 방법을 결정합니다. 모바일 장치를 사용하거나 IME를 통해 입력할 때 이 설정을 변경해야 할 수도 있습니다. 이 세팅은 Guacamole 메뉴 내에서 연결마다 재정의할 수 있습니다.", "HELP_DEFAULT_MOUSE_MODE" : "기본 마우스 에뮬레이션 방법은 원격 마우스가 터치와 관련해서 새 연결에서 어떻게 작동할지 결정합니다. 이 세팅은 Guacamole 메뉴 내에서 연결마다 재정의할 수 있습니다.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "아래의 옵션은 사용자의 지역과 관련이 있으며 인터페이스의 여러 부분이 표시되는 방법에 영향을 줄 것입니다.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "패스워드를 변경하려면 현재 패스워드와 새 패스워드를 아래에 입력하고, \"패스워드 변경\"을 클릭하십시오. 변경사항은 즉시 적용될 것입니다.", "INFO_PASSWORD_CHANGED" : "패스워드가 변경되었습니다.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "기본 입력 방법", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "기본 마우스 에뮬레이션 방법", @@ -904,14 +904,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "새 사용자", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "아래 사용자를 클릭하거나 탭하여 해당 사용자를 관리합니다. 엑세스 레벨에 따라 사용자를 추가하고 삭제할 수 있으며, 패스워드를 변경할 수 있습니다.", @@ -926,14 +926,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "새 그룹", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "아래 그룹을 클릭하거나 탭하여 해당 그룹을 관리합니다. 엑세스 레벨에 따라 그룹을 추가하고 삭제할 수 있으며, 구성원 사용자들과 그룹을 변경할 수 있습니다.", @@ -945,16 +945,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "세션 종료", "DIALOG_HEADER_CONFIRM_DELETE" : "세션 종료", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "이 페이지는 현재 활성화된 연결로 이루어져 있습니다. 세션을 하나 이상 종료하려면, 해당 세션 옆의 체크 박스를 선택하고 \"세션 종료\"를 클릭하십시오. 세션을 종료하면 관련된 사용자의 연결이 즉시 끊어질 것입니다.", @@ -982,15 +982,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/nl.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/nl.json similarity index 92% rename from guacamole/src/main/frontend/src/translations/nl.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/nl.json index ef03dc36e2..0a1af99293 100644 --- a/guacamole/src/main/frontend/src/translations/nl.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/nl.json @@ -44,14 +44,14 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wis lijst", "ACTION_DISCONNECT" : "Verbreek Verbinding", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Verbind Opnieuw", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", "ACTION_UPLOAD_FILES" : "Upload Bestanden", "DIALOG_HEADER_CONNECTING" : "Aan Het Verbinden", @@ -91,7 +91,7 @@ "ERROR_UPLOAD_31D" : "Er worden momenteel te veel bestanden overdragen. Gelieve te wachten tot de bestaande bestandsoverdracht is voltooid, en probeer het opnieuw.", "ERROR_UPLOAD_DEFAULT" : "Er is een interne fout opgetreden op de Guacamole server, en de verbinding is beëindigd. Als het probleem aanhoudt, neem dan contact op met uw systeembeheerder of kijk in uw systeem logs.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Tekst gekopieerd / geknipt binnen Guacamole zal hier verschijnen. Wijzigingen in onderstaande tekst zal externe klembord beïnvloeden.", "HELP_INPUT_METHOD_NONE" : "Geen invoer methode gebruiken. Toetsenbord invoer wordt geaccepteerd van een aangesloten, fysiek toetsenbord.", @@ -148,9 +148,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Geen recente verbindingen.", @@ -163,11 +163,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Ongeldige Login", @@ -178,20 +178,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Verbinding", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Locatie:", "FIELD_HEADER_NAME" : "Naam:", "FIELD_HEADER_PROTOCOL" : "Protocol:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Nu Actief", @@ -206,19 +206,19 @@ "TABLE_HEADER_HISTORY_DURATION" : "Tijdsduur", "TEXT_CONFIRM_DELETE" : "Verbindingen kunnen niet worden hersteld nadat ze zijn verwijderd. Weet u zeker dat u deze verbinding wilt verwijderen?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Verbindingsgroep", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Locatie:", "FIELD_HEADER_NAME" : "Naam:", @@ -235,13 +235,13 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Naam:", @@ -251,27 +251,27 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Verwijder Gebruiker", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Systeem Beheer:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Wijzigen eigen wachtwoord:", "FIELD_HEADER_CREATE_NEW_USERS" : "Nieuwe gebruikers aanmaken:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Nieuwe verbindingen aanmaken:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Nieuwe verbindingsgroepen aanmaken:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USERNAME" : "Gebruikersnaam:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "INFO_READ_ONLY" : "Sorry, maar dit gebruikers account kan niet gewijzigd worden", @@ -551,11 +551,11 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "De gebruikgeschiedenis van verbindingen wordt hier onder getoond en kan gesorteerd worden door op de titel van de kolom te klikken. Voer een zoekterm in en klik op \"Zoeken\", om op specifieke resultaten te zoeken. Alleen de resultaten die voldoen aan de zoekterm zullen dan getoond worden.", @@ -567,23 +567,23 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Starttijd", "TABLE_HEADER_SESSION_USERNAME" : "Gebruikersnaam", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Nieuwe Verbinding", "ACTION_NEW_CONNECTION_GROUP" : "Nieuwe Verbindingsgroep", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Klik of tik op een verbinding hieronder om die verbinding te beheren. Afhankelijk van uw toegangsniveau kunnen verbindingen worden toegevoegd en verwijderd en hun eigenschappen (protocol, hostname, port, etc.) worden gewijzigd. ", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Verbindingen" @@ -591,14 +591,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Taal Keuze:", "FIELD_HEADER_PASSWORD" : "Wachtwoord:", @@ -609,19 +609,19 @@ "HELP_DEFAULT_INPUT_METHOD" : "De standaard invoer methode bepaalt hoe toetsenbord gebeurtenissen ontvangen worden door Guacamole. Het veranderen van deze instelling kan nodig zijn bij gebruik van een mobiel apparaat of wanneer er via een IME getypt wordt. Deze instelling kan per verbinding worden overschreven via het Guacamole menu.", "HELP_DEFAULT_MOUSE_MODE" : "De standaard muis emulatie modus bepaalt hoe de externe muis in nieuwe verbindingen omgaat met touch. Deze instelling kan per verbinding worden overschreven via het Guacamole menu.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LANGUAGE" : "Selecteer onderstaand een andere taal om alle text in Guacamole hieraan aan te passen. De beschikbare keuzes zijn afhankelijk van welke talen er geinstalleerd zijn.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Als u uw wachtwoord wilt wijzigen, voer dan uw huidige wachtwoord en uw nieuwe wachtwoord hieronder in en klik op \"Wijzig Wachtwoord\". Deze wijziging zal meteen actief zijn.", "INFO_PASSWORD_CHANGED" : "Wachtwoord gewijzigd.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Standaard Invoer Methode", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Standaard Muis Emulatie Modus", @@ -631,14 +631,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nieuwe Gebruiker", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Klik of tik op een van de onderstaande gebruikers om die te beheren. Afhankelijk van uw toegangsniveau kunnen gebruikers worden toegevoegd, verwijderd en hun wachtwoorden gewijzigd.", @@ -650,16 +650,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Beeindig Sessies", "DIALOG_HEADER_CONFIRM_DELETE" : "Beeindig Sessie", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Deze pagina wordt gevuld met momenteel actieve verbindingen. De vermelde verbindingen en de mogelijkheid om die verbindingen te doden, zijn afhankelijk van uw toegangsniveau. Als u een of meerdere sessies wilt beeindigen, vink die sessie(s) dan aan en klik op \"Beeindig Sessies\". Door het verbreken van een sessie verliest de gebruiker ogenblikkelijk het contact met die sessie(s).", @@ -678,14 +678,14 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/no.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/no.json similarity index 92% rename from guacamole/src/main/frontend/src/translations/no.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/no.json index 2ddd389320..b0f966153c 100644 --- a/guacamole/src/main/frontend/src/translations/no.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/no.json @@ -1,7 +1,7 @@ { - + "NAME" : "Norsk Bokmål", - + "APP" : { "ACTION_ACKNOWLEDGE" : "OK", @@ -28,7 +28,7 @@ "ERROR_PASSWORD_BLANK" : "Passordet ditt kan ikke være blankt.", "ERROR_PASSWORD_MISMATCH" : "Passordene er ikke like.", - + "FIELD_HEADER_PASSWORD" : "Passord:", "FIELD_HEADER_PASSWORD_AGAIN" : "Gjenta Passord:", @@ -46,14 +46,14 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Tøm", "ACTION_DISCONNECT" : "Koble fra", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Koble til på nytt", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", "ACTION_UPLOAD_FILES" : "Last Opp Filer", "DIALOG_HEADER_CONNECTING" : "Kobler til", @@ -93,7 +93,7 @@ "ERROR_UPLOAD_31D" : "For mange filer blir overført. Vent til aktive overføringer fullfører og prøv igjen.", "ERROR_UPLOAD_DEFAULT" : "En intern feil har oppstått i Guacamole og forbindelsen er terminert. Kontakt systemadministrator dersom problemet fortsetter eller sjekk systemloggene dine.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Tekst som er kopiert eller klippet i Guacamole vises her. Endringer i teksten under vil påvirke den eksterne utklippstavlen.", "HELP_INPUT_METHOD_NONE" : "Ingen innenhet er brukt. Tastetrykk fra et fysisk tilkoblet tastatur blir akseptert.", @@ -148,12 +148,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Ingen nylige tilkoblinger.", - + "PASSWORD_CHANGED" : "Passord endret.", "SECTION_HEADER_ALL_CONNECTIONS" : "Alle tilkoblinger", @@ -163,11 +163,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Ugyldig Pålogging", @@ -178,20 +178,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Slett tilkobling", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Lokasjon:", "FIELD_HEADER_NAME" : "Navn:", "FIELD_HEADER_PROTOCOL" : "Protokoll:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Aktiv Nå", @@ -206,19 +206,19 @@ "TABLE_HEADER_HISTORY_DURATION" : "Varighet", "TEXT_CONFIRM_DELETE" : "Tilkoblinger kan ikke gjennopprettes etter at de er slettet. Er du sikker på at du vil slette denne tilkoblingen?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Slett Tilkoblingsgruppe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Lokasjon:", "FIELD_HEADER_NAME" : "Navn:", @@ -235,27 +235,27 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Slett Bruker", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administrer systemet:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Endre eget passord:", "FIELD_HEADER_CREATE_NEW_USERS" : "Opprett ny bruker:", "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Opprett ny tilkobling:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Opprett ny tilkoblingsgruppe:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USERNAME" : "Brukernavn:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "INFO_READ_ONLY" : "Beklager, denne brukerkontoen kan ikke redigeres.", @@ -266,7 +266,7 @@ "TEXT_CONFIRM_DELETE" : "Brukere kan ikke gjenopprettes etter at de er slettet. Er du sikker på du vil slette denne brukeren?" }, - + "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "Klientnavn:", @@ -532,11 +532,11 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Tidligere forbindelser er listet her historisk og kan sorteres ved å klikke kolonneoverskriftene. For å lete etter spesifikke poster kan du legge inn en filtreringstekst og klikke \"Søk\". Bare poster som passer filtreringsteksten vil bli listet opp.", @@ -548,23 +548,23 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Starttidspunkt", "TABLE_HEADER_SESSION_USERNAME" : "Brukernavn", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Ny Forbindelse", "ACTION_NEW_CONNECTION_GROUP" : "Ny Gruppe", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Klikk eller berør en forbindelse under for å administrere forbindelsen. Avhengig av ditt tilgangsnivå kan forbindelser opprettes, slettes og egenskaper (protokoll, server, port, m.m.) kan endres.", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Forbindelser" @@ -572,14 +572,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Visningsspråk:", "FIELD_HEADER_PASSWORD" : "Passord:", @@ -587,22 +587,22 @@ "FIELD_HEADER_PASSWORD_NEW" : "Nytt Passord:", "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Verifiser Nytt Passord:", "FIELD_HEADER_USERNAME" : "Brukernavn:", - + "HELP_DEFAULT_INPUT_METHOD" : "Standard inndatametode bestemmer hvordan tastetrykk mottas av Guacamole. Det kan være nødvendig å endre på dette når du bruker mobile enheter eller IME. Denne instillingen kan overstyres på hver forbindelse i Guacamolemenyen.", "HELP_DEFAULT_MOUSE_MODE" : "Standard musemulering bestemmer hvordan den eksterne musen vil oppføre seg med hensyn til berøringer. Denne instillingen kan overstyres på hver forbindelse i Guacamolemenyen.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LANGUAGE" : "Velg et språk under for å endre språket på all tekst i Guacamole. Tilgjengelige valg er avhengig av hvilke språk som er installert.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Skriv inn ditt gamle passord og det nye passordet under dersom du ønsker å endre passord. Klikk \"Oppdater Passord\". Endringen trer i kraft umiddelbart.", "INFO_PASSWORD_CHANGED" : "Passord endret.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Standard Innenhet", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Standard Metode For Musemulering", @@ -612,14 +612,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Ny Bruker", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Klikk på en bruker under for å administrere den brukeren. Avhengig av din tilgang kan brukere legges til, slettes og passordet kan endres.", @@ -628,45 +628,45 @@ "TABLE_HEADER_USERNAME" : "Brukernavn" }, - + "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Avbryt sesjoner", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Avbryt Sesjoner", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Denne siden vil bli fylt med nåværende aktive forbindelser. Tilkoblingene oppført og evnen til å drepe disse tilkoblingene er avhengig av tilgangsnivået ditt. Dersom du ønsker å avbryte en eller flere sesjoner haker du av boksen ved siden av sesjonen og klikker \"Avbryt sesjoner\". Avbrytes en sesjon vil brukeren umiddelbart kobles av den aktuelle sesjonen.", - + "INFO_NO_SESSIONS" : "Ingen aktive sesjoner", "SECTION_HEADER_SESSIONS" : "Aktive Sesjoner", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Navn på forbindelse", "TABLE_HEADER_SESSION_REMOTEHOST" : "Server", "TABLE_HEADER_SESSION_STARTDATE" : "Aktiv siden", "TABLE_HEADER_SESSION_USERNAME" : "Brukernavn", - + "TEXT_CONFIRM_DELETE" : "Er du sikker på at du vil avslutte valgte sesjoner? Brukere av disse sesjonene vil bli frakoblet umiddelbart." }, "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/pl.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pl.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/pl.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pl.json index a01724d323..3accbe2a86 100644 --- a/guacamole/src/main/frontend/src/translations/pl.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pl.json @@ -58,17 +58,17 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Wyczyść", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Rozłącz", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Połącz Ponownie", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_UPLOAD_FILES" : "Wyślij Pliki", "DIALOG_HEADER_CONNECTING" : "Łączenie", @@ -114,7 +114,7 @@ "ERROR_UPLOAD_31D" : "Zbyt wiele plików jest aktualnie transferowanych. Poczekaj na zakończenie trwających transferów i spróbuj ponownie.", "ERROR_UPLOAD_DEFAULT" : "Wystąpił błąd wewnętrzny serwera Guacamole i połączenie zostało zakończone. Jeśli problem nie zniknie, powiadom administratora systemu lub sprawdź log systemu.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Tekst skopiowany/wycięty wewnątrz serwera Guacamole pojawi się tutaj. Zmiany dokonane w tym miejscu będą miały wpływ na zawartość zdalnego schowka", "HELP_INPUT_METHOD_NONE" : "Żadna metoda wejścia nie jest w użyciu. Wejście klawiatury jest akceptowane z podłączonej fizycznej klawiatury.", @@ -160,9 +160,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Ukryj", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Pokaż", "FIELD_HEADER_BACKGROUND" : "Drugi plan", @@ -190,9 +190,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Brak ostatnich połączeń.", @@ -211,11 +211,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Niepoprawny Login", @@ -226,20 +226,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Połączenie", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Lokalizacja:", "FIELD_HEADER_NAME" : "Nazwa:", "FIELD_HEADER_PROTOCOL" : "Protokół:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Aktywne Teraz", @@ -255,20 +255,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Zdalny Host", "TEXT_CONFIRM_DELETE" : "Połączenia nie mogą zostać przywrócone po ich usunięciu. Czy na pewno chcesz usunąć to połączenie?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Grupę Połączeń", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Lokalizacja:", "FIELD_HEADER_NAME" : "Nazwa:", @@ -285,14 +285,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Profil Udostępniania", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Nazwa:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Podstawowe Połączenie:", @@ -306,16 +306,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń Użytkownika", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administracja systemem:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Zmiana własnego hasła:", @@ -324,12 +324,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Tworzenie nowych połączeń:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Tworzenie nowych grup połączeń:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Tworzenie nowych profili udostępniania:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Logowanie zablokowane:", "FIELD_HEADER_USERNAME" : "Nazwa użytkownika:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Ten użytkownik nie należy obecnie do żadnej grupy. Rozwiń tę sekcje aby dodać do grupy.", @@ -349,42 +349,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Usuń grupę", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_DISABLED" : "Wyłączona:", "FIELD_HEADER_USER_GROUP_NAME" : "Nazwa grupy:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "To grupa nie należy do żadnej grupy. Rozwiń tę sekcje aby dodać do grupy.", "HELP_NO_MEMBER_USER_GROUPS" : "Ta grupa nie zawiera żadnych grup. Rozwiń tę sekcje aby dodać grupy.", "HELP_NO_MEMBER_USERS" : "Ta grupa nie zawiera żadnych użytkowników. Rozwiń tę sekcje aby dodać użytkowników do grupy.", "INFO_READ_ONLY" : "Ta grupa nie może być edytowana", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Brak dostępnych użytkowników.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Edytuj Grupę", "SECTION_HEADER_MEMBER_USERS" : "Należący użytkownicy", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Należące grupy", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Grupy nadrzędne", "TEXT_CONFIRM_DELETE" : "Grupy nie mogą zostać przywrócone po tym jak zostaną usunięte. Czy na pewno chcesz usunąć tę grupę?" @@ -393,9 +393,9 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "INFO_LOADING_RECORDING" : "Twoje nagranie jest właśnie ładowane. Proszę czekać...", "INFO_SEEK_IN_PROGRESS" : "Szukanie żądanej pozycji. Proszę czekać..." @@ -874,15 +874,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Rekordy historii poprzednich połączeń są wylistowane tutaj i można je sortować klikając nagłówki kolumn. Aby wyszukać określone rekordy wprowadź ciąg filtru i kliknij \"Szukaj\". Tylko rekordy pasujące do podanego filtru zostaną wyświetlone.", @@ -896,24 +896,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Czas rozpoczęcia", "TABLE_HEADER_SESSION_USERNAME" : "Użytkownik", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Nowe Połączenie", "ACTION_NEW_CONNECTION_GROUP" : "Nowa Grupa", "ACTION_NEW_SHARING_PROFILE" : "Nowy Profil Udostępniania", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Kliknij lub naciśnij połączenie poniżej, aby nim zarządzać. W zależności od poziomu dostępu, połączenia mogą być dodawane i usuwane, a ich właściwości (protokół, nazwa hosta, port itp.) mogą być zmieniane.", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Połączenia" @@ -921,15 +921,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Język:", "FIELD_HEADER_PASSWORD" : "Hasło:", @@ -941,20 +941,20 @@ "HELP_DEFAULT_INPUT_METHOD" : "Domyślna metoda wprowadzania określa sposób odbierania zdarzeń klawiatury przez Guacamole. Zmiana tego ustawienia może być konieczna w przypadku korzystania z urządzenia mobilnego lub pisania w edytorze IME. To ustawienie można zmienić dla każdego połączenia w menu Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Domyślny tryb emulacji myszy określa, jak zdalna mysz będzie się zachowywać w nowych połączeniach w odniesieniu do dotknięć. To ustawienie można zmienić dla każdego połączenia w menu Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Poniższe opcje są związane z ustawieniami regionalnymi użytkownika i wpłyną na sposób wyświetlania różnych części interfejsu", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Jeśli chcesz zmienić swoje hasło, wprowadź poniżej obecne hasło i żądane nowe hasło, a następnie kliknij \"Zmień Hasło\". Zmiana będzie miała efekt natychmiastowy.", "INFO_PASSWORD_CHANGED" : "Hasło zmienione.", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "Ustawienia użytkownika zapisane.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Domyślna Metoda Wprowadzania", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Domyślny Tryb Emulacji Myszy", @@ -964,14 +964,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Nowy Użytkownik", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Kliknij lub dotknij użytkownika poniżej, aby nim zarządzać. W zależności od Twojego poziomu dostępu, użytkowników można dodawać i usuwać, a także zmieniać ich hasła.", @@ -986,14 +986,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Nowa Grupa", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Kliknij lub naciśnij grupę poniżej, aby nią zarządzać. W zależności od Twojego poziomu dostępu, grupy mogą być dodawane i usuwane, a ich użytkownicy i grupy mogą być edytowane.", @@ -1005,16 +1005,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Zakończ sesję", "DIALOG_HEADER_CONFIRM_DELETE" : "Zakończ Sesję", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Ta strona zostanie wypełniona aktualnie aktywnymi połączeniami. Lista połączeń i możliwość ich zakończenia zależy od Twojego poziomu dostępu. Jeśli chcesz zakończyć jedną lub więcej sesji, zaznacz pola obok tych sesji i kliknij \"Zakończ sesję\". Zakończenie sesji spowoduje natychmiastowe rozłączenie użytkownika z powiązanym połączeniem.", @@ -1043,15 +1043,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/pt.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pt.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/pt.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pt.json index bfd95e92a1..3f7129a0eb 100644 --- a/guacamole/src/main/frontend/src/translations/pt.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/pt.json @@ -1,7 +1,7 @@ { - + "NAME" : "Português", - + "APP" : { "ACTION_ACKNOWLEDGE" : "OK", @@ -32,7 +32,7 @@ "ERROR_PAGE_UNAVAILABLE" : "Um erro ocorreu e esta ação não pode ser completada. Se o problema persistir, por favor contacte o administrador do sistema ou verifique os logs do sistema.", "ERROR_PASSWORD_BLANK" : "Sua senha não pode ficar em branco.", "ERROR_PASSWORD_MISMATCH" : "As senhas são diferentes.", - + "FIELD_HEADER_PASSWORD" : "Senha:", "FIELD_HEADER_PASSWORD_AGAIN" : "Repita a senha:", @@ -50,17 +50,17 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Limpar", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "Desconectar", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Reconectar", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_UPLOAD_FILES" : "Enviar Arquivos", "DIALOG_HEADER_CONNECTING" : "Conectando", @@ -106,7 +106,7 @@ "ERROR_UPLOAD_31D" : "Muitos arquivos estão sendo transferidos ao mesmo tempo. Por favor aguarde que as transferências atuais terminem e tente novamente.", "ERROR_UPLOAD_DEFAULT" : "Um erro interno ocorreu no servidor do Guacamole e a conexão foi finalizada. Se o problema persistir, por favor informe o administrador do sistema, ou verifique os logs do sistema.", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "Texto copiado/cortado dentro do Guacamole será mostrado aqui. Mudanças no text abaixo afetarão a área de transferência remota.", "HELP_INPUT_METHOD_NONE" : "Nenhum método de entrada está sendo usado. A entrada do teclado é aceita a partir de um teclado físico conectado.", @@ -152,9 +152,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Ocultar", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Mostrar", "FIELD_HEADER_BACKGROUND" : "Fundo", @@ -182,12 +182,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Nenhuma conexão recente.", - + "PASSWORD_CHANGED" : "Senha alterada.", "SECTION_HEADER_ALL_CONNECTIONS" : "Todas as Conexões", @@ -203,11 +203,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Autenticação Inválida", @@ -218,20 +218,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Remover conexão", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Localização:", "FIELD_HEADER_NAME" : "Nome:", "FIELD_HEADER_PROTOCOL" : "Protocolo:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Ativa agora", @@ -247,20 +247,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Estação Remota", "TEXT_CONFIRM_DELETE" : "Conexões não podem ser restauradas depois de removidas. Tem certeza que deseja remover esta conexão?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Delete Connection Group", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Localização:", "FIELD_HEADER_NAME" : "Nome:", @@ -277,14 +277,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Remover Perfil de Compartilhamento", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Nome:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Conexão Primária:", @@ -298,16 +298,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Remover Usuário", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Administrar o sistema:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Alterar a própria senha:", @@ -316,12 +316,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Criar novas conexões:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Criar novos grupos de conexão:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Criar novos perfis de compartilhamento:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Login desativado:", "FIELD_HEADER_USERNAME" : "Usuário:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Este usuário não pertence a nenhum grupo no momento. Expanda esta seção para adicionar grupos.", @@ -341,42 +341,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Remover Grupo", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Desativado:", "FIELD_HEADER_USER_GROUP_NAME" : "Nome do grupo:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Este grupo não pertence a nenhum outro grupo atualmente. Expanda esta seção para adicionar grupos.", "HELP_NO_MEMBER_USER_GROUPS" : "Este grupo não contém nenhum outro grupo atualmente. Expanda esta seção para adicionar grupos.", "HELP_NO_MEMBER_USERS" : "Este grupo não contém nenhum usuário atualmente. Expanda esta seção para adicionar usuários.", "INFO_READ_ONLY" : "Desculpe, mas este grupo não pode ser editado.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Nenhum usuário disponível.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Editar Grupo", "SECTION_HEADER_MEMBER_USERS" : "Usuários Membros", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Grupos Membros", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Grupos Pai", "TEXT_CONFIRM_DELETE" : "Grupos não podem ser restaurados depois de removidos. Tem certeza que deseja remover este grupos?" @@ -530,7 +530,7 @@ "FIELD_HEADER_WOL_MAC_ADDR" : "Endereço MAC do host remoto:", "FIELD_HEADER_WOL_SEND_PACKET" : "Enviar pacote WoL:", "FIELD_HEADER_WOL_WAIT_TIME" : "Tempo de espera da inicialização do host:", - + "FIELD_OPTION_COLOR_DEPTH_16" : "Low color (16-bit)", "FIELD_OPTION_COLOR_DEPTH_24" : "True color (24-bit)", @@ -838,14 +838,14 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "history.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Os registros históricos das conexões são mostrados aqui e podem ser reordenadas clicando nos títulos das colunas. Para buscar por registros específicos, digite algo e clique em \"Procurar\". Apenas registros que correspondem ao filtro fornecido serão listados.", @@ -858,24 +858,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Horário de início", "TABLE_HEADER_SESSION_USERNAME" : "Usuário", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Nova conexão", "ACTION_NEW_CONNECTION_GROUP" : "Novo grupo", "ACTION_NEW_SHARING_PROFILE" : "Novo Perfil Compartilhado", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Clique na conexão abaixo para gerenciar a conexão. Dependendo do seu nível de acesso, conexões podem ser adicionadas e removidas, bem como suas propriedades (protocolo, nome do host, porta, etc) podem ser alteradas.", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Conexões" @@ -883,14 +883,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Idioma:", "FIELD_HEADER_PASSWORD" : "Senha:", @@ -899,22 +899,22 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "Confirme a Nova Senha:", "FIELD_HEADER_TIMEZONE" : "Fuso Horário:", "FIELD_HEADER_USERNAME" : "Usuário:", - + "HELP_DEFAULT_INPUT_METHOD" : "O método de entrada padrão determina como os eventos do teclado serão recebidos pelo Guacamole. Mudar este parâmetro pode ser necessário para utilização de um dispositivo móvel, ou ao digitar através de um IME (método de entrada). Esta configuração pode ser substituída em cada conexão através do menu do Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "O modo de emulação padrão do mouse determina como o mouse remoto vai se comportar em novas conexões em relação aos toques. Esta configuração pode ser substituída em cada conexão através do menu do Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "As opções abaixo estão relacionadas ao idioma e localização do usuário e terão impacto sobre como várias partes da interface são exibidas", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Se você deseja alterar a sua senha, informe a senha atual e a nova senha abaixo, e clique em \"Alterar Senha\". A mudança terá efeito imediato.", "INFO_PASSWORD_CHANGED" : "Senha alterada.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Método de Entrada Padrão", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Modo de Emulação de Mouse Padrão", @@ -924,14 +924,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Novo Usuário", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Clique em um usuário abaixo para gerenciá-lo. Dependendo do seu nível de acesso, usuários podem ser adicionados e removidos, e suas senhas podem ser alteradas.", @@ -946,14 +946,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Novo Grupo", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Clique em um grupo abaixo para gerenciá-lo. Dependendo do seu nível de acesso, grupos podem ser adicionados ou removidos, e seus usuários e grupos membros podem ser modificados.", @@ -964,29 +964,29 @@ }, "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Encerrar Sessões", - + "DIALOG_HEADER_CONFIRM_DELETE" : "Encerrar Sessões", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Esta página será preenchida com conexões ativas no momento. As conexões listadas e a capacidade de encerrar estas conexões dependem do seu nível de acesso. Se você deseja encerrar uma ou mais sessões, marque a caixa ao lado destas sessões e clique em \"Encerrar Sessões\". Encerrar uma sessão desconectará imediatamente o usuário da conexão associada.", - + "INFO_NO_SESSIONS" : "Nenhuma sessão ativa", "SECTION_HEADER_SESSIONS" : "Sessões Ativas", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "Nome da conexão", "TABLE_HEADER_SESSION_REMOTEHOST" : "Host remoto", "TABLE_HEADER_SESSION_STARTDATE" : "Ativo desde", "TABLE_HEADER_SESSION_USERNAME" : "Usuário", - + "TEXT_CONFIRM_DELETE" : "Você tem certeza que quer encerrar as sessões selecionadas? Usuários que estão utilizando estas sessões serão imediatamente desconectados." }, @@ -1002,15 +1002,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/ru.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ru.json similarity index 93% rename from guacamole/src/main/frontend/src/translations/ru.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ru.json index a086f73390..072f0527aa 100644 --- a/guacamole/src/main/frontend/src/translations/ru.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/ru.json @@ -49,15 +49,15 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_CLEAR_COMPLETED_TRANSFERS" : "Очистить", "ACTION_DISCONNECT" : "Отключиться", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "Переподключиться", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_UPLOAD_FILES" : "Загрузка файлов", "DIALOG_HEADER_CONNECTING" : "Подключение", @@ -143,9 +143,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "Спрятать", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "Показать", "FIELD_HEADER_BACKGROUND" : "Цвет фона", @@ -173,9 +173,9 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "Нет недавних подключений.", @@ -188,17 +188,17 @@ "LIST" : { - "TEXT_ANONYMOUS_USER" : "@:APP.TEXT_ANONYMOUS_USER" + "TEXT_ANONYMOUS_USER" : "@:APP.TEXT_ANONYMOUS_USER:@" }, "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "Неверные данные для входа", @@ -209,20 +209,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить подключение", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Размещение:", "FIELD_HEADER_NAME" : "Название:", "FIELD_HEADER_PROTOCOL" : "Протокол:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "Активно", @@ -238,20 +238,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "Удаленный узел", "TEXT_CONFIRM_DELETE" : "Подключения не могут быть восстановлены после удаления. Вы уверены, что хотите удалить подключение?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить группу подключений", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "Размещение:", "FIELD_HEADER_NAME" : "Название:", @@ -268,14 +268,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить профиль расшаривания", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "Название:", "FIELD_HEADER_PRIMARY_CONNECTION" : "Изначальное подключение:", @@ -289,16 +289,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить пользователя", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "Администрирование системы:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "Изменение собственного пароля:", @@ -307,12 +307,12 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "Создание нового подключения:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "Создание новой группы подключений:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "Создание нового профиля расшаривания:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "Аккаунт отключен:", "FIELD_HEADER_USERNAME" : "Имя пользователя:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Этот пользователь еще не сопоставлен ни с одной группой. Раскройте эту секцию для добавления групп.", @@ -332,42 +332,42 @@ "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "Удалить группу", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "Группа отключена:", "FIELD_HEADER_USER_GROUP_NAME" : "Название:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "Эта группа еще не сопоставлена ни с одной группой. Раскройте эту секцию для добавления групп.", "HELP_NO_MEMBER_USER_GROUPS" : "Эта группа еще не содержит ни одной группы. Раскройте эту секцию для добавления групп.", "HELP_NO_MEMBER_USERS" : "Эта группа еще не содержит ни одного пользователя. Раскройте эту секцию для добавления пользователей.", "INFO_READ_ONLY" : "Извините, эта группа не может быть изменена.", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "Нет доступных пользователей.", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "Редактирование группы", "SECTION_HEADER_MEMBER_USERS" : "Пользователи в группе", "SECTION_HEADER_MEMBER_USER_GROUPS" : "Дочерние группы", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "Родительские группы", "TEXT_CONFIRM_DELETE" : "Группы не могут быть восстановлены после удаления. Вы уверены, что хотите удалить группу?" @@ -667,12 +667,12 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "Здесь отображается журнал предыдущих подключений, который можно отсортировать, кликнув на заголовке колонки. Для поиска конкретных записей введите нужные данные и нажмите «Поиск». Тогда будут отображаться только записи, соответствующие данным для поиска.", @@ -685,24 +685,24 @@ "TABLE_HEADER_SESSION_STARTDATE" : "Время начала", "TABLE_HEADER_SESSION_USERNAME" : "Имя пользователя", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_CONNECTION" : "Новое подключение", "ACTION_NEW_CONNECTION_GROUP" : "Новая группа", "ACTION_NEW_SHARING_PROFILE" : "Новый профиль расшаривания", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "Нажмите на подключение, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление подключений, а также изменение их свойств (протокол, название сервера, порт и т.д.).", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "Подключения" @@ -710,14 +710,14 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "Язык:", "FIELD_HEADER_PASSWORD" : "Пароль:", @@ -729,19 +729,19 @@ "HELP_DEFAULT_INPUT_METHOD" : "Режим ввода по умолчанию определяет, каким образом нажатия на клавиатуру будут передаваться Guacamole. Изменение данной настройки может быть полезным при работе с мобильных устройств или при вводе через IME. Данная настройка может быть сделана для каждого подключения через основное меню Guacamole.", "HELP_DEFAULT_MOUSE_MODE" : "Режим эмуляции мыши по умолчанию определяет, каким образом мышь на удаленном сервере будет реагировать на прикосновения для новых подключений. Данная настройка может быть переопределена для каждого подключения через основное меню Guacamole.", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "Ниже вы можете выбрать язык и установить другие настройки локали, влияющие на отображение интерфейса пользователя.", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "Если вы хотите изменить пароль, укажите ваш текущий пароль и дважды укажите новый. Затем нажмите «Обновить пароль». Изменения вступят в силу моментально.", "INFO_PASSWORD_CHANGED" : "Пароль успешно изменен.", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "Метод ввода по умолчанию", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "Режим эмуляции мыши по умолчанию", @@ -751,14 +751,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "Создать пользователя", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "Нажмите на пользователя, чтобы управлять им. В зависимости от прав доступа возможно добавление и удаление пользователей, а также изменение паролей.", @@ -773,14 +773,14 @@ "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "Создать группу", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "Нажмите на группу, чтобы управлять ей. В зависимости от прав доступа возможно добавление и удаление групп, а также изменение членства других групп и пользователей.", @@ -792,16 +792,16 @@ "SETTINGS_SESSIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "Завершить сессии", "DIALOG_HEADER_CONFIRM_DELETE" : "Завершение сессий", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "Здесь показаны текущие активные сессии Guacamole. Отображаемые сессии и возможность их завершения зависит от ваших прав доступа. Если вы хотите завершить одну или несколько сессий, выберите нужные и нажмите «Завершить сессии». Принудительное завершение сессий приведет к немедленному отключению пользователей.", @@ -829,15 +829,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/frontend/src/translations/zh.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/zh.json similarity index 92% rename from guacamole/src/main/frontend/src/translations/zh.json rename to guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/zh.json index 86d84a9671..035fdbfb59 100644 --- a/guacamole/src/main/frontend/src/translations/zh.json +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/src/translations/zh.json @@ -1,7 +1,7 @@ { - + "NAME" : "简体中文", - + "APP" : { "NAME" : "Apache Guacamole", @@ -60,18 +60,18 @@ "CLIENT" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR", - "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR_CLIENT_MESSAGES" : "@:APP.ACTION_CLEAR:@", + "ACTION_CLEAR_COMPLETED_TRANSFERS" : "@:APP.ACTION_CLEAR:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", "ACTION_DISCONNECT" : "断开连接", - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_NAVIGATE_BACK" : "@:APP.ACTION_NAVIGATE_BACK:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", "ACTION_RECONNECT" : "重新连接", - "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE", - "ACTION_SHARE" : "@:APP.ACTION_SHARE", + "ACTION_SAVE_FILE" : "@:APP.ACTION_SAVE:@", + "ACTION_SHARE" : "@:APP.ACTION_SHARE:@", "ACTION_SHOW_CLIPBOARD" : "点击以查看剪贴板内容。", "ACTION_UPLOAD_FILES" : "上传文件", @@ -118,7 +118,7 @@ "ERROR_UPLOAD_31D" : "正在同时传输太多文件。请等待当前的传输任务完成后,再重试。", "ERROR_UPLOAD_DEFAULT" : "本连接因为 Guacamole 服务器出现了内部错误而被终止。如果问题持续,请通知您的系统管理员,或检查您的系统日志。", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CLIPBOARD" : "复制/剪切的文本将出现在这里。对下面文本内容所作的修改将会影响远程电脑上的剪贴板。", "HELP_INPUT_METHOD_NONE" : "没有选择任何输入方式。将从连接的物理键盘接受键盘输入。", @@ -172,9 +172,9 @@ "COLOR_SCHEME" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_HIDE_DETAILS" : "隐藏", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "ACTION_SHOW_DETAILS" : "显示", "FIELD_HEADER_BACKGROUND" : "背景色", @@ -188,15 +188,15 @@ "IMPORT": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_BROWSE" : "浏览文件", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLEAR" : "@:APP.ACTION_CLEAR", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLEAR" : "@:APP.ACTION_CLEAR:@", "ACTION_VIEW_FORMAT_HELP" : "查看帮助", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_IMPORT_CONNECTIONS" : "导入连接", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "DIALOG_HEADER_SUCCESS" : "成功", "ERROR_AMBIGUOUS_CSV_HEADER" : "存在冲突的 CSV 列名: \"{HEADER}\" ,可能是一个连接属性或者参数", @@ -218,7 +218,7 @@ "ERROR_REQUIRED_NAME" : "在提供的文件中未找到连接名称", "ERROR_REQUIRED_PROTOCOL" : "在提供的文件中未找到协议", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FIELD_HEADER_EXISTING_CONNECTION_MODE" : "替换/更新已存在的连接", "FIELD_HEADER_EXISTING_PERMISSION_MODE" : "重置权限", @@ -240,7 +240,7 @@ "HELP_YAML_EXAMPLE" : "---\n - name: conn1\n protocol: vnc\n parameters:\n hostname: conn1.web.com\n group: ROOT\n users:\n - guac user 1\n - guac user 2\n groups:\n - Connection 1 Users\n attributes:\n guacd-encryption: none\n - name: conn2\n protocol: rdp\n parameters:\n hostname: conn2.web.com\n group: ROOT/Parent Group\n users:\n - guac user 1\n attributes:\n guacd-encryption: none\n - name: conn3\n protocol: ssh\n parameters:\n hostname: conn3.web.com\n group: ROOT/Parent Group/Child Group\n users:\n - guac user 2\n - guac user 3\n - name: conn4\n protocol: kubernetes", "INFO_CONNECTIONS_IMPORTED_SUCCESS" : "{NUMBER} 条连接已成功导入。", - + "SECTION_HEADER_CONNECTION_IMPORT" : "导入连接", "SECTION_HEADER_HELP_CONNECTION_IMPORT_FILE" : "导入连接文件格式", "SECTION_HEADER_CSV" : "CSV 格式", @@ -270,12 +270,12 @@ "HOME" : { - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "INFO_NO_RECENT_CONNECTIONS" : "无最近使用过的连接。", - + "PASSWORD_CHANGED" : "密码已修改。", "SECTION_HEADER_ALL_CONNECTIONS" : "全部连接", @@ -291,11 +291,11 @@ "LOGIN": { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE", - "ACTION_LOGIN" : "@:APP.ACTION_LOGIN", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CONTINUE" : "@:APP.ACTION_CONTINUE:@", + "ACTION_LOGIN" : "@:APP.ACTION_LOGIN:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "ERROR_INVALID_LOGIN" : "非法登录", @@ -306,20 +306,20 @@ "MANAGE_CONNECTION" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "删除连接", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "位置:", "FIELD_HEADER_NAME" : "名称:", "FIELD_HEADER_PROTOCOL" : "协议:", - "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_HISTORY_START" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "INFO_CONNECTION_DURATION_UNKNOWN" : "--", "INFO_CONNECTION_ACTIVE_NOW" : "活动中", @@ -335,20 +335,20 @@ "TABLE_HEADER_HISTORY_REMOTEHOST" : "远程主机", "TEXT_CONFIRM_DELETE" : "连接被删除后将无法恢复。确定要删除这个连接吗?", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "MANAGE_CONNECTION_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "删除连接组", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_LOCATION" : "位置:", "FIELD_HEADER_NAME" : "名字:", @@ -365,14 +365,14 @@ "MANAGE_SHARING_PROFILE" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "删除共享设定", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", "FIELD_HEADER_NAME" : "名字:", "FIELD_HEADER_PRIMARY_CONNECTION" : "主连接:", @@ -386,16 +386,16 @@ "MANAGE_USER" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "删除用户", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_ADMINISTER_SYSTEM" : "授权管理系统:", "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "修改自己的密码:", @@ -404,15 +404,15 @@ "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "新建连接:", "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "新建连接组:", "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "新建共享设定:", - "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD", - "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN", + "FIELD_HEADER_PASSWORD" : "@:APP.FIELD_HEADER_PASSWORD:@", + "FIELD_HEADER_PASSWORD_AGAIN" : "@:APP.FIELD_HEADER_PASSWORD_AGAIN:@", "FIELD_HEADER_USER_DISABLED" : "已禁用登录:", "FIELD_HEADER_USERNAME" : "用户名:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "该用户当前不属于任何组。 展开此部分以添加组。", - + "INFO_READ_ONLY" : "对不起,不能编辑此用户的账户。", "INFO_NO_USER_GROUPS_AVAILABLE" : "没用可用的用户组.", @@ -422,49 +422,49 @@ "SECTION_HEADER_EDIT_USER" : "编辑用户", "SECTION_HEADER_PERMISSIONS" : "使用权限", "SECTION_HEADER_USER_GROUPS" : "用户组", - + "TEXT_CONFIRM_DELETE" : "将不能恢复已被删除的用户。确定要删除这个用户吗?" }, - + "MANAGE_USER_GROUP" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_CLONE" : "@:APP.ACTION_CLONE", - "ACTION_DELETE" : "@:APP.ACTION_DELETE", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_CLONE" : "@:APP.ACTION_CLONE:@", + "ACTION_DELETE" : "@:APP.ACTION_DELETE:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", "DIALOG_HEADER_CONFIRM_DELETE" : "删除用户组", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM", - "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD", - "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS", - "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS", - "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS", - "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS", - "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_HEADER_ADMINISTER_SYSTEM" : "@:MANAGE_USER.FIELD_HEADER_ADMINISTER_SYSTEM:@", + "FIELD_HEADER_CHANGE_OWN_PASSWORD" : "@:MANAGE_USER.FIELD_HEADER_CHANGE_OWN_PASSWORD:@", + "FIELD_HEADER_CREATE_NEW_USERS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USERS:@", + "FIELD_HEADER_CREATE_NEW_USER_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_USER_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTIONS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTIONS:@", + "FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_CONNECTION_GROUPS:@", + "FIELD_HEADER_CREATE_NEW_SHARING_PROFILES" : "@:MANAGE_USER.FIELD_HEADER_CREATE_NEW_SHARING_PROFILES:@", "FIELD_HEADER_USER_GROUP_DISABLED" : "禁用:", "FIELD_HEADER_USER_GROUP_NAME" : "用户组名称:", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_NO_USER_GROUPS" : "该组当前不属于任何组。展开此部分以添加组。", "HELP_NO_MEMBER_USER_GROUPS" : "该组当前不包含任何组。展开此部分以添加组。", "HELP_NO_MEMBER_USERS" : "该组当前不包含任何用户。展开此部分以添加用户。", "INFO_READ_ONLY" : "抱歉,无法编辑此用户组。", - "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE", + "INFO_NO_USER_GROUPS_AVAILABLE" : "@:MANAGE_USER.INFO_NO_USER_GROUPS_AVAILABLE:@", "INFO_NO_USERS_AVAILABLE" : "没有可用的用户。", - "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS", - "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS", - "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS", + "SECTION_HEADER_ALL_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_ALL_CONNECTIONS:@", + "SECTION_HEADER_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CONNECTIONS:@", + "SECTION_HEADER_CURRENT_CONNECTIONS" : "@:MANAGE_USER.SECTION_HEADER_CURRENT_CONNECTIONS:@", "SECTION_HEADER_EDIT_USER_GROUP" : "编辑用户组", "SECTION_HEADER_MEMBER_USERS" : "成员用户", "SECTION_HEADER_MEMBER_USER_GROUPS" : "成员用户组", - "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS", + "SECTION_HEADER_PERMISSIONS" : "@:MANAGE_USER.SECTION_HEADER_PERMISSIONS:@", "SECTION_HEADER_USER_GROUPS" : "父用户组", "TEXT_CONFIRM_DELETE" : "删除用户组后将无法还原。确定要删除该用户组吗?" @@ -473,15 +473,15 @@ "PLAYER" : { - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_PAUSE" : "@:APP.ACTION_PAUSE", - "ACTION_PLAY" : "@:APP.ACTION_PLAY", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_PAUSE" : "@:APP.ACTION_PAUSE:@", + "ACTION_PLAY" : "@:APP.ACTION_PLAY:@", "INFO_LOADING_RECORDING" : "录像已载入。请稍候...", "INFO_SEEK_IN_PROGRESS" : "正在跳转到指定位置。请稍候..." }, - + "PROTOCOL_KUBERNETES" : { "FIELD_HEADER_BACKSPACE" : "发送退格键:", @@ -551,7 +551,7 @@ "SECTION_HEADER_NETWORK" : "网络" }, - + "PROTOCOL_RDP" : { "FIELD_HEADER_CLIENT_NAME" : "客户端:", @@ -640,7 +640,7 @@ "FIELD_OPTION_NORMALIZE_CLIPBOARD_EMPTY" : "", "FIELD_OPTION_NORMALIZE_CLIPBOARD_PRESERVE" : "保持原样", "FIELD_OPTION_NORMALIZE_CLIPBOARD_UNIX" : "Linux/Mac/Unix (LF)", - "FIELD_OPTION_NORMALIZE_CLIPBOARD_WINDOWS" : "Windows (CRLF)", + "FIELD_OPTION_NORMALIZE_CLIPBOARD_WINDOWS" : "Windows (CRLF)", "FIELD_OPTION_COLOR_DEPTH_16" : "低彩色(16 位)", "FIELD_OPTION_COLOR_DEPTH_24" : "真彩色(24 位)", @@ -747,7 +747,7 @@ "FIELD_OPTION_BACKSPACE_EMPTY" : "", "FIELD_OPTION_BACKSPACE_8" : "退格键(Ctrl-H)", "FIELD_OPTION_BACKSPACE_127" : "删除键(Ctrl-?)", - + "FIELD_OPTION_COLOR_SCHEME_BLACK_WHITE" : "白底黑字", "FIELD_OPTION_COLOR_SCHEME_EMPTY" : "", "FIELD_OPTION_COLOR_SCHEME_GRAY_BLACK" : "黑底灰字", @@ -769,7 +769,7 @@ "FIELD_OPTION_FONT_SIZE_72" : "72", "FIELD_OPTION_FONT_SIZE_96" : "96", "FIELD_OPTION_FONT_SIZE_EMPTY" : "", - + "FIELD_OPTION_TERMINAL_TYPE_ANSI" : "ansi", "FIELD_OPTION_TERMINAL_TYPE_EMPTY" : "", "FIELD_OPTION_TERMINAL_TYPE_LINUX" : "linux", @@ -777,7 +777,7 @@ "FIELD_OPTION_TERMINAL_TYPE_VT220" : "vt220", "FIELD_OPTION_TERMINAL_TYPE_XTERM" : "xterm", "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color", - + "NAME" : "SSH", "SECTION_HEADER_AUTHENTICATION" : "认证", @@ -860,7 +860,7 @@ "FIELD_OPTION_TERMINAL_TYPE_VT220" : "vt220", "FIELD_OPTION_TERMINAL_TYPE_XTERM" : "xterm", "FIELD_OPTION_TERMINAL_TYPE_XTERM_256COLOR" : "xterm-256color", - + "NAME" : "Telnet", "SECTION_HEADER_AUTHENTICATION" : "认证", @@ -955,15 +955,15 @@ "SETTINGS_CONNECTION_HISTORY" : { - "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD", - "ACTION_SEARCH" : "@:APP.ACTION_SEARCH", - "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING", + "ACTION_DOWNLOAD" : "@:APP.ACTION_DOWNLOAD:@", + "ACTION_SEARCH" : "@:APP.ACTION_SEARCH:@", + "ACTION_VIEW_RECORDING" : "@:APP.ACTION_VIEW_RECORDING:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "FILENAME_HISTORY_CSV" : "连接历史.csv", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_CONNECTION_HISTORY" : "下表中是过往的连接历史,可以点击列头来进行排序。如需搜索特定的记录,输入一个过滤字符串并点击“搜索”。列表中将只显示符合过滤条件的记录。", @@ -977,25 +977,25 @@ "TABLE_HEADER_SESSION_STARTDATE" : "起始时间", "TABLE_HEADER_SESSION_USERNAME" : "用户名", - "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION" + "TEXT_HISTORY_DURATION" : "@:APP.TEXT_HISTORY_DURATION:@" }, "SETTINGS_CONNECTIONS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_IMPORT" : "@:APP.ACTION_IMPORT", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_IMPORT" : "@:APP.ACTION_IMPORT:@", "ACTION_NEW_CONNECTION" : "新建连接", "ACTION_NEW_CONNECTION_GROUP" : "新建连接组", "ACTION_NEW_SHARING_PROFILE" : "新建共享设定", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", "HELP_CONNECTIONS" : "点击下列连接,以管理该连接。基于您的权限,可以新建和删除连接,或修改连接的属性(如协议、主机名、端口等)。", - - "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT", + + "INFO_ACTIVE_USER_COUNT" : "@:APP.INFO_ACTIVE_USER_COUNT:@", "SECTION_HEADER_CONNECTIONS" : "连接" @@ -1003,15 +1003,15 @@ "SETTINGS_PREFERENCES" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", - "ACTION_SAVE" : "@:APP.ACTION_SAVE", - "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", + "ACTION_SAVE" : "@:APP.ACTION_SAVE:@", + "ACTION_UPDATE_PASSWORD" : "@:APP.ACTION_UPDATE_PASSWORD:@", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK", - "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH", + "ERROR_PASSWORD_BLANK" : "@:APP.ERROR_PASSWORD_BLANK:@", + "ERROR_PASSWORD_MISMATCH" : "@:APP.ERROR_PASSWORD_MISMATCH:@", "FIELD_HEADER_LANGUAGE" : "界面语言:", "FIELD_HEADER_PASSWORD" : "密码:", @@ -1020,23 +1020,23 @@ "FIELD_HEADER_PASSWORD_NEW_AGAIN" : "确认新密码:", "FIELD_HEADER_TIMEZONE" : "时区:", "FIELD_HEADER_USERNAME" : "用户名:", - + "HELP_DEFAULT_INPUT_METHOD" : "默认输入法决定了 Guacamole 如何接收键盘事件。当使用移动设备或使用 IME 输入时,有可能需要更改设置。本设置可在 Guacamole 菜单内被单个连接的设定覆盖。", "HELP_DEFAULT_MOUSE_MODE" : "默认鼠标模拟方式决定了新连接内的远程鼠标如何响应屏幕触控。本设置可在 Guacamole 菜单内被单个连接的设定覆盖。", - "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE", - "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK", - "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT", + "HELP_INPUT_METHOD_NONE" : "@:CLIENT.HELP_INPUT_METHOD_NONE:@", + "HELP_INPUT_METHOD_OSK" : "@:CLIENT.HELP_INPUT_METHOD_OSK:@", + "HELP_INPUT_METHOD_TEXT" : "@:CLIENT.HELP_INPUT_METHOD_TEXT:@", "HELP_LOCALE" : "以下选项与用户的语言环境有关,并将影响界面各部分的显示方式。", - "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE", - "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE", + "HELP_MOUSE_MODE_ABSOLUTE" : "@:CLIENT.HELP_MOUSE_MODE_ABSOLUTE:@", + "HELP_MOUSE_MODE_RELATIVE" : "@:CLIENT.HELP_MOUSE_MODE_RELATIVE:@", "HELP_UPDATE_PASSWORD" : "如需改变密码,请在下面输入您的当前密码与希望使用的新密码,并点击“更新密码” 。密码的改动会立即生效。", "INFO_PASSWORD_CHANGED" : "密码已更改。", "INFO_PREFERENCE_ATTRIBUTES_CHANGED" : "用户设定已保存。", - "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE", - "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK", - "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT", + "NAME_INPUT_METHOD_NONE" : "@:CLIENT.NAME_INPUT_METHOD_NONE:@", + "NAME_INPUT_METHOD_OSK" : "@:CLIENT.NAME_INPUT_METHOD_OSK:@", + "NAME_INPUT_METHOD_TEXT" : "@:CLIENT.NAME_INPUT_METHOD_TEXT:@", "SECTION_HEADER_DEFAULT_INPUT_METHOD" : "缺省输入法", "SECTION_HEADER_DEFAULT_MOUSE_MODE" : "缺省鼠标模拟方式", @@ -1046,14 +1046,14 @@ "SETTINGS_USERS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER" : "新用户", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USERS" : "点击下面的用户以管理该用户。基于您的权限,可以新增和删除用户,也可以更改他们的密码。", @@ -1065,17 +1065,17 @@ "TABLE_HEADER_USERNAME" : "用户名" }, - + "SETTINGS_USER_GROUPS" : { - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", "ACTION_NEW_USER_GROUP" : "新建用户组", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", - "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "FORMAT_DATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_USER_GROUPS" : "单击或触摸下面的组以管理该组。 根据您的访问级别,可以添加和删除组,还可以更改其成员用户和用户组。", @@ -1084,31 +1084,31 @@ "TABLE_HEADER_USER_GROUP_NAME" : "用户组名称" }, - + "SETTINGS_SESSIONS" : { - - "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE", - "ACTION_CANCEL" : "@:APP.ACTION_CANCEL", + + "ACTION_ACKNOWLEDGE" : "@:APP.ACTION_ACKNOWLEDGE:@", + "ACTION_CANCEL" : "@:APP.ACTION_CANCEL:@", "ACTION_DELETE" : "终止会话", - + "DIALOG_HEADER_CONFIRM_DELETE" : "终止会话", - "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR", - - "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER", - - "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE", + "DIALOG_HEADER_ERROR" : "@:APP.DIALOG_HEADER_ERROR:@", + + "FIELD_PLACEHOLDER_FILTER" : "@:APP.FIELD_PLACEHOLDER_FILTER:@", + + "FORMAT_STARTDATE" : "@:APP.FORMAT_DATE_TIME_PRECISE:@", "HELP_SESSIONS" : "该页面将填充当前活动的连接。 列出的连接和终止连接的能力取决于您的访问级别。如需终止一个或多个会话,勾选目标会话并点击“终止会话”。终止会话会立即断开对应用户的连接。", - + "INFO_NO_SESSIONS" : "无活动会话", "SECTION_HEADER_SESSIONS" : "活动会话", - + "TABLE_HEADER_SESSION_CONNECTION_NAME" : "连接名", "TABLE_HEADER_SESSION_REMOTEHOST" : "远程主机", "TABLE_HEADER_SESSION_STARTDATE" : "开始时间", "TABLE_HEADER_SESSION_USERNAME" : "用户名", - + "TEXT_CONFIRM_DELETE" : "确定要终止所选定的会话?对应的用户会被立即断开连接。" }, @@ -1124,15 +1124,15 @@ "USER_MENU" : { - "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT", - "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS", - "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES", - "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS", - "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS", - "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS", - "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS", - "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME", - "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY" + "ACTION_LOGOUT" : "@:APP.ACTION_LOGOUT:@", + "ACTION_MANAGE_CONNECTIONS" : "@:APP.ACTION_MANAGE_CONNECTIONS:@", + "ACTION_MANAGE_PREFERENCES" : "@:APP.ACTION_MANAGE_PREFERENCES:@", + "ACTION_MANAGE_SESSIONS" : "@:APP.ACTION_MANAGE_SESSIONS:@", + "ACTION_MANAGE_SETTINGS" : "@:APP.ACTION_MANAGE_SETTINGS:@", + "ACTION_MANAGE_USERS" : "@:APP.ACTION_MANAGE_USERS:@", + "ACTION_MANAGE_USER_GROUPS" : "@:APP.ACTION_MANAGE_USER_GROUPS:@", + "ACTION_NAVIGATE_HOME" : "@:APP.ACTION_NAVIGATE_HOME:@", + "ACTION_VIEW_HISTORY" : "@:APP.ACTION_VIEW_HISTORY:@" } diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.app.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.app.json new file mode 100644 index 0000000000..b5a8796207 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.app.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [ + "global" + ] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.federation.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.federation.json new file mode 100644 index 0000000000..4f22847973 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.federation.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/app", + "types": [ + "global" + ] + }, + "files": [ + "src/main.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} \ No newline at end of file diff --git a/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.spec.json b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.spec.json new file mode 100644 index 0000000000..a8dc58dd67 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/projects/guacamole-frontend/tsconfig.spec.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "../../out-tsc/spec", + "types": [ + "jasmine", + "global" + ] + }, + "include": [ + "src/**/*.spec.ts", + "src/**/*.d.ts" + ] +} diff --git a/guacamole/src/main/guacamole-frontend/tsconfig.json b/guacamole/src/main/guacamole-frontend/tsconfig.json new file mode 100644 index 0000000000..bec0076e90 --- /dev/null +++ b/guacamole/src/main/guacamole-frontend/tsconfig.json @@ -0,0 +1,48 @@ +{ + "compileOnSave": false, + "exclude": [ + "node_modules" + ], + "compilerOptions": { + "typeRoots": [ + "node_modules/@types", + "typings" + ], + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "esModuleInterop": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "allowUmdGlobalAccess": true, + "paths": { + "guacamole-frontend-ext-lib": [ + "dist/guacamole-frontend-ext-lib" + ], + "guacamole-frontend-lib": [ + "dist/guacamole-frontend-lib" + ] + }, + "declaration": false, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "ES2022", + "module": "ES2022", + "useDefineForClassFields": false, + "lib": [ + "ES2022", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/guacamole/src/main/frontend/src/app/osk/oskModule.js b/guacamole/src/main/guacamole-frontend/typings/global.d.ts similarity index 90% rename from guacamole/src/main/frontend/src/app/osk/oskModule.js rename to guacamole/src/main/guacamole-frontend/typings/global.d.ts index ff7fc7c21f..406b580b4c 100644 --- a/guacamole/src/main/frontend/src/app/osk/oskModule.js +++ b/guacamole/src/main/guacamole-frontend/typings/global.d.ts @@ -18,6 +18,6 @@ */ /** - * Module for displaying the Guacamole on-screen keyboard. + * Augment the global scope with the Guacamole types. */ -angular.module('osk', []); +import 'guacamole-common-js'; diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java index be67a384ba..822b9c315f 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/Extension.java @@ -38,6 +38,8 @@ import org.apache.guacamole.net.auth.AuthenticationProvider; import org.apache.guacamole.resource.ClassPathResource; import org.apache.guacamole.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; /** * A Guacamole extension, which may provide custom authentication, static @@ -45,6 +47,11 @@ */ public class Extension { + /** + * TODO + */ + private static final Logger log = LoggerFactory.getLogger(Extension.class); + /** * The Jackson parser for parsing the language JSON files. */ @@ -127,6 +134,11 @@ public class Extension { */ private final Resource largeIcon; + /** + * TODO + */ + private final Resource nativeFederationConfiguration; + /** * Returns a new map of all resources corresponding to the collection of * paths provided. Each resource will be associated with the given @@ -440,6 +452,15 @@ public Extension(final ClassLoader parent, final File file, largeIcon = new ClassPathResource(classLoader, "image/png", manifest.getLargeIcon()); else largeIcon = null; + + // TODO + if (manifest.getNativeFederationConfiguration() != null) { + log.info("getNativeFederationConfiguration(): {}", manifest.getNativeFederationConfiguration()); + nativeFederationConfiguration = new ClassPathResource(classLoader, "application/json", manifest.getNativeFederationConfiguration()); + } else { + nativeFederationConfiguration = null; + } + } /** @@ -597,4 +618,12 @@ public Resource getLargeIcon() { return largeIcon; } + + /** + * TODO + * @return + */ + public Resource getNativeFederationConfiguration() { + return nativeFederationConfiguration; + } } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java index b30c5dd5e5..dda521c651 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionManifest.java @@ -104,6 +104,11 @@ public class ExtensionManifest { */ private String largeIcon; + /** + * TODO + */ + private String nativeFederationConfiguration; + /** * Returns the version of the Guacamole web application for which the * extension was built, such as "0.9.7". @@ -430,4 +435,19 @@ public void setLargeIcon(String largeIcon) { this.largeIcon = largeIcon; } + /** + * TODO + * @return + */ + public String getNativeFederationConfiguration() { + return nativeFederationConfiguration; + } + + /** + * TODO + * @param nativeFederationConfiguration + */ + public void setNativeFederationConfiguration(String nativeFederationConfiguration) { + this.nativeFederationConfiguration = nativeFederationConfiguration; + } } diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java index c4744e2d98..8c0d911b4e 100644 --- a/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java +++ b/guacamole/src/main/java/org/apache/guacamole/extension/ExtensionModule.java @@ -164,6 +164,11 @@ public String getName() { * Service for adding and retrieving HTML patch resources. */ private final PatchResourceService patchResourceService; + + /** + * TODO + */ + private final NativeFederationService nativeFederationService; /** * Returns the classloader that should be used as the parent classloader @@ -204,6 +209,7 @@ public ExtensionModule(Environment environment) { this.environment = environment; this.languageResourceService = new LanguageResourceService(environment); this.patchResourceService = new PatchResourceService(); + this.nativeFederationService = new NativeFederationService(); } /** @@ -608,6 +614,13 @@ private void loadExtensions(Collection javaScriptResources, if(extension.getLargeIcon()!= null) serve("/images/logo-144.png").with(new ResourceServlet(extension.getLargeIcon())); + // TODO + nativeFederationService.addNativeFederationConfigurationResource( + extension.getNamespace(), + extension.getNativeFederationConfiguration() + ); + + // Log successful loading of extension by name logger.info("Extension \"{}\" ({}) loaded.", extension.getName(), extension.getNamespace()); @@ -643,6 +656,9 @@ protected void configureServlets() { // Dynamically generate app.js and app.css from extensions serve("/app.js").with(new ResourceServlet(new SequenceResource(javaScriptResources))); serve("/app.css").with(new ResourceServlet(new SequenceResource(cssResources))); + // TODO + serve("/native-federation-manifest.json").with(new ResourceServlet(nativeFederationService.getNativeFederationManifest())); + serve("/native-federation-config.json").with(new ResourceServlet(nativeFederationService.getNativeFederationConfiguration())); // Dynamically serve all language resources for (Map.Entry entry : languageResourceService.getLanguageResources().entrySet()) { diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationConfiguration.java b/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationConfiguration.java new file mode 100644 index 0000000000..a637bad0f8 --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationConfiguration.java @@ -0,0 +1,79 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.extension; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * TODO + */ +public class NativeFederationConfiguration { + + public static final String DEFAULT_BOOTSTRAP_FUNCTION_NAME = "bootsrapExtension"; + + /** + * The name of the function that will be called by the shell to bootstrap the extension. + * Defaults to {@link NativeFederationConfiguration#DEFAULT_BOOTSTRAP_FUNCTION_NAME}. + */ + private String bootstrapFunctionName = DEFAULT_BOOTSTRAP_FUNCTION_NAME; + + /** + * The title of the page to be displayed in the browser tab when the route is active. + * If not set, the title of the main application will be used. Specific routes can + * override this value by setting the title property on the route. + */ + private String pageTitle; + + /** + * The path where the routes of the extension should be mounted. + */ + private String routePath; + + + public String getBootstrapFunctionName() { + return bootstrapFunctionName; + } + + @JsonProperty(value = "bootstrapFunctionName", defaultValue = DEFAULT_BOOTSTRAP_FUNCTION_NAME) + public void setBootstrapFunctionName(String bootstrapFunctionName) { + this.bootstrapFunctionName = bootstrapFunctionName; + } + + @JsonProperty("pageTitle") + public void setPageTitle(String pageTitle) { + this.pageTitle = pageTitle; + } + + @JsonProperty("routePath") + public void setRoutePath(String routePath) { + this.routePath = routePath; + } + + public String getPageTitle() { + return pageTitle; + } + + + public String getRoutePath() { + return routePath; + } + + +} diff --git a/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationService.java b/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationService.java new file mode 100644 index 0000000000..693a36ba6c --- /dev/null +++ b/guacamole/src/main/java/org/apache/guacamole/extension/NativeFederationService.java @@ -0,0 +1,118 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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. + */ + +package org.apache.guacamole.extension; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.apache.guacamole.resource.ByteArrayResource; +import org.apache.guacamole.resource.Resource; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; + +/** + * TODO + */ +public class NativeFederationService { + + /** + * Logger for this class. + */ + private final Logger logger = LoggerFactory.getLogger(NativeFederationService.class); + + private final ObjectMapper objectMapper = new ObjectMapper(); + + /** + * TODO + */ + private final Map nativeFederationConfiguration = new HashMap<>(); + private final Map nativeFederationManifest = new HashMap<>(); + + + /** + * TODO + * + * @param resource + */ + public void addNativeFederationConfigurationResource(String namespace, Resource resource) { + + if (resource == null) { + return; + } + + NativeFederationConfiguration config = null; + try { + config = objectMapper.readValue(resource.asStream(), NativeFederationConfiguration.class); + this.nativeFederationConfiguration.put(namespace, config); + String staticResourcePrefix = "./app/ext/" + namespace + "/"; + this.nativeFederationManifest.put(namespace, staticResourcePrefix + "remoteEntry.json"); + } catch (IOException e) { + + logger.error("Unable to parse native federation configuration for extension in namespace {}: {}", namespace, e.getMessage()); + logger.debug("Error parsing serialize native federation configuration.", e); + + } + + } + + + /** + * TODO + * + * @return + */ + public Resource getNativeFederationConfiguration() { + + try { + return new ByteArrayResource("application/json", objectMapper.writeValueAsBytes(this.nativeFederationConfiguration)); + } catch (JsonProcessingException e) { + + logger.error("Unable to serialize native federation configuration: {}", e.getMessage()); + logger.debug("Error serializing serialize native federation configuration.", e); + + // TODO: Fallback - Empty configuration + return new ByteArrayResource("application/json", "{}".getBytes(StandardCharsets.UTF_8)); + } + + } + + /** + * TODO + * + * @return + */ + public Resource getNativeFederationManifest() { + try { + return new ByteArrayResource("application/json", objectMapper.writeValueAsBytes(this.nativeFederationManifest)); + } catch (JsonProcessingException e) { + + logger.error("Unable to serialize native federation manifest: {}", e.getMessage()); + logger.debug("Error serializing serialize native federation manifest.", e); + + // TODO: Fallback - Empty configuration + return new ByteArrayResource("application/json", "{}".getBytes(StandardCharsets.UTF_8)); + } + } + +} diff --git a/pom.xml b/pom.xml index 215ba3a38c..9e8ac0ac3b 100644 --- a/pom.xml +++ b/pom.xml @@ -117,6 +117,8 @@ **/templates/*.html src/licenses/**/* target/**/* + **/dist/**/* + **/.angular/**/* ${ignoreLicenseErrors}