summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 20:04:57 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-16 20:04:57 +0000
commit09a16e2a8151bccee444ddc33846b3d02437e838 (patch)
treed61db1fd59874266469e92996cfcf8a8625f7ec4
parentInitial commit. (diff)
downloadcockpit-podman-upstream.tar.xz
cockpit-podman-upstream.zip
Adding upstream version 85.upstream/85upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
-rw-r--r--.eslintignore2
-rw-r--r--.eslintrc.json54
-rw-r--r--.flake82
-rw-r--r--.fmf/version1
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md31
-rw-r--r--.github/ISSUE_TEMPLATE/enhancement.md11
-rw-r--r--.github/codeql-config.yml2
-rw-r--r--.github/dependabot.yml38
-rw-r--r--.github/workflows/cockpit-lib-update.yml33
-rw-r--r--.github/workflows/codeql.yml33
-rw-r--r--.github/workflows/dependabot.yml82
-rw-r--r--.github/workflows/nightly.yml22
-rw-r--r--.github/workflows/release.yml68
-rw-r--r--.github/workflows/reposchutz.yml66
-rw-r--r--.github/workflows/weblate-sync-po.yml46
-rw-r--r--.github/workflows/weblate-sync-pot.yml42
-rw-r--r--.gitignore34
-rw-r--r--.gitmodules7
-rw-r--r--.stylelintrc.json39
-rw-r--r--HACKING.md107
-rw-r--r--LICENSE502
-rw-r--r--Makefile217
-rw-r--r--README.md78
-rwxr-xr-xbuild.js107
-rw-r--r--cockpit-podman.spec88
-rw-r--r--dist/index.css.LEGAL.txt35
-rw-r--r--dist/index.css.gzbin0 -> 144167 bytes
-rw-r--r--dist/index.html36
-rw-r--r--dist/index.js.LEGAL.txt46
-rw-r--r--dist/index.js.gzbin0 -> 428668 bytes
-rw-r--r--dist/manifest.json16
-rw-r--r--dist/po.cs.js.gzbin0 -> 6153 bytes
-rw-r--r--dist/po.de.js.gzbin0 -> 5644 bytes
-rw-r--r--dist/po.es.js.gzbin0 -> 5763 bytes
-rw-r--r--dist/po.fi.js.gzbin0 -> 5778 bytes
-rw-r--r--dist/po.fr.js.gzbin0 -> 5673 bytes
-rw-r--r--dist/po.ja.js.gzbin0 -> 6135 bytes
-rw-r--r--dist/po.ka.js.gzbin0 -> 6373 bytes
-rw-r--r--dist/po.ko.js.gzbin0 -> 5988 bytes
-rw-r--r--dist/po.manifest.cs.js.gzbin0 -> 197 bytes
-rw-r--r--dist/po.manifest.de.js.gzbin0 -> 175 bytes
-rw-r--r--dist/po.manifest.es.js.gzbin0 -> 180 bytes
-rw-r--r--dist/po.manifest.fi.js.gzbin0 -> 179 bytes
-rw-r--r--dist/po.manifest.fr.js.gzbin0 -> 177 bytes
-rw-r--r--dist/po.manifest.ja.js.gzbin0 -> 193 bytes
-rw-r--r--dist/po.manifest.ka.js.gzbin0 -> 228 bytes
-rw-r--r--dist/po.manifest.ko.js.gzbin0 -> 199 bytes
-rw-r--r--dist/po.manifest.pl.js.gzbin0 -> 216 bytes
-rw-r--r--dist/po.manifest.sk.js.gzbin0 -> 196 bytes
-rw-r--r--dist/po.manifest.sv.js.gzbin0 -> 183 bytes
-rw-r--r--dist/po.manifest.tr.js.gzbin0 -> 186 bytes
-rw-r--r--dist/po.manifest.uk.js.gzbin0 -> 251 bytes
-rw-r--r--dist/po.manifest.zh_CN.js.gzbin0 -> 187 bytes
-rw-r--r--dist/po.pl.js.gzbin0 -> 6050 bytes
-rw-r--r--dist/po.sk.js.gzbin0 -> 4099 bytes
-rw-r--r--dist/po.sv.js.gzbin0 -> 5613 bytes
-rw-r--r--dist/po.tr.js.gzbin0 -> 5704 bytes
-rw-r--r--dist/po.uk.js.gzbin0 -> 6809 bytes
-rw-r--r--dist/po.zh_CN.js.gzbin0 -> 5687 bytes
-rw-r--r--node_modules/chrome-remote-interface/LICENSE18
-rw-r--r--node_modules/chrome-remote-interface/README.md992
-rwxr-xr-xnode_modules/chrome-remote-interface/bin/client.js311
-rw-r--r--node_modules/chrome-remote-interface/chrome-remote-interface.js1
-rw-r--r--node_modules/chrome-remote-interface/index.js44
-rw-r--r--node_modules/chrome-remote-interface/lib/api.js92
-rw-r--r--node_modules/chrome-remote-interface/lib/chrome.js314
-rw-r--r--node_modules/chrome-remote-interface/lib/defaults.js4
-rw-r--r--node_modules/chrome-remote-interface/lib/devtools.js127
-rw-r--r--node_modules/chrome-remote-interface/lib/external-request.js44
-rw-r--r--node_modules/chrome-remote-interface/lib/protocol.json27862
-rw-r--r--node_modules/chrome-remote-interface/lib/websocket-wrapper.js39
-rw-r--r--node_modules/chrome-remote-interface/package.json64
-rw-r--r--node_modules/chrome-remote-interface/webpack.config.js48
-rw-r--r--node_modules/commander/History.md298
-rw-r--r--node_modules/commander/LICENSE22
-rw-r--r--node_modules/commander/Readme.md351
-rw-r--r--node_modules/commander/index.js1137
-rw-r--r--node_modules/commander/package.json29
-rw-r--r--node_modules/sizzle/AUTHORS.txt67
-rw-r--r--node_modules/sizzle/LICENSE.txt36
-rw-r--r--node_modules/sizzle/README.md55
-rw-r--r--node_modules/sizzle/dist/sizzle.js2514
-rw-r--r--node_modules/sizzle/dist/sizzle.min.js3
-rw-r--r--node_modules/sizzle/dist/sizzle.min.map1
-rw-r--r--node_modules/sizzle/package.json85
-rw-r--r--node_modules/ws/LICENSE21
-rw-r--r--node_modules/ws/README.md495
-rw-r--r--node_modules/ws/browser.js8
-rw-r--r--node_modules/ws/index.js10
-rw-r--r--node_modules/ws/lib/buffer-util.js129
-rw-r--r--node_modules/ws/lib/constants.js10
-rw-r--r--node_modules/ws/lib/event-target.js184
-rw-r--r--node_modules/ws/lib/extension.js223
-rw-r--r--node_modules/ws/lib/limiter.js55
-rw-r--r--node_modules/ws/lib/permessage-deflate.js518
-rw-r--r--node_modules/ws/lib/receiver.js607
-rw-r--r--node_modules/ws/lib/sender.js409
-rw-r--r--node_modules/ws/lib/stream.js180
-rw-r--r--node_modules/ws/lib/validation.js104
-rw-r--r--node_modules/ws/lib/websocket-server.js447
-rw-r--r--node_modules/ws/lib/websocket.js1195
-rw-r--r--node_modules/ws/package.json56
-rw-r--r--org.cockpit-project.podman.metainfo.xml16
-rw-r--r--package-lock.json5427
-rw-r--r--package.json61
-rw-r--r--packaging/arch/PKGBUILD15
-rw-r--r--packaging/debian/changelog5
-rw-r--r--packaging/debian/control23
-rw-r--r--packaging/debian/copyright38
-rwxr-xr-xpackaging/debian/rules13
-rw-r--r--packaging/debian/source/format1
-rw-r--r--packaging/debian/source/lintian-overrides2
-rw-r--r--packaging/debian/upstream/metadata4
-rw-r--r--packaging/debian/watch5
-rw-r--r--packit.yaml80
-rw-r--r--pkg/lib/README5
-rw-r--r--pkg/lib/_global-variables.scss14
-rw-r--r--pkg/lib/cockpit-components-context-menu.jsx110
-rw-r--r--pkg/lib/cockpit-components-dialog.jsx372
-rw-r--r--pkg/lib/cockpit-components-dialog.scss8
-rw-r--r--pkg/lib/cockpit-components-dropdown.jsx76
-rw-r--r--pkg/lib/cockpit-components-dynamic-list.jsx143
-rw-r--r--pkg/lib/cockpit-components-dynamic-list.scss39
-rw-r--r--pkg/lib/cockpit-components-empty-state.css3
-rw-r--r--pkg/lib/cockpit-components-empty-state.jsx62
-rw-r--r--pkg/lib/cockpit-components-file-autocomplete.jsx212
-rw-r--r--pkg/lib/cockpit-components-firewalld-request.jsx167
-rw-r--r--pkg/lib/cockpit-components-firewalld-request.scss12
-rw-r--r--pkg/lib/cockpit-components-form-helper.jsx43
-rw-r--r--pkg/lib/cockpit-components-inline-notification.css7
-rw-r--r--pkg/lib/cockpit-components-inline-notification.jsx96
-rw-r--r--pkg/lib/cockpit-components-install-dialog.css43
-rw-r--r--pkg/lib/cockpit-components-install-dialog.jsx211
-rw-r--r--pkg/lib/cockpit-components-listing-panel.jsx87
-rw-r--r--pkg/lib/cockpit-components-listing-panel.scss93
-rw-r--r--pkg/lib/cockpit-components-logs-panel.jsx185
-rw-r--r--pkg/lib/cockpit-components-logs-panel.scss15
-rw-r--r--pkg/lib/cockpit-components-modifications.css28
-rw-r--r--pkg/lib/cockpit-components-modifications.jsx182
-rw-r--r--pkg/lib/cockpit-components-password.jsx188
-rw-r--r--pkg/lib/cockpit-components-password.scss11
-rw-r--r--pkg/lib/cockpit-components-plot.jsx513
-rw-r--r--pkg/lib/cockpit-components-plot.scss119
-rw-r--r--pkg/lib/cockpit-components-privileged.jsx77
-rw-r--r--pkg/lib/cockpit-components-shutdown.jsx248
-rw-r--r--pkg/lib/cockpit-components-shutdown.scss17
-rw-r--r--pkg/lib/cockpit-components-table.jsx297
-rw-r--r--pkg/lib/cockpit-components-table.scss106
-rw-r--r--pkg/lib/cockpit-components-terminal.jsx362
-rw-r--r--pkg/lib/cockpit-components-truncate.jsx42
-rw-r--r--pkg/lib/cockpit-components-truncate.scss4
-rw-r--r--pkg/lib/cockpit-dark-theme.js70
-rw-r--r--pkg/lib/cockpit-po-plugin.js133
-rw-r--r--pkg/lib/cockpit-rsync-plugin.js49
-rw-r--r--pkg/lib/cockpit.js4444
-rw-r--r--pkg/lib/console.css16
-rw-r--r--pkg/lib/context-menu.scss20
-rw-r--r--pkg/lib/credentials-ssh-private-keys.sh34
-rw-r--r--pkg/lib/credentials-ssh-remove-key.sh10
-rw-r--r--pkg/lib/credentials.js344
-rw-r--r--pkg/lib/ct-card.scss63
-rw-r--r--pkg/lib/dialogs.jsx131
-rw-r--r--pkg/lib/esbuild-cleanup-plugin.js17
-rw-r--r--pkg/lib/esbuild-common.js33
-rw-r--r--pkg/lib/esbuild-compress-plugin.js50
-rw-r--r--pkg/lib/esbuild-test-html-plugin.js28
-rw-r--r--pkg/lib/get-timesync-backend.py61
-rw-r--r--pkg/lib/hooks.js326
-rwxr-xr-xpkg/lib/html2po.js235
-rw-r--r--pkg/lib/inotify.py72
-rw-r--r--pkg/lib/journal.css161
-rw-r--r--pkg/lib/journal.js453
-rw-r--r--pkg/lib/long-running-process.js166
-rw-r--r--pkg/lib/machine-info.js259
-rwxr-xr-xpkg/lib/manifest2po.js179
-rw-r--r--pkg/lib/menu-select-widget.scss35
-rw-r--r--pkg/lib/notifications.js167
-rw-r--r--pkg/lib/os-release.js38
-rw-r--r--pkg/lib/packagekit.js528
-rw-r--r--pkg/lib/page.scss197
-rw-r--r--pkg/lib/pam_user_parser.js95
-rw-r--r--pkg/lib/patternfly/_fonts.scss38
-rw-r--r--pkg/lib/patternfly/patternfly-5-cockpit.scss9
-rw-r--r--pkg/lib/patternfly/patternfly-5-overrides.scss391
-rw-r--r--pkg/lib/plot.js574
-rw-r--r--pkg/lib/polyfills.js25
-rw-r--r--pkg/lib/python.js30
-rw-r--r--pkg/lib/qunit-tests.js90
-rw-r--r--pkg/lib/serverTime.js768
-rw-r--r--pkg/lib/serverTime.scss7
-rw-r--r--pkg/lib/service.js344
-rw-r--r--pkg/lib/superuser.js126
-rw-r--r--pkg/lib/table.css138
-rw-r--r--pkg/lib/timeformat.js66
-rw-r--r--pkg/lib/utils.jsx33
-rw-r--r--plans/all.fmf23
-rw-r--r--po/cs.po1511
-rw-r--r--po/de.po1506
-rw-r--r--po/es.po1490
-rw-r--r--po/fi.po1485
-rw-r--r--po/fr.po1530
-rw-r--r--po/ja.po1468
-rw-r--r--po/ka.po1409
-rw-r--r--po/ko.po1462
-rw-r--r--po/pl.po1509
-rw-r--r--po/sk.po1554
-rw-r--r--po/sv.po1485
-rw-r--r--po/tr.po1502
-rw-r--r--po/uk.po1523
-rw-r--r--po/zh_CN.po1480
-rw-r--r--pyproject.toml50
-rw-r--r--src/ContainerCheckpointModal.jsx68
-rw-r--r--src/ContainerCommitModal.jsx166
-rw-r--r--src/ContainerDeleteModal.jsx42
-rw-r--r--src/ContainerDetails.jsx80
-rw-r--r--src/ContainerHeader.jsx37
-rw-r--r--src/ContainerHealthLogs.jsx131
-rw-r--r--src/ContainerIntegration.jsx121
-rw-r--r--src/ContainerLogs.jsx175
-rw-r--r--src/ContainerRenameModal.jsx107
-rw-r--r--src/ContainerRestoreModal.jsx74
-rw-r--r--src/ContainerTerminal.css7
-rw-r--r--src/ContainerTerminal.jsx278
-rw-r--r--src/Containers.jsx877
-rw-r--r--src/Containers.scss93
-rw-r--r--src/Env.jsx96
-rw-r--r--src/ForceRemoveModal.jsx33
-rw-r--r--src/ImageDeleteModal.jsx121
-rw-r--r--src/ImageDetails.jsx47
-rw-r--r--src/ImageHistory.jsx65
-rw-r--r--src/ImageRunModal.jsx1183
-rw-r--r--src/ImageRunModal.scss47
-rw-r--r--src/ImageSearchModal.css59
-rw-r--r--src/ImageSearchModal.jsx218
-rw-r--r--src/ImageUsedBy.jsx44
-rw-r--r--src/Images.css23
-rw-r--r--src/Images.jsx429
-rw-r--r--src/Notification.jsx45
-rw-r--r--src/PodActions.jsx195
-rw-r--r--src/PodCreateModal.jsx220
-rw-r--r--src/PruneUnusedContainersModal.jsx110
-rw-r--r--src/PruneUnusedImagesModal.jsx123
-rw-r--r--src/PublishPort.jsx142
-rw-r--r--src/Volume.jsx90
-rw-r--r--src/app.jsx791
-rw-r--r--src/client.js183
-rw-r--r--src/index.html36
-rw-r--r--src/index.js30
-rw-r--r--src/manifest.json16
-rw-r--r--src/podman.scss149
-rw-r--r--src/rest.js89
-rw-r--r--src/util.js185
-rwxr-xr-xtest/browser/browser.sh88
-rw-r--r--test/browser/main.fmf24
-rwxr-xr-xtest/browser/run-test.sh48
-rwxr-xr-xtest/check-application2871
-rw-r--r--test/common/__init__.py0
-rw-r--r--test/common/cdp.py381
-rwxr-xr-xtest/common/chromium-cdp-driver.js332
-rwxr-xr-xtest/common/firefox-cdp-driver.js392
-rw-r--r--test/common/git-utils.sh150
-rwxr-xr-xtest/common/lcov.py505
-rw-r--r--test/common/link-patterns.json39
-rwxr-xr-xtest/common/make-bots28
-rw-r--r--test/common/netlib.py214
-rw-r--r--test/common/packagelib.py428
-rwxr-xr-xtest/common/pixel-tests261
-rw-r--r--test/common/pixeldiff.html623
-rwxr-xr-xtest/common/pywrap30
-rw-r--r--test/common/ruff.toml11
-rwxr-xr-xtest/common/run-tests585
-rw-r--r--test/common/storagelib.py669
-rwxr-xr-xtest/common/tap-cdp119
-rw-r--r--test/common/test-functions.js360
-rw-r--r--test/common/testlib.py2535
-rw-r--r--test/reference-image1
-rwxr-xr-xtest/run17
-rwxr-xr-xtest/static-code200
-rwxr-xr-xtest/vm.install38
-rwxr-xr-xtools/node-modules198
280 files changed, 101153 insertions, 0 deletions
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 0000000..85f5a45
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,2 @@
+node_modules/*
+pkg/lib/*
diff --git a/.eslintrc.json b/.eslintrc.json
new file mode 100644
index 0000000..5850feb
--- /dev/null
+++ b/.eslintrc.json
@@ -0,0 +1,54 @@
+{
+ "root": true,
+ "env": {
+ "browser": true,
+ "es2022": true
+ },
+ "extends": ["eslint:recommended", "standard", "standard-jsx", "standard-react", "plugin:jsx-a11y/recommended"],
+ "parserOptions": {
+ "ecmaVersion": 2022
+ },
+ "plugins": ["react", "react-hooks", "jsx-a11y"],
+ "rules": {
+ "indent": ["error", 4,
+ {
+ "ObjectExpression": "first",
+ "CallExpression": {"arguments": "first"},
+ "MemberExpression": 2,
+ "ignoredNodes": [ "JSXAttribute" ]
+ }],
+ "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 2 }],
+ "no-var": "error",
+ "lines-between-class-members": ["error", "always", { "exceptAfterSingleLine": true }],
+ "prefer-promise-reject-errors": ["error", { "allowEmptyReject": true }],
+ "react/jsx-indent": ["error", 4],
+ "semi": ["error", "always", { "omitLastInOneLineBlock": true }],
+
+ "camelcase": "off",
+ "comma-dangle": "off",
+ "curly": "off",
+ "jsx-quotes": "off",
+ "no-console": "off",
+ "no-undef": "error",
+ "quotes": "off",
+ "react/jsx-curly-spacing": "off",
+ "react/jsx-indent-props": "off",
+ "react/jsx-closing-bracket-location": "off",
+ "react/jsx-closing-tag-location": "off",
+ "react/jsx-first-prop-new-line": "off",
+ "react/jsx-curly-newline": "off",
+ "react/jsx-handler-names": "off",
+ "react/prop-types": "off",
+ "space-before-function-paren": "off",
+ "standard/no-callback-literal": "off",
+
+ "jsx-a11y/anchor-is-valid": "off",
+
+ "eqeqeq": "off",
+ "react/jsx-no-bind": "off"
+ },
+ "globals": {
+ "require": false,
+ "module": false
+ }
+}
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..cf4c387
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,2 @@
+[flake8]
+max-line-length = 118
diff --git a/.fmf/version b/.fmf/version
new file mode 100644
index 0000000..d00491f
--- /dev/null
+++ b/.fmf/version
@@ -0,0 +1 @@
+1
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..629f4fc
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,31 @@
+---
+name: Bug report
+about: For bugs and general problems
+title:
+labels: 'bug'
+assignees: ''
+
+---
+
+Cockpit version: xxx
+Cockpit-podman version: xxx
+Podman version: xxx
+OS:
+
+<!--- Issue description -->
+
+<!---
+Please help us out by explaining how to reproduce it.
+
+Relevant parts of the system log are also useful:
+`journalctl --since -10m` if the issue happened in the last 10 minutes
+`journalctl -u podman` or `journalctl --user -u podman` depending on the logged in user in cockpit.
+-->
+
+Steps to reproduce
+
+1.
+2.
+3.
+
+<!--- In case the issue is clearly visible, screenshots are very helpful -->
diff --git a/.github/ISSUE_TEMPLATE/enhancement.md b/.github/ISSUE_TEMPLATE/enhancement.md
new file mode 100644
index 0000000..4978991
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/enhancement.md
@@ -0,0 +1,11 @@
+---
+name: Enhancement
+about: For feature requests and discussion of ideas
+title:
+labels: 'enhancement'
+assignees: ''
+
+---
+
+<!--- Feature description -->
+
diff --git a/.github/codeql-config.yml b/.github/codeql-config.yml
new file mode 100644
index 0000000..bd11a70
--- /dev/null
+++ b/.github/codeql-config.yml
@@ -0,0 +1,2 @@
+paths:
+ - src
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
new file mode 100644
index 0000000..a6ca0d0
--- /dev/null
+++ b/.github/dependabot.yml
@@ -0,0 +1,38 @@
+version: 2
+updates:
+ - package-ecosystem: "npm"
+ directory: "/"
+ schedule:
+ interval: "daily"
+ # run these when most of our developers don't work, don't DoS our CI over the day
+ time: "22:00"
+ timezone: "Europe/Berlin"
+ open-pull-requests-limit: 3
+ commit-message:
+ prefix: "[no-test]"
+ labels:
+ - "node_modules"
+ groups:
+ eslint:
+ patterns:
+ - "eslint*"
+ esbuild:
+ patterns:
+ - "esbuild*"
+ stylelint:
+ patterns:
+ - "stylelint*"
+ xterm:
+ patterns:
+ - "xterm*"
+ patternfly:
+ patterns:
+ - "@patternfly*"
+
+ - package-ecosystem: "github-actions"
+ directory: "/"
+ open-pull-requests-limit: 3
+ labels:
+ - "no-test"
+ schedule:
+ interval: "weekly"
diff --git a/.github/workflows/cockpit-lib-update.yml b/.github/workflows/cockpit-lib-update.yml
new file mode 100644
index 0000000..37c398a
--- /dev/null
+++ b/.github/workflows/cockpit-lib-update.yml
@@ -0,0 +1,33 @@
+name: cockpit-lib-update
+on:
+ schedule:
+ - cron: '0 2 * * 4'
+ # can be run manually on https://github.com/cockpit-project/starter-kit/actions
+ workflow_dispatch:
+jobs:
+ cockpit-lib-update:
+ environment: self
+ runs-on: ubuntu-latest
+ permissions:
+ pull-requests: write
+ statuses: write
+ steps:
+ - name: Set up dependencies
+ run: |
+ sudo apt update
+ sudo apt install -y make
+
+ - name: Set up configuration and secrets
+ run: |
+ printf '[user]\n\tname = Cockpit Project\n\temail=cockpituous@gmail.com\n' > ~/.gitconfig
+ echo '${{ secrets.GITHUB_TOKEN }}' > ~/.config/github-token
+
+ - name: Clone repository
+ uses: actions/checkout@v4
+ with:
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
+
+ - name: Run cockpit-lib-update
+ run: |
+ make bots
+ bots/cockpit-lib-update
diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml
new file mode 100644
index 0000000..ad050d0
--- /dev/null
+++ b/.github/workflows/codeql.yml
@@ -0,0 +1,33 @@
+name: CodeQL
+on: [push, pull_request]
+
+jobs:
+ analyze:
+ name: Analyze
+ runs-on: ubuntu-22.04
+ permissions:
+ actions: read
+ contents: read
+ security-events: write
+
+ strategy:
+ fail-fast: false
+ matrix:
+ language:
+ - javascript
+
+ steps:
+ - name: Checkout
+ uses: actions/checkout@v4
+
+ - name: Initialize CodeQL
+ uses: github/codeql-action/init@v3
+ with:
+ languages: ${{ matrix.language }}
+ queries: +security-and-quality
+ config-file: ./.github/codeql-config.yml
+
+ - name: Perform CodeQL Analysis
+ uses: github/codeql-action/analyze@v3
+ with:
+ category: "/language:${{ matrix.language }}"
diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml
new file mode 100644
index 0000000..0ab8975
--- /dev/null
+++ b/.github/workflows/dependabot.yml
@@ -0,0 +1,82 @@
+name: update node_modules
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize, labeled]
+
+jobs:
+ dependabot:
+ environment: npm-update
+ permissions:
+ contents: read
+ pull-requests: write
+ timeout-minutes: 5
+ # 22.04's podman has issues with piping and causes tar errors
+ runs-on: ubuntu-20.04
+ if: ${{ contains(github.event.pull_request.labels.*.name, 'node_modules') }}
+
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+ with:
+ ref: ${{ github.event.pull_request.head.ref }}
+ repository: ${{ github.event.pull_request.head.repo.full_name }}
+ fetch-depth: 0
+
+ - name: Clear node_modules label
+ uses: actions/github-script@v7
+ with:
+ script: |
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: 'node_modules'
+ });
+ } catch (e) {
+ if (e.name == 'HttpError' && e.status == 404) {
+ /* expected: 404 if label is unset */
+ } else {
+ throw e;
+ }
+ }
+
+ - name: Update node_modules for package.json changes
+ run: |
+ make tools/node-modules
+ git config --global user.name "GitHub Workflow"
+ git config --global user.email "cockpituous@cockpit-project.org"
+ eval $(ssh-agent)
+ ssh-add - <<< '${{ secrets.NODE_CACHE_DEPLOY_KEY }}'
+ ./tools/node-modules install
+ ./tools/node-modules push
+ git add node_modules
+ ssh-add -D
+ ssh-agent -k
+
+ - name: Clear [no-test] prefix from PR title
+ if: ${{ contains(github.event.pull_request.title, '[no-test]') }}
+ uses: actions/github-script@v7
+ env:
+ TITLE: '${{ github.event.pull_request.title }}'
+ with:
+ script: |
+ const title = process.env['TITLE'].replace(/\[no-test\]\W+ /, '')
+ await github.rest.pulls.update({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ pull_number: context.issue.number,
+ title,
+ });
+
+ - name: Force push node_modules update
+ run: |
+ # Dependabot prefixes the commit with [no-test] which we don't want to keep in the commit
+ title=$(git show --pretty="%s" -s | sed -E "s/\[no-test\]\W+ //")
+ body=$(git show -s --pretty="%b")
+ git commit --amend -m "${title}" -m "${body}" --no-edit node_modules
+ eval $(ssh-agent)
+ ssh-add - <<< '${{ secrets.SELF_DEPLOY_KEY }}'
+ git push --force 'git@github.com:${{ github.repository }}' '${{ github.head_ref }}'
+ ssh-add -D
+ ssh-agent -k
diff --git a/.github/workflows/nightly.yml b/.github/workflows/nightly.yml
new file mode 100644
index 0000000..9f7de13
--- /dev/null
+++ b/.github/workflows/nightly.yml
@@ -0,0 +1,22 @@
+name: nightly
+on:
+ schedule:
+ - cron: '0 1 * * *'
+ # can be run manually on https://github.com/cockpit-project/cockpit-podman/actions
+ workflow_dispatch:
+jobs:
+ trigger:
+ permissions:
+ statuses: write
+ runs-on: ubuntu-22.04
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Trigger updates-testing scenario
+ run: |
+ make bots
+ mkdir -p ~/.config/cockpit-dev
+ echo "${{ github.token }}" >> ~/.config/cockpit-dev/github-token
+ TEST_OS=$(PYTHONPATH=bots python3 -c 'from lib.constants import TEST_OS_DEFAULT; print(TEST_OS_DEFAULT)')
+ bots/tests-trigger --force "-" "${TEST_OS}/updates-testing" "${TEST_OS}/podman-next"
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 0000000..807860b
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,68 @@
+name: release
+on:
+ push:
+ tags:
+ # this is a glob, not a regexp
+ - '[0-9]*'
+jobs:
+ source:
+ runs-on: ubuntu-latest
+ container:
+ image: quay.io/cockpit/tasks:latest
+ options: --user root
+ permissions:
+ # create GitHub release
+ contents: write
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ # https://github.blog/2022-04-12-git-security-vulnerability-announced/
+ - name: Pacify git's permission check
+ run: git config --global --add safe.directory /__w/cockpit-podman/cockpit-podman
+
+ - name: Workaround for https://github.com/actions/checkout/pull/697
+ run: git fetch --force origin $(git describe --tags):refs/tags/$(git describe --tags)
+
+ - name: Build release
+ run: make dist
+
+ - name: Publish GitHub release
+ uses: cockpit-project/action-release@7d2e2657382e8d34f88a24b5987f2b81ea165785
+ with:
+ filename: "cockpit-podman-${{ github.ref_name }}.tar.xz"
+
+ node-cache:
+ # doesn't depend on it, but let's make sure the build passes before we do this
+ needs: [source]
+ runs-on: ubuntu-latest
+ environment: node-cache
+ # done via deploy key, token needs no write permissions at all
+ permissions: {}
+ steps:
+ - name: Clone repository
+ uses: actions/checkout@v4
+
+ - name: Set up git
+ run: |
+ git config user.name "GitHub Workflow"
+ git config user.email "cockpituous@cockpit-project.org"
+
+ - name: Tag node-cache
+ run: |
+ set -eux
+ # this is a shared repo, prefix with project name
+ TAG="${GITHUB_REPOSITORY#*/}-$(basename $GITHUB_REF)"
+ make tools/node-modules
+ tools/node-modules checkout
+ cd node_modules
+ git tag "$TAG"
+ git remote add cache "ssh://git@github.com/${GITHUB_REPOSITORY%/*}/node-cache"
+ eval $(ssh-agent)
+ ssh-add - <<< '${{ secrets.DEPLOY_KEY }}'
+ # make this idempotent: delete an existing tag
+ git push cache :"$TAG" || true
+ git push cache tag "$TAG"
+ ssh-add -D
diff --git a/.github/workflows/reposchutz.yml b/.github/workflows/reposchutz.yml
new file mode 100644
index 0000000..bdadab6
--- /dev/null
+++ b/.github/workflows/reposchutz.yml
@@ -0,0 +1,66 @@
+name: repository
+on:
+ pull_request_target:
+ types: [opened, reopened, synchronize, labeled, unlabeled]
+
+jobs:
+ check:
+ name: Protection checks
+ # 22.04's podman has issues with piping and causes tar errors
+ runs-on: ubuntu-20.04
+ permissions:
+ contents: read
+ pull-requests: write
+ timeout-minutes: 5
+ env:
+ HEAD_SHA: ${{ github.event.pull_request.head.sha }}
+ BASE_SHA: ${{ github.event.pull_request.base.sha }}
+
+ steps:
+ - name: Clone target branch
+ uses: actions/checkout@v4
+ with:
+ fetch-depth: 0
+
+ - name: Fetch PR commits
+ run: git fetch origin "${BASE_SHA}" "${HEAD_SHA}"
+
+ - name: Clear .github-changes label
+ if: ${{ !endsWith(github.event.action, 'labeled') }}
+ uses: actions/github-script@v7
+ with:
+ script: |
+ try {
+ await github.rest.issues.removeLabel({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ issue_number: context.issue.number,
+ name: '.github-changes'
+ });
+ } catch (e) {
+ if (e.name == 'HttpError' && e.status == 404) {
+ /* expected: 404 if label is unset */
+ } else {
+ throw e;
+ }
+ }
+
+ - name: Check for .github changes
+ # We want to run this check any time the .github-changes label is not
+ # set, which needs to include the case where we just unset it above.
+ if: ${{ !endsWith(github.event.action, 'labeled') ||
+ !contains(github.event.pull_request.labels.*.name, '.github-changes') }}
+ run: |
+ set -x
+ git log --full-history --exit-code --patch "${HEAD_SHA}" --not "${BASE_SHA}" -- .github >&2
+
+ - name: Check for node_modules availability and package.json consistency
+ run: |
+ # Make tools/node-modules available
+ make tools/node-modules
+ # for each commit in the PR which modifies package.json or node_modules...
+ for commit in $(git log --reverse --full-history --format=%H \
+ "${HEAD_SHA}" --not "${BASE_SHA}" -- package.json node_modules); do
+ # ... check that package.json and node_modules/.package.json are in sync
+ tools/node-modules verify "${commit}"
+ done
diff --git a/.github/workflows/weblate-sync-po.yml b/.github/workflows/weblate-sync-po.yml
new file mode 100644
index 0000000..a74d300
--- /dev/null
+++ b/.github/workflows/weblate-sync-po.yml
@@ -0,0 +1,46 @@
+name: weblate-sync-po
+on:
+ schedule:
+ # Run this on Tuesday evening (UTC), so that it's ready for release on
+ # Wednesday, with some spare time
+ - cron: '0 18 * * 2'
+ # can be run manually on https://github.com/cockpit-project/cockpit/actions
+ workflow_dispatch:
+
+jobs:
+ po-refresh:
+ environment: self
+ permissions:
+ pull-requests: write
+ statuses: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up dependencies
+ run: |
+ sudo apt update
+ sudo apt install -y --no-install-recommends gettext
+
+ - name: Clone source repository
+ uses: actions/checkout@v4
+ with:
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
+ path: src
+
+ - name: Clone weblate repository
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ github.repository }}-weblate
+ path: weblate
+
+ - name: Copy .po files from weblate repository
+ run: cp weblate/*.po src/po/
+
+ - name: Run po-refresh bot
+ run: |
+ cd src
+ make bots
+ git config --global user.name "GitHub Workflow"
+ git config --global user.email "cockpituous@cockpit-project.org"
+ mkdir -p ~/.config/cockpit-dev
+ echo ${{ github.token }} >> ~/.config/cockpit-dev/github-token
+ PO_REFRESH_NO_SYNC=1 bots/po-refresh
diff --git a/.github/workflows/weblate-sync-pot.yml b/.github/workflows/weblate-sync-pot.yml
new file mode 100644
index 0000000..8221884
--- /dev/null
+++ b/.github/workflows/weblate-sync-pot.yml
@@ -0,0 +1,42 @@
+name: weblate-sync-pot
+on:
+ schedule:
+ # Run this every morning
+ - cron: '45 2 * * *'
+ # can be run manually on https://github.com/cockpit-project/cockpit-podman/actions
+ workflow_dispatch:
+
+jobs:
+ pot-upload:
+ environment: cockpit-podman-weblate
+ permissions:
+ pull-requests: write
+ runs-on: ubuntu-latest
+ steps:
+ - name: Set up dependencies
+ run: |
+ sudo apt update
+ sudo apt install -y --no-install-recommends npm make gettext appstream
+
+ - name: Clone source repository
+ uses: actions/checkout@v4
+ with:
+ path: src
+
+ - name: Generate .pot file
+ run: make -C src po/podman.pot
+
+ - name: Clone weblate repository
+ uses: actions/checkout@v4
+ with:
+ path: weblate
+ repository: ${{ github.repository }}-weblate
+ ssh-key: ${{ secrets.DEPLOY_KEY }}
+
+ - name: Commit .pot to weblate repo
+ run: |
+ cp src/po/podman.pot weblate/podman.pot
+ git config --global user.name "GitHub Workflow"
+ git config --global user.email "cockpituous@cockpit-project.org"
+ git -C weblate commit -m "Update source file" -- podman.pot
+ git -C weblate push
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..07d6855
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,34 @@
+# Please keep this file sorted (LC_COLLATE=C.UTF-8),
+# grouped into the 3 categories below:
+# - general patterns (match in all directories)
+# - patterns to match files at the toplevel
+# - patterns to match files in subdirs
+
+# general patterns
+*.pyc
+*.rpm
+
+# toplevel (/...)
+/Test*.html
+/Test*.json
+/Test*.log
+/Test*.log.gz
+/Test*.png
+/*.whl
+/bots
+/cockpit-*.tar.xz
+/cockpit-podman.spec
+/dist/
+/package-lock.json
+/pkg/
+/tmp/
+/tools/
+
+# subdirs (/subdir/...)
+/packaging/arch/PKGBUILD
+/packaging/debian/changelog
+/po/*.pot
+ /po/LINGUAS
+/test/common/
+/test/images/
+/test/static-code
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..2ca4fcf
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,7 @@
+[submodule "test/reference"]
+ path = test/reference
+ url = https://github.com/cockpit-project/pixel-test-reference
+ branch = empty
+[submodule "node_modules"]
+ path = node_modules
+ url = https://github.com/cockpit-project/node-cache.git
diff --git a/.stylelintrc.json b/.stylelintrc.json
new file mode 100644
index 0000000..67a29f0
--- /dev/null
+++ b/.stylelintrc.json
@@ -0,0 +1,39 @@
+{
+ "extends": "stylelint-config-standard-scss",
+ "plugins": [
+ "stylelint-use-logical-spec"
+ ],
+ "rules": {
+ "at-rule-empty-line-before": null,
+ "declaration-empty-line-before": null,
+ "custom-property-empty-line-before": null,
+ "comment-empty-line-before": null,
+ "scss/double-slash-comment-empty-line-before": null,
+ "scss/dollar-variable-colon-space-after": null,
+
+ "custom-property-pattern": null,
+ "declaration-block-no-duplicate-properties": null,
+ "declaration-block-no-redundant-longhand-properties": null,
+ "declaration-block-no-shorthand-property-overrides": null,
+ "declaration-block-single-line-max-declarations": null,
+ "font-family-no-duplicate-names": null,
+ "function-url-quotes": null,
+ "keyframes-name-pattern": null,
+ "media-feature-range-notation": "prefix",
+ "no-descending-specificity": null,
+ "no-duplicate-selectors": null,
+ "scss/at-extend-no-missing-placeholder": null,
+ "scss/at-import-partial-extension": null,
+ "scss/at-import-no-partial-leading-underscore": null,
+ "scss/load-no-partial-leading-underscore": true,
+ "scss/at-mixin-pattern": null,
+ "scss/comment-no-empty": null,
+ "scss/dollar-variable-pattern": null,
+ "scss/double-slash-comment-whitespace-inside": null,
+ "scss/no-global-function-names": null,
+ "scss/operator-no-unspaced": null,
+ "selector-class-pattern": null,
+ "selector-id-pattern": null,
+ "liberty/use-logical-spec": "always"
+ }
+}
diff --git a/HACKING.md b/HACKING.md
new file mode 100644
index 0000000..9b03381
--- /dev/null
+++ b/HACKING.md
@@ -0,0 +1,107 @@
+# Hacking on Cockpit Podman
+
+The commands here assume you're in the top level of the Cockpit Podman git
+repository checkout.
+
+## Running out of git checkout
+
+For development, you usually want to run your module straight out of the git
+tree. To do that, run `make devel-install`, which links your checkout to the
+location were `cockpit-bridge` looks for packages. If you prefer to do this
+manually:
+
+```
+mkdir -p ~/.local/share/cockpit
+ln -s `pwd`/dist ~/.local/share/cockpit/podman
+```
+
+After changing the code and running `make` again, reload the Cockpit page in
+your browser.
+
+You can also use
+[watch mode](https://esbuild.github.io/api/#watch) to
+automatically update the bundle on every code change with
+
+ $ make watch
+
+When developing against a virtual machine, watch mode can also automatically upload
+the code changes by setting the `RSYNC` environment variable to
+the remote hostname.
+
+ $ RSYNC=c make watch
+
+When developing against a remote host as a normal user, `RSYNC_DEVEL` can be
+set to upload code changes to `~/.local/share/cockpit/` instead of
+`/usr/local`.
+
+ $ RSYNC_DEVEL=example.com make watch
+
+## Running eslint
+
+Cockpit Podman uses [ESLint](https://eslint.org/) to automatically check
+JavaScript code style in `.jsx` and `.js` files.
+
+eslint is executed as part of `test/static-code`, aka. `make codecheck`.
+
+For developer convenience, the ESLint can be started explicitly by:
+
+ $ npm run eslint
+
+Violations of some rules can be fixed automatically by:
+
+ $ npm run eslint:fix
+
+Rules configuration can be found in the `.eslintrc.json` file.
+
+## Running stylelint
+
+Cockpit uses [Stylelint](https://stylelint.io/) to automatically check CSS code
+style in `.css` and `scss` files.
+
+styleint is executed as part of `test/static-code`, aka. `make codecheck`.
+
+For developer convenience, the Stylelint can be started explicitly by:
+
+ $ npm run stylelint
+
+Violations of some rules can be fixed automatically by:
+
+ $ npm run stylelint:fix
+
+Rules configuration can be found in the `.stylelintrc.json` file.
+
+# Running tests locally
+
+Run `make vm` to build an RPM and install it into a standard Cockpit test VM.
+This will be `fedora-39` by default. You can set `$TEST_OS` to use a different
+image, for example
+
+ TEST_OS=centos-8-stream make vm
+
+Then run
+
+ make test/common
+
+to pull in [Cockpit's shared test API](https://github.com/cockpit-project/cockpit/tree/main/test/common)
+for running Chrome DevTools Protocol based browser tests.
+
+With this preparation, you can manually run a single test without
+rebuilding the VM, possibly with extra options for tracing and halting on test
+failures (for interactive debugging):
+
+ TEST_OS=... test/check-application TestApplication.testRunImageSystem -stv
+
+Use this command to list all known tests:
+
+ test/check-application -l
+
+You can also run all of the tests:
+
+ TEST_OS=centos-8-stream make check
+
+However, this is rather expensive, and most of the time it's better to let the
+CI machinery do this on a draft pull request.
+
+Please see [Cockpit's test documentation](https://github.com/cockpit-project/cockpit/blob/main/test/README.md)
+for details how to run against existing VMs, interactive browser window,
+interacting with the test VM, and more.
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..4362b49
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,502 @@
+ GNU LESSER GENERAL PUBLIC LICENSE
+ Version 2.1, February 1999
+
+ Copyright (C) 1991, 1999 Free Software Foundation, Inc.
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+[This is the first released version of the Lesser GPL. It also counts
+ as the successor of the GNU Library Public License, version 2, hence
+ the version number 2.1.]
+
+ Preamble
+
+ The licenses for most software are designed to take away your
+freedom to share and change it. By contrast, the GNU General Public
+Licenses are intended to guarantee your freedom to share and change
+free software--to make sure the software is free for all its users.
+
+ This license, the Lesser General Public License, applies to some
+specially designated software packages--typically libraries--of the
+Free Software Foundation and other authors who decide to use it. You
+can use it too, but we suggest you first think carefully about whether
+this license or the ordinary General Public License is the better
+strategy to use in any particular case, based on the explanations below.
+
+ When we speak of free software, we are referring to freedom of use,
+not price. Our General Public Licenses are designed to make sure that
+you have the freedom to distribute copies of free software (and charge
+for this service if you wish); that you receive source code or can get
+it if you want it; that you can change the software and use pieces of
+it in new free programs; and that you are informed that you can do
+these things.
+
+ To protect your rights, we need to make restrictions that forbid
+distributors to deny you these rights or to ask you to surrender these
+rights. These restrictions translate to certain responsibilities for
+you if you distribute copies of the library or if you modify it.
+
+ For example, if you distribute copies of the library, whether gratis
+or for a fee, you must give the recipients all the rights that we gave
+you. You must make sure that they, too, receive or can get the source
+code. If you link other code with the library, you must provide
+complete object files to the recipients, so that they can relink them
+with the library after making changes to the library and recompiling
+it. And you must show them these terms so they know their rights.
+
+ We protect your rights with a two-step method: (1) we copyright the
+library, and (2) we offer you this license, which gives you legal
+permission to copy, distribute and/or modify the library.
+
+ To protect each distributor, we want to make it very clear that
+there is no warranty for the free library. Also, if the library is
+modified by someone else and passed on, the recipients should know
+that what they have is not the original version, so that the original
+author's reputation will not be affected by problems that might be
+introduced by others.
+
+ Finally, software patents pose a constant threat to the existence of
+any free program. We wish to make sure that a company cannot
+effectively restrict the users of a free program by obtaining a
+restrictive license from a patent holder. Therefore, we insist that
+any patent license obtained for a version of the library must be
+consistent with the full freedom of use specified in this license.
+
+ Most GNU software, including some libraries, is covered by the
+ordinary GNU General Public License. This license, the GNU Lesser
+General Public License, applies to certain designated libraries, and
+is quite different from the ordinary General Public License. We use
+this license for certain libraries in order to permit linking those
+libraries into non-free programs.
+
+ When a program is linked with a library, whether statically or using
+a shared library, the combination of the two is legally speaking a
+combined work, a derivative of the original library. The ordinary
+General Public License therefore permits such linking only if the
+entire combination fits its criteria of freedom. The Lesser General
+Public License permits more lax criteria for linking other code with
+the library.
+
+ We call this license the "Lesser" General Public License because it
+does Less to protect the user's freedom than the ordinary General
+Public License. It also provides other free software developers Less
+of an advantage over competing non-free programs. These disadvantages
+are the reason we use the ordinary General Public License for many
+libraries. However, the Lesser license provides advantages in certain
+special circumstances.
+
+ For example, on rare occasions, there may be a special need to
+encourage the widest possible use of a certain library, so that it becomes
+a de-facto standard. To achieve this, non-free programs must be
+allowed to use the library. A more frequent case is that a free
+library does the same job as widely used non-free libraries. In this
+case, there is little to gain by limiting the free library to free
+software only, so we use the Lesser General Public License.
+
+ In other cases, permission to use a particular library in non-free
+programs enables a greater number of people to use a large body of
+free software. For example, permission to use the GNU C Library in
+non-free programs enables many more people to use the whole GNU
+operating system, as well as its variant, the GNU/Linux operating
+system.
+
+ Although the Lesser General Public License is Less protective of the
+users' freedom, it does ensure that the user of a program that is
+linked with the Library has the freedom and the wherewithal to run
+that program using a modified version of the Library.
+
+ The precise terms and conditions for copying, distribution and
+modification follow. Pay close attention to the difference between a
+"work based on the library" and a "work that uses the library". The
+former contains code derived from the library, whereas the latter must
+be combined with the library in order to run.
+
+ GNU LESSER GENERAL PUBLIC LICENSE
+ TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+ 0. This License Agreement applies to any software library or other
+program which contains a notice placed by the copyright holder or
+other authorized party saying it may be distributed under the terms of
+this Lesser General Public License (also called "this License").
+Each licensee is addressed as "you".
+
+ A "library" means a collection of software functions and/or data
+prepared so as to be conveniently linked with application programs
+(which use some of those functions and data) to form executables.
+
+ The "Library", below, refers to any such software library or work
+which has been distributed under these terms. A "work based on the
+Library" means either the Library or any derivative work under
+copyright law: that is to say, a work containing the Library or a
+portion of it, either verbatim or with modifications and/or translated
+straightforwardly into another language. (Hereinafter, translation is
+included without limitation in the term "modification".)
+
+ "Source code" for a work means the preferred form of the work for
+making modifications to it. For a library, complete source code means
+all the source code for all modules it contains, plus any associated
+interface definition files, plus the scripts used to control compilation
+and installation of the library.
+
+ Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope. The act of
+running a program using the Library is not restricted, and output from
+such a program is covered only if its contents constitute a work based
+on the Library (independent of the use of the Library in a tool for
+writing it). Whether that is true depends on what the Library does
+and what the program that uses the Library does.
+
+ 1. You may copy and distribute verbatim copies of the Library's
+complete source code as you receive it, in any medium, provided that
+you conspicuously and appropriately publish on each copy an
+appropriate copyright notice and disclaimer of warranty; keep intact
+all the notices that refer to this License and to the absence of any
+warranty; and distribute a copy of this License along with the
+Library.
+
+ You may charge a fee for the physical act of transferring a copy,
+and you may at your option offer warranty protection in exchange for a
+fee.
+
+ 2. You may modify your copy or copies of the Library or any portion
+of it, thus forming a work based on the Library, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+ a) The modified work must itself be a software library.
+
+ b) You must cause the files modified to carry prominent notices
+ stating that you changed the files and the date of any change.
+
+ c) You must cause the whole of the work to be licensed at no
+ charge to all third parties under the terms of this License.
+
+ d) If a facility in the modified Library refers to a function or a
+ table of data to be supplied by an application program that uses
+ the facility, other than as an argument passed when the facility
+ is invoked, then you must make a good faith effort to ensure that,
+ in the event an application does not supply such function or
+ table, the facility still operates, and performs whatever part of
+ its purpose remains meaningful.
+
+ (For example, a function in a library to compute square roots has
+ a purpose that is entirely well-defined independent of the
+ application. Therefore, Subsection 2d requires that any
+ application-supplied function or table used by this function must
+ be optional: if the application does not supply it, the square
+ root function must still compute square roots.)
+
+These requirements apply to the modified work as a whole. If
+identifiable sections of that work are not derived from the Library,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works. But when you
+distribute the same sections as part of a whole which is a work based
+on the Library, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote
+it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Library.
+
+In addition, mere aggregation of another work not based on the Library
+with the Library (or with a work based on the Library) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+ 3. You may opt to apply the terms of the ordinary GNU General Public
+License instead of this License to a given copy of the Library. To do
+this, you must alter all the notices that refer to this License, so
+that they refer to the ordinary GNU General Public License, version 2,
+instead of to this License. (If a newer version than version 2 of the
+ordinary GNU General Public License has appeared, then you can specify
+that version instead if you wish.) Do not make any other change in
+these notices.
+
+ Once this change is made in a given copy, it is irreversible for
+that copy, so the ordinary GNU General Public License applies to all
+subsequent copies and derivative works made from that copy.
+
+ This option is useful when you wish to copy part of the code of
+the Library into a program that is not a library.
+
+ 4. You may copy and distribute the Library (or a portion or
+derivative of it, under Section 2) in object code or executable form
+under the terms of Sections 1 and 2 above provided that you accompany
+it with the complete corresponding machine-readable source code, which
+must be distributed under the terms of Sections 1 and 2 above on a
+medium customarily used for software interchange.
+
+ If distribution of object code is made by offering access to copy
+from a designated place, then offering equivalent access to copy the
+source code from the same place satisfies the requirement to
+distribute the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+ 5. A program that contains no derivative of any portion of the
+Library, but is designed to work with the Library by being compiled or
+linked with it, is called a "work that uses the Library". Such a
+work, in isolation, is not a derivative work of the Library, and
+therefore falls outside the scope of this License.
+
+ However, linking a "work that uses the Library" with the Library
+creates an executable that is a derivative of the Library (because it
+contains portions of the Library), rather than a "work that uses the
+library". The executable is therefore covered by this License.
+Section 6 states terms for distribution of such executables.
+
+ When a "work that uses the Library" uses material from a header file
+that is part of the Library, the object code for the work may be a
+derivative work of the Library even though the source code is not.
+Whether this is true is especially significant if the work can be
+linked without the Library, or if the work is itself a library. The
+threshold for this to be true is not precisely defined by law.
+
+ If such an object file uses only numerical parameters, data
+structure layouts and accessors, and small macros and small inline
+functions (ten lines or less in length), then the use of the object
+file is unrestricted, regardless of whether it is legally a derivative
+work. (Executables containing this object code plus portions of the
+Library will still fall under Section 6.)
+
+ Otherwise, if the work is a derivative of the Library, you may
+distribute the object code for the work under the terms of Section 6.
+Any executables containing that work also fall under Section 6,
+whether or not they are linked directly with the Library itself.
+
+ 6. As an exception to the Sections above, you may also combine or
+link a "work that uses the Library" with the Library to produce a
+work containing portions of the Library, and distribute that work
+under terms of your choice, provided that the terms permit
+modification of the work for the customer's own use and reverse
+engineering for debugging such modifications.
+
+ You must give prominent notice with each copy of the work that the
+Library is used in it and that the Library and its use are covered by
+this License. You must supply a copy of this License. If the work
+during execution displays copyright notices, you must include the
+copyright notice for the Library among them, as well as a reference
+directing the user to the copy of this License. Also, you must do one
+of these things:
+
+ a) Accompany the work with the complete corresponding
+ machine-readable source code for the Library including whatever
+ changes were used in the work (which must be distributed under
+ Sections 1 and 2 above); and, if the work is an executable linked
+ with the Library, with the complete machine-readable "work that
+ uses the Library", as object code and/or source code, so that the
+ user can modify the Library and then relink to produce a modified
+ executable containing the modified Library. (It is understood
+ that the user who changes the contents of definitions files in the
+ Library will not necessarily be able to recompile the application
+ to use the modified definitions.)
+
+ b) Use a suitable shared library mechanism for linking with the
+ Library. A suitable mechanism is one that (1) uses at run time a
+ copy of the library already present on the user's computer system,
+ rather than copying library functions into the executable, and (2)
+ will operate properly with a modified version of the library, if
+ the user installs one, as long as the modified version is
+ interface-compatible with the version that the work was made with.
+
+ c) Accompany the work with a written offer, valid for at
+ least three years, to give the same user the materials
+ specified in Subsection 6a, above, for a charge no more
+ than the cost of performing this distribution.
+
+ d) If distribution of the work is made by offering access to copy
+ from a designated place, offer equivalent access to copy the above
+ specified materials from the same place.
+
+ e) Verify that the user has already received a copy of these
+ materials or that you have already sent this user a copy.
+
+ For an executable, the required form of the "work that uses the
+Library" must include any data and utility programs needed for
+reproducing the executable from it. However, as a special exception,
+the materials to be distributed need not include anything that is
+normally distributed (in either source or binary form) with the major
+components (compiler, kernel, and so on) of the operating system on
+which the executable runs, unless that component itself accompanies
+the executable.
+
+ It may happen that this requirement contradicts the license
+restrictions of other proprietary libraries that do not normally
+accompany the operating system. Such a contradiction means you cannot
+use both them and the Library together in an executable that you
+distribute.
+
+ 7. You may place library facilities that are a work based on the
+Library side-by-side in a single library together with other library
+facilities not covered by this License, and distribute such a combined
+library, provided that the separate distribution of the work based on
+the Library and of the other library facilities is otherwise
+permitted, and provided that you do these two things:
+
+ a) Accompany the combined library with a copy of the same work
+ based on the Library, uncombined with any other library
+ facilities. This must be distributed under the terms of the
+ Sections above.
+
+ b) Give prominent notice with the combined library of the fact
+ that part of it is a work based on the Library, and explaining
+ where to find the accompanying uncombined form of the same work.
+
+ 8. You may not copy, modify, sublicense, link with, or distribute
+the Library except as expressly provided under this License. Any
+attempt otherwise to copy, modify, sublicense, link with, or
+distribute the Library is void, and will automatically terminate your
+rights under this License. However, parties who have received copies,
+or rights, from you under this License will not have their licenses
+terminated so long as such parties remain in full compliance.
+
+ 9. You are not required to accept this License, since you have not
+signed it. However, nothing else grants you permission to modify or
+distribute the Library or its derivative works. These actions are
+prohibited by law if you do not accept this License. Therefore, by
+modifying or distributing the Library (or any work based on the
+Library), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Library or works based on it.
+
+ 10. Each time you redistribute the Library (or any work based on the
+Library), the recipient automatically receives a license from the
+original licensor to copy, distribute, link with or modify the Library
+subject to these terms and conditions. You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties with
+this License.
+
+ 11. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Library at all. For example, if a patent
+license would not permit royalty-free redistribution of the Library by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Library.
+
+If any portion of this section is held invalid or unenforceable under any
+particular circumstance, the balance of the section is intended to apply,
+and the section as a whole is intended to apply in other circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system which is
+implemented by public license practices. Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+ 12. If the distribution and/or use of the Library is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Library under this License may add
+an explicit geographical distribution limitation excluding those countries,
+so that distribution is permitted only in or among countries not thus
+excluded. In such case, this License incorporates the limitation as if
+written in the body of this License.
+
+ 13. The Free Software Foundation may publish revised and/or new
+versions of the Lesser General Public License from time to time.
+Such new versions will be similar in spirit to the present version,
+but may differ in detail to address new problems or concerns.
+
+Each version is given a distinguishing version number. If the Library
+specifies a version number of this License which applies to it and
+"any later version", you have the option of following the terms and
+conditions either of that version or of any later version published by
+the Free Software Foundation. If the Library does not specify a
+license version number, you may choose any version ever published by
+the Free Software Foundation.
+
+ 14. If you wish to incorporate parts of the Library into other free
+programs whose distribution conditions are incompatible with these,
+write to the author to ask for permission. For software which is
+copyrighted by the Free Software Foundation, write to the Free
+Software Foundation; we sometimes make exceptions for this. Our
+decision will be guided by the two goals of preserving the free status
+of all derivatives of our free software and of promoting the sharing
+and reuse of software generally.
+
+ NO WARRANTY
+
+ 15. BECAUSE THE LIBRARY IS LICENSED FREE OF CHARGE, THERE IS NO
+WARRANTY FOR THE LIBRARY, TO THE EXTENT PERMITTED BY APPLICABLE LAW.
+EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR
+OTHER PARTIES PROVIDE THE LIBRARY "AS IS" WITHOUT WARRANTY OF ANY
+KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE
+LIBRARY IS WITH YOU. SHOULD THE LIBRARY PROVE DEFECTIVE, YOU ASSUME
+THE COST OF ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN
+WRITING WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY
+AND/OR REDISTRIBUTE THE LIBRARY AS PERMITTED ABOVE, BE LIABLE TO YOU
+FOR DAMAGES, INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR
+CONSEQUENTIAL DAMAGES ARISING OUT OF THE USE OR INABILITY TO USE THE
+LIBRARY (INCLUDING BUT NOT LIMITED TO LOSS OF DATA OR DATA BEING
+RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD PARTIES OR A
+FAILURE OF THE LIBRARY TO OPERATE WITH ANY OTHER SOFTWARE), EVEN IF
+SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF SUCH
+DAMAGES.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Libraries
+
+ If you develop a new library, and you want it to be of the greatest
+possible use to the public, we recommend making it free software that
+everyone can redistribute and change. You can do so by permitting
+redistribution under these terms (or, alternatively, under the terms of the
+ordinary General Public License).
+
+ To apply these terms, attach the following notices to the library. It is
+safest to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least the
+"copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the library's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ This library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ This library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+Also add information on how to contact you by electronic and paper mail.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the library, if
+necessary. Here is a sample; alter the names:
+
+ Yoyodyne, Inc., hereby disclaims all copyright interest in the
+ library `Frob' (a library for tweaking knobs) written by James Random Hacker.
+
+ <signature of Ty Coon>, 1 April 1990
+ Ty Coon, President of Vice
+
+That's all there is to it!
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..77b3184
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,217 @@
+# extract name from package.json
+PACKAGE_NAME := $(shell awk '/"name":/ {gsub(/[",]/, "", $$2); print $$2}' package.json)
+RPM_NAME := cockpit-$(PACKAGE_NAME)
+VERSION := $(shell T=$$(git describe 2>/dev/null) || T=1; echo $$T | tr '-' '.')
+ifeq ($(TEST_OS),)
+TEST_OS = fedora-39
+endif
+export TEST_OS
+TARFILE=$(RPM_NAME)-$(VERSION).tar.xz
+NODE_CACHE=$(RPM_NAME)-node-$(VERSION).tar.xz
+SPEC=$(RPM_NAME).spec
+PREFIX ?= /usr/local
+APPSTREAMFILE=org.cockpit-project.$(PACKAGE_NAME).metainfo.xml
+VM_IMAGE=$(CURDIR)/test/images/$(TEST_OS)
+# stamp file to check for node_modules/
+NODE_MODULES_TEST=package-lock.json
+# one example file in dist/ from bundler to check if that already ran
+DIST_TEST=dist/manifest.json
+# one example file in pkg/lib to check if it was already checked out
+COCKPIT_REPO_STAMP=pkg/lib/cockpit-po-plugin.js
+# common arguments for tar, mostly to make the generated tarballs reproducible
+TAR_ARGS = --sort=name --mtime "@$(shell git show --no-patch --format='%at')" --mode=go=rX,u+rw,a-s --numeric-owner --owner=0 --group=0
+
+VM_CUSTOMIZE_FLAGS =
+
+# HACK: https://github.com/containers/podman/issues/21896
+VM_CUSTOMIZE_FLAGS += --run-command 'nmcli con add type dummy con-name fake ifname fake0 ip4 1.2.3.4/24 gw4 1.2.3.1 >&2'
+
+# the following scenarios need network access
+ifeq ("$(TEST_SCENARIO)","updates-testing")
+VM_CUSTOMIZE_FLAGS += --run-command 'dnf -y update --setopt=install_weak_deps=False --enablerepo=updates-testing >&2'
+else ifeq ("$(TEST_SCENARIO)","podman-next")
+VM_CUSTOMIZE_FLAGS += --run-command 'dnf -y copr enable rhcontainerbot/podman-next >&2; dnf -y update --repo "copr*" >&2'
+else
+# default scenario does not install packages
+VM_CUSTOMIZE_FLAGS += --no-network
+endif
+
+ifeq ($(TEST_COVERAGE),yes)
+RUN_TESTS_OPTIONS+=--coverage
+NODE_ENV=development
+endif
+
+all: $(DIST_TEST)
+
+# checkout common files from Cockpit repository required to build this project;
+# this has no API stability guarantee, so check out a stable tag when you start
+# a new project, use the latest release, and update it from time to time
+COCKPIT_REPO_FILES = \
+ pkg/lib \
+ test/common \
+ test/static-code \
+ tools/node-modules \
+ $(NULL)
+
+COCKPIT_REPO_URL = https://github.com/cockpit-project/cockpit.git
+COCKPIT_REPO_COMMIT = 50fc1b962eeefefc9926ece8e4891477a88a4454 # 312 + 22 commits
+
+$(COCKPIT_REPO_FILES): $(COCKPIT_REPO_STAMP)
+COCKPIT_REPO_TREE = '$(strip $(COCKPIT_REPO_COMMIT))^{tree}'
+$(COCKPIT_REPO_STAMP): Makefile
+ @git rev-list --quiet --objects $(COCKPIT_REPO_TREE) -- 2>/dev/null || \
+ git fetch --no-tags --no-write-fetch-head --depth=1 $(COCKPIT_REPO_URL) $(COCKPIT_REPO_COMMIT)
+ git archive $(COCKPIT_REPO_TREE) -- $(COCKPIT_REPO_FILES) | tar x
+
+#
+# i18n
+#
+
+LINGUAS=$(basename $(notdir $(wildcard po/*.po)))
+
+po/$(PACKAGE_NAME).js.pot:
+ xgettext --default-domain=$(PACKAGE_NAME) --output=$@ --language=C --keyword= \
+ --keyword=_:1,1t --keyword=_:1c,2,2t --keyword=C_:1c,2 \
+ --keyword=N_ --keyword=NC_:1c,2 \
+ --keyword=gettext:1,1t --keyword=gettext:1c,2,2t \
+ --keyword=ngettext:1,2,3t --keyword=ngettext:1c,2,3,4t \
+ --keyword=gettextCatalog.getString:1,3c --keyword=gettextCatalog.getPlural:2,3,4c \
+ --from-code=UTF-8 $$(find src/ -name '*.js' -o -name '*.jsx')
+
+po/$(PACKAGE_NAME).html.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
+ pkg/lib/html2po.js -o $@ $$(find src -name '*.html')
+
+po/$(PACKAGE_NAME).manifest.pot: $(NODE_MODULES_TEST) $(COCKPIT_REPO_STAMP)
+ pkg/lib/manifest2po.js src/manifest.json -o $@
+
+po/$(PACKAGE_NAME).metainfo.pot: $(APPSTREAMFILE)
+ xgettext --default-domain=$(PACKAGE_NAME) --output=$@ $<
+
+po/$(PACKAGE_NAME).pot: po/$(PACKAGE_NAME).html.pot po/$(PACKAGE_NAME).js.pot po/$(PACKAGE_NAME).manifest.pot po/$(PACKAGE_NAME).metainfo.pot
+ msgcat --sort-output --output-file=$@ $^
+
+po/LINGUAS:
+ echo $(LINGUAS) | tr ' ' '\n' > $@
+
+#
+# Build/Install/dist
+#
+$(SPEC): packaging/$(SPEC).in $(NODE_MODULES_TEST)
+ provides=$$(npm ls --omit dev --package-lock-only --depth=Infinity | grep -Eo '[^[:space:]]+@[^[:space:]]+' | sort -u | sed 's/^/Provides: bundled(npm(/; s/\(.*\)@/\1)) = /'); \
+ awk -v p="$$provides" '{gsub(/%{VERSION}/, "$(VERSION)"); gsub(/%{NPM_PROVIDES}/, p)}1' $< > $@
+
+packaging/arch/PKGBUILD: packaging/arch/PKGBUILD.in
+ sed 's/VERSION/$(VERSION)/; s/SOURCE/$(TARFILE)/' $< > $@
+
+packaging/debian/changelog: packaging/debian/changelog.in
+ sed 's/VERSION/$(VERSION)/' $< > $@
+
+$(DIST_TEST): $(COCKPIT_REPO_STAMP) $(shell find src/ -type f) package.json build.js
+ $(MAKE) package-lock.json && NODE_ENV=$(NODE_ENV) ./build.js
+
+watch: $(NODE_MODULES_TEST)
+ NODE_ENV=$(NODE_ENV) ./build.js -w
+
+clean:
+ rm -rf dist/
+ rm -f $(SPEC) packaging/arch/PKGBUILD packaging/debian/changelog
+ rm -f po/LINGUAS
+
+install: $(DIST_TEST) po/LINGUAS
+ mkdir -p $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
+ cp -r dist/* $(DESTDIR)$(PREFIX)/share/cockpit/$(PACKAGE_NAME)
+ mkdir -p $(DESTDIR)$(PREFIX)/share/metainfo/
+ msgfmt --xml -d po \
+ --template $(APPSTREAMFILE) \
+ -o $(DESTDIR)$(PREFIX)/share/metainfo/$(APPSTREAMFILE)
+
+# this requires a built source tree and avoids having to install anything system-wide
+devel-install: $(DIST_TEST)
+ mkdir -p ~/.local/share/cockpit
+ ln -s `pwd`/dist ~/.local/share/cockpit/$(PACKAGE_NAME)
+
+# assumes that there was symlink set up using the above devel-install target,
+# and removes it
+devel-uninstall:
+ rm -f ~/.local/share/cockpit/$(PACKAGE_NAME)
+
+print-version:
+ @echo "$(VERSION)"
+
+# required for running integration tests; commander and ws are deps of chrome-remote-interface
+TEST_NPMS = \
+ node_modules/chrome-remote-interface \
+ node_modules/commander \
+ node_modules/sizzle \
+ node_modules/ws \
+ $(NULL)
+
+dist: $(TARFILE)
+ @ls -1 $(TARFILE)
+
+# when building a distribution tarball, call bundler with a 'production' environment by default
+# we don't ship most node_modules for license and compactness reasons, only the ones necessary for running tests
+# we ship a pre-built dist/ (so it's not necessary) and ship package-lock.json (so that node_modules/ can be reconstructed if necessary)
+$(TARFILE): export NODE_ENV ?= production
+$(TARFILE): $(DIST_TEST) $(SPEC) packaging/arch/PKGBUILD packaging/debian/changelog
+ if type appstream-util >/dev/null 2>&1; then appstream-util validate-relax --nonet *.metainfo.xml; fi
+ tar --xz $(TAR_ARGS) -cf $(TARFILE) --transform 's,^,$(RPM_NAME)/,' \
+ --exclude '*.in' --exclude test/reference \
+ $$(git ls-files | grep -v node_modules) \
+ $(COCKPIT_REPO_FILES) $(NODE_MODULES_TEST) $(SPEC) $(TEST_NPMS) \
+ packaging/arch/PKGBUILD packaging/debian/changelog dist/
+
+# convenience target for developers
+rpm: $(TARFILE)
+ rpmbuild -tb --define "_topdir $(CURDIR)/tmp/rpmbuild" $(TARFILE)
+ find tmp/rpmbuild -name '*.rpm' -printf '%f\n' -exec mv {} . \;
+ rm -r tmp/rpmbuild
+
+# build a VM with locally built distro pkgs installed
+$(VM_IMAGE): $(TARFILE) packaging/debian/rules packaging/debian/control packaging/arch/PKGBUILD bots
+ # HACK for ostree images: skip the rpm build/install
+ if [ "$$TEST_OS" = "fedora-coreos" ] || [ "$$TEST_OS" = "rhel4edge" ]; then \
+ bots/image-customize --verbose --fresh --no-network --run-command 'mkdir -p /usr/local/share/cockpit' \
+ --upload dist/:/usr/local/share/cockpit/podman \
+ --script $(CURDIR)/test/vm.install $(TEST_OS); \
+ else \
+ bots/image-customize --verbose --fresh $(VM_CUSTOMIZE_FLAGS) --build $(TARFILE) \
+ --script $(CURDIR)/test/vm.install $(TEST_OS); \
+ fi
+
+# convenience target for the above
+vm: $(VM_IMAGE)
+ @echo $(VM_IMAGE)
+
+# convenience target to print the filename of the test image
+print-vm:
+ @echo $(VM_IMAGE)
+
+# run static code checks for python code
+PYEXEFILES=$(shell git grep -lI '^#!.*python')
+
+codecheck: test/static-code $(NODE_MODULES_TEST)
+ test/static-code
+
+# convenience target to setup all the bits needed for the integration tests
+# without actually running them
+prepare-check: $(NODE_MODULES_TEST) $(VM_IMAGE) test/common test/reference
+
+# run the browser integration tests; skip check for SELinux denials
+# this will run all tests/check-* and format them as TAP
+check: prepare-check
+ TEST_AUDIT_NO_SELINUX=1 test/common/run-tests ${RUN_TESTS_OPTIONS}
+
+bots: $(COCKPIT_REPO_STAMP)
+ test/common/make-bots
+
+test/reference: test/common
+ test/common/pixel-tests pull
+
+# We want tools/node-modules to run every time package-lock.json is requested
+# See https://www.gnu.org/software/make/manual/html_node/Force-Targets.html
+FORCE:
+$(NODE_MODULES_TEST): FORCE tools/node-modules
+ tools/node-modules make_package_lock_json
+
+.PHONY: all clean install devel-install devel-uninstall print-version dist rpm prepare-check check vm print-vm
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..e14ec47
--- /dev/null
+++ b/README.md
@@ -0,0 +1,78 @@
+# cockpit-podman
+
+This is the [Cockpit](https://cockpit-project.org/) user interface for [podman
+containers](https://podman.io/).
+
+## Technologies
+
+ - cockpit-podman communicates to podman through its [REST API](https://podman.readthedocs.io/en/latest/_static/api.html).
+
+ - This project is based on the [Cockpit Starter Kit](https://github.com/cockpit-project/starter-kit).
+ See [Starter Kit Intro](http://cockpit-project.org/blog/cockpit-starter-kit.html) for details.
+
+# Development dependencies
+
+On Debian/Ubuntu:
+
+ $ sudo apt install gettext nodejs make
+
+On Fedora:
+
+ $ sudo dnf install gettext nodejs make
+
+# Getting and building the source
+
+These commands check out the source and build it into the `dist/` directory:
+
+```
+git clone https://github.com/cockpit-project/cockpit-podman
+cd cockpit-podman
+make
+```
+
+# Installing
+
+`sudo make install` installs the package in `/usr/local/share/cockpit/`. This depends
+on the `dist` target, which generates the distribution tarball.
+
+You can also run `make rpm` to build RPMs for local installation.
+
+In `production` mode, source files are automatically minified and compressed.
+Set `NODE_ENV=production` if you want to duplicate this behavior.
+
+# Development instructions
+
+See [HACKING.md](./HACKING.md) for details about how to efficiently change the
+code, run, and test it.
+
+# Automated release
+
+The intention is that the only manual step for releasing a project is to create
+a signed tag for the version number, which includes a summary of the noteworthy
+changes:
+
+```
+123
+
+- this new feature
+- fix bug #123
+```
+
+Pushing the release tag triggers the [release.yml](.github/workflows/release.yml)
+[GitHub action](https://github.com/features/actions) workflow. This creates the
+official release tarball and publishes as upstream release to GitHub.
+
+The Fedora and COPR releases are done with [Packit](https://packit.dev/),
+see the [packit.yaml](./packit.yaml) control file.
+
+# Automated maintenance
+
+It is important to keep your [NPM modules](./package.json) up to date, to keep
+up with security updates and bug fixes. This happens with
+[dependabot](https://github.com/dependabot),
+see [configuration file](.github/dependabot.yml).
+
+Translations are refreshed every Tuesday evening (or manually) through the
+[weblate-sync-po.yml](.github/workflows/weblate-sync-po.yml) action.
+Conversely, the PO template is uploaded to weblate every day through the
+[weblate-sync-pot.yml](.github/workflows/weblate-sync-pot.yml) action.
diff --git a/build.js b/build.js
new file mode 100755
index 0000000..1966bdc
--- /dev/null
+++ b/build.js
@@ -0,0 +1,107 @@
+#!/usr/bin/env node
+
+import fs from 'node:fs';
+import os from 'node:os';
+
+import copy from 'esbuild-plugin-copy';
+
+import { cockpitCompressPlugin } from './pkg/lib/esbuild-compress-plugin.js';
+import { cockpitPoEsbuildPlugin } from './pkg/lib/cockpit-po-plugin.js';
+import { cockpitRsyncEsbuildPlugin } from './pkg/lib/cockpit-rsync-plugin.js';
+import { cleanPlugin } from './pkg/lib/esbuild-cleanup-plugin.js';
+import { esbuildStylesPlugins } from './pkg/lib/esbuild-common.js';
+
+const useWasm = os.arch() !== 'x64';
+const esbuild = (await import(useWasm ? 'esbuild-wasm' : 'esbuild'));
+
+const production = process.env.NODE_ENV === 'production';
+/* List of directories to use when resolving import statements */
+const nodePaths = ['pkg/lib'];
+const outdir = 'dist';
+
+// Obtain package name from package.json
+const packageJson = JSON.parse(fs.readFileSync('package.json'));
+
+const parser = (await import('argparse')).default.ArgumentParser();
+parser.add_argument('-r', '--rsync', { help: "rsync bundles to ssh target after build", metavar: "HOST" });
+parser.add_argument('-w', '--watch', { action: 'store_true', help: "Enable watch mode", default: process.env.ESBUILD_WATCH === "true" });
+const args = parser.parse_args();
+
+if (args.rsync)
+ process.env.RSYNC = args.rsync;
+
+function notifyEndPlugin() {
+ return {
+ name: 'notify-end',
+ setup(build) {
+ let startTime;
+
+ build.onStart(() => {
+ startTime = new Date();
+ });
+
+ build.onEnd(() => {
+ const endTime = new Date();
+ const timeStamp = endTime.toTimeString().split(' ')[0];
+ console.log(`${timeStamp}: Build finished in ${endTime - startTime} ms`);
+ });
+ }
+ };
+}
+
+const context = await esbuild.context({
+ ...!production ? { sourcemap: "linked" } : {},
+ bundle: true,
+ entryPoints: ["./src/index.js"],
+ external: ['*.woff', '*.woff2', '*.jpg', '*.svg', '../../assets*'], // Allow external font files which live in ../../static/fonts
+ legalComments: 'external', // Move all legal comments to a .LEGAL.txt file
+ loader: { ".js": "jsx" },
+ minify: production,
+ nodePaths,
+ outdir,
+ target: ['es2020'],
+ plugins: [
+ cleanPlugin(),
+ // Esbuild will only copy assets that are explicitly imported and used
+ // in the code. This is a problem for index.html and manifest.json which are not imported
+ copy({
+ assets: [
+ { from: ['./src/manifest.json'], to: ['./manifest.json'] },
+ { from: ['./src/index.html'], to: ['./index.html'] },
+ ]
+ }),
+ ...esbuildStylesPlugins,
+ cockpitPoEsbuildPlugin(),
+
+ ...production ? [cockpitCompressPlugin()] : [],
+ cockpitRsyncEsbuildPlugin({ dest: packageJson.name }),
+
+ notifyEndPlugin(),
+ ]
+});
+
+try {
+ await context.rebuild();
+} catch (e) {
+ if (!args.watch)
+ process.exit(1);
+ // ignore errors in watch mode
+}
+
+if (args.watch) {
+ // Attention: this does not watch subdirectories -- if you ever introduce one, need to set up one watch per subdir
+ fs.watch('src', {}, async (ev, path) => {
+ // only listen for "change" events, as renames are noisy
+ if (ev !== "change")
+ return;
+ console.log("change detected:", path);
+ await context.cancel();
+ try {
+ await context.rebuild();
+ } catch (e) {} // ignore in watch mode
+ });
+ // wait forever until Control-C
+ await new Promise(() => {});
+}
+
+context.dispose();
diff --git a/cockpit-podman.spec b/cockpit-podman.spec
new file mode 100644
index 0000000..7cb7262
--- /dev/null
+++ b/cockpit-podman.spec
@@ -0,0 +1,88 @@
+#
+# Copyright (C) 2017-2020 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+#
+
+Name: cockpit-podman
+Version: 85
+Release: 1%{?dist}
+Summary: Cockpit component for Podman containers
+License: LGPL-2.1-or-later
+URL: https://github.com/cockpit-project/cockpit-podman
+
+Source0: https://github.com/cockpit-project/%{name}/releases/download/%{version}/%{name}-%{version}.tar.xz
+BuildArch: noarch
+BuildRequires: libappstream-glib
+BuildRequires: make
+BuildRequires: gettext
+%if 0%{?rhel} && 0%{?rhel} <= 8
+BuildRequires: libappstream-glib-devel
+%endif
+
+Requires: cockpit-bridge
+Requires: podman >= 2.0.4
+# HACK https://github.com/containers/crun/issues/1091
+%if 0%{?centos} == 9
+Requires: criu-libs
+%endif
+
+Provides: bundled(npm(@patternfly/patternfly)) = 5.2.1
+Provides: bundled(npm(@patternfly/react-core)) = 5.2.1
+Provides: bundled(npm(@patternfly/react-icons)) = 5.2.1
+Provides: bundled(npm(@patternfly/react-styles)) = 5.2.1
+Provides: bundled(npm(@patternfly/react-table)) = 5.2.1
+Provides: bundled(npm(@patternfly/react-tokens)) = 5.2.1
+Provides: bundled(npm(attr-accept)) = 2.2.2
+Provides: bundled(npm(date-fns)) = 3.4.0
+Provides: bundled(npm(docker-names)) = 1.2.1
+Provides: bundled(npm(file-selector)) = 0.6.0
+Provides: bundled(npm(focus-trap)) = 7.5.2
+Provides: bundled(npm(ipaddr.js)) = 2.1.0
+Provides: bundled(npm(js-tokens)) = 4.0.0
+Provides: bundled(npm(lodash)) = 4.17.21
+Provides: bundled(npm(loose-envify)) = 1.4.0
+Provides: bundled(npm(object-assign)) = 4.1.1
+Provides: bundled(npm(prop-types)) = 15.8.1
+Provides: bundled(npm(react-dom)) = 18.2.0
+Provides: bundled(npm(react-dropzone)) = 14.2.3
+Provides: bundled(npm(react-is)) = 16.13.1
+Provides: bundled(npm(react)) = 18.2.0
+Provides: bundled(npm(scheduler)) = 0.23.0
+Provides: bundled(npm(tabbable)) = 6.2.0
+Provides: bundled(npm(throttle-debounce)) = 5.0.0
+Provides: bundled(npm(tslib)) = 2.6.2
+Provides: bundled(npm(xterm-addon-canvas)) = 0.4.0
+Provides: bundled(npm(xterm)) = 5.1.0
+
+%description
+The Cockpit user interface for Podman containers.
+
+%prep
+%setup -q -n %{name}
+
+%build
+# Nothing to build
+
+%install
+%make_install PREFIX=/usr
+appstream-util validate-relax --nonet %{buildroot}/%{_datadir}/metainfo/*
+
+%files
+%doc README.md
+%license LICENSE dist/index.js.LEGAL.txt dist/index.css.LEGAL.txt
+%{_datadir}/cockpit/*
+%{_datadir}/metainfo/*
+
+%changelog
diff --git a/dist/index.css.LEGAL.txt b/dist/index.css.LEGAL.txt
new file mode 100644
index 0000000..f1c05c0
--- /dev/null
+++ b/dist/index.css.LEGAL.txt
@@ -0,0 +1,35 @@
+Bundled license information:
+
+xterm/css/xterm.css:
+ /**
+ * Copyright (c) 2014 The xterm.js authors. All rights reserved.
+ * Copyright (c) 2012-2013, Christopher Jeffrey (MIT License)
+ * https://github.com/chjj/term.js
+ * @license MIT
+ *
+ * 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.
+ *
+ * Originally forked from (with the author's permission):
+ * Fabrice Bellard's javascript vt100 for jslinux:
+ * http://bellard.org/jslinux/
+ * Copyright (c) 2011 Fabrice Bellard
+ * The original design remains. The terminal itself
+ * has been extended to include xterm CSI codes, among
+ * other features.
+ */
diff --git a/dist/index.css.gz b/dist/index.css.gz
new file mode 100644
index 0000000..321bff0
--- /dev/null
+++ b/dist/index.css.gz
Binary files differ
diff --git a/dist/index.html b/dist/index.html
new file mode 100644
index 0000000..c4e1856
--- /dev/null
+++ b/dist/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this package; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="podman-page" class="pf-theme-dark" lang="en">
+<head>
+ <title translatable="yes">Podman containers</title>
+ <meta charset="utf-8">
+ <meta name="description" content="">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="stylesheet" href="index.css">
+
+ <script type="text/javascript" src="index.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+</head>
+
+<body class="pf-m-redhat-font pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app">
+ </div>
+</body>
+</html>
diff --git a/dist/index.js.LEGAL.txt b/dist/index.js.LEGAL.txt
new file mode 100644
index 0000000..eb4acfb
--- /dev/null
+++ b/dist/index.js.LEGAL.txt
@@ -0,0 +1,46 @@
+Bundled license information:
+
+react/cjs/react.production.min.js:
+ /**
+ * @license React
+ * react.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+scheduler/cjs/scheduler.production.min.js:
+ /**
+ * @license React
+ * scheduler.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+react-dom/cjs/react-dom.production.min.js:
+ /**
+ * @license React
+ * react-dom.production.min.js
+ *
+ * Copyright (c) Facebook, Inc. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ */
+
+tabbable/dist/index.esm.js:
+ /*!
+ * tabbable 6.2.0
+ * @license MIT, https://github.com/focus-trap/tabbable/blob/master/LICENSE
+ */
+
+focus-trap/dist/focus-trap.esm.js:
+ /*!
+ * focus-trap 7.5.2
+ * @license MIT, https://github.com/focus-trap/focus-trap/blob/master/LICENSE
+ */
diff --git a/dist/index.js.gz b/dist/index.js.gz
new file mode 100644
index 0000000..4d2c5cc
--- /dev/null
+++ b/dist/index.js.gz
Binary files differ
diff --git a/dist/manifest.json b/dist/manifest.json
new file mode 100644
index 0000000..530aa6d
--- /dev/null
+++ b/dist/manifest.json
@@ -0,0 +1,16 @@
+{
+ "conditions": [
+ {"path-exists": "/lib/systemd/system/podman.socket"}
+ ],
+ "menu": {
+ "index": {
+ "label": "Podman containers",
+ "order": 50,
+ "keywords": [
+ {
+ "matches": ["podman", "container", "image"]
+ }
+ ]
+ }
+ }
+}
diff --git a/dist/po.cs.js.gz b/dist/po.cs.js.gz
new file mode 100644
index 0000000..8751107
--- /dev/null
+++ b/dist/po.cs.js.gz
Binary files differ
diff --git a/dist/po.de.js.gz b/dist/po.de.js.gz
new file mode 100644
index 0000000..990476a
--- /dev/null
+++ b/dist/po.de.js.gz
Binary files differ
diff --git a/dist/po.es.js.gz b/dist/po.es.js.gz
new file mode 100644
index 0000000..bd8df16
--- /dev/null
+++ b/dist/po.es.js.gz
Binary files differ
diff --git a/dist/po.fi.js.gz b/dist/po.fi.js.gz
new file mode 100644
index 0000000..49734cb
--- /dev/null
+++ b/dist/po.fi.js.gz
Binary files differ
diff --git a/dist/po.fr.js.gz b/dist/po.fr.js.gz
new file mode 100644
index 0000000..d09fde6
--- /dev/null
+++ b/dist/po.fr.js.gz
Binary files differ
diff --git a/dist/po.ja.js.gz b/dist/po.ja.js.gz
new file mode 100644
index 0000000..82eb352
--- /dev/null
+++ b/dist/po.ja.js.gz
Binary files differ
diff --git a/dist/po.ka.js.gz b/dist/po.ka.js.gz
new file mode 100644
index 0000000..998ddd5
--- /dev/null
+++ b/dist/po.ka.js.gz
Binary files differ
diff --git a/dist/po.ko.js.gz b/dist/po.ko.js.gz
new file mode 100644
index 0000000..1eb7323
--- /dev/null
+++ b/dist/po.ko.js.gz
Binary files differ
diff --git a/dist/po.manifest.cs.js.gz b/dist/po.manifest.cs.js.gz
new file mode 100644
index 0000000..670a628
--- /dev/null
+++ b/dist/po.manifest.cs.js.gz
Binary files differ
diff --git a/dist/po.manifest.de.js.gz b/dist/po.manifest.de.js.gz
new file mode 100644
index 0000000..68ccc99
--- /dev/null
+++ b/dist/po.manifest.de.js.gz
Binary files differ
diff --git a/dist/po.manifest.es.js.gz b/dist/po.manifest.es.js.gz
new file mode 100644
index 0000000..604dbc0
--- /dev/null
+++ b/dist/po.manifest.es.js.gz
Binary files differ
diff --git a/dist/po.manifest.fi.js.gz b/dist/po.manifest.fi.js.gz
new file mode 100644
index 0000000..e67d6df
--- /dev/null
+++ b/dist/po.manifest.fi.js.gz
Binary files differ
diff --git a/dist/po.manifest.fr.js.gz b/dist/po.manifest.fr.js.gz
new file mode 100644
index 0000000..e02b181
--- /dev/null
+++ b/dist/po.manifest.fr.js.gz
Binary files differ
diff --git a/dist/po.manifest.ja.js.gz b/dist/po.manifest.ja.js.gz
new file mode 100644
index 0000000..a99264a
--- /dev/null
+++ b/dist/po.manifest.ja.js.gz
Binary files differ
diff --git a/dist/po.manifest.ka.js.gz b/dist/po.manifest.ka.js.gz
new file mode 100644
index 0000000..e6c892f
--- /dev/null
+++ b/dist/po.manifest.ka.js.gz
Binary files differ
diff --git a/dist/po.manifest.ko.js.gz b/dist/po.manifest.ko.js.gz
new file mode 100644
index 0000000..865b27d
--- /dev/null
+++ b/dist/po.manifest.ko.js.gz
Binary files differ
diff --git a/dist/po.manifest.pl.js.gz b/dist/po.manifest.pl.js.gz
new file mode 100644
index 0000000..494ba26
--- /dev/null
+++ b/dist/po.manifest.pl.js.gz
Binary files differ
diff --git a/dist/po.manifest.sk.js.gz b/dist/po.manifest.sk.js.gz
new file mode 100644
index 0000000..7e079e9
--- /dev/null
+++ b/dist/po.manifest.sk.js.gz
Binary files differ
diff --git a/dist/po.manifest.sv.js.gz b/dist/po.manifest.sv.js.gz
new file mode 100644
index 0000000..414cbf7
--- /dev/null
+++ b/dist/po.manifest.sv.js.gz
Binary files differ
diff --git a/dist/po.manifest.tr.js.gz b/dist/po.manifest.tr.js.gz
new file mode 100644
index 0000000..bfdf5e1
--- /dev/null
+++ b/dist/po.manifest.tr.js.gz
Binary files differ
diff --git a/dist/po.manifest.uk.js.gz b/dist/po.manifest.uk.js.gz
new file mode 100644
index 0000000..ec75de3
--- /dev/null
+++ b/dist/po.manifest.uk.js.gz
Binary files differ
diff --git a/dist/po.manifest.zh_CN.js.gz b/dist/po.manifest.zh_CN.js.gz
new file mode 100644
index 0000000..0d2d466
--- /dev/null
+++ b/dist/po.manifest.zh_CN.js.gz
Binary files differ
diff --git a/dist/po.pl.js.gz b/dist/po.pl.js.gz
new file mode 100644
index 0000000..d485ae8
--- /dev/null
+++ b/dist/po.pl.js.gz
Binary files differ
diff --git a/dist/po.sk.js.gz b/dist/po.sk.js.gz
new file mode 100644
index 0000000..a197bb4
--- /dev/null
+++ b/dist/po.sk.js.gz
Binary files differ
diff --git a/dist/po.sv.js.gz b/dist/po.sv.js.gz
new file mode 100644
index 0000000..093e070
--- /dev/null
+++ b/dist/po.sv.js.gz
Binary files differ
diff --git a/dist/po.tr.js.gz b/dist/po.tr.js.gz
new file mode 100644
index 0000000..cdb6f65
--- /dev/null
+++ b/dist/po.tr.js.gz
Binary files differ
diff --git a/dist/po.uk.js.gz b/dist/po.uk.js.gz
new file mode 100644
index 0000000..72ffc9a
--- /dev/null
+++ b/dist/po.uk.js.gz
Binary files differ
diff --git a/dist/po.zh_CN.js.gz b/dist/po.zh_CN.js.gz
new file mode 100644
index 0000000..c886b0a
--- /dev/null
+++ b/dist/po.zh_CN.js.gz
Binary files differ
diff --git a/node_modules/chrome-remote-interface/LICENSE b/node_modules/chrome-remote-interface/LICENSE
new file mode 100644
index 0000000..91137a0
--- /dev/null
+++ b/node_modules/chrome-remote-interface/LICENSE
@@ -0,0 +1,18 @@
+Copyright (c) 2023 Andrea Cardaci <cyrus.and@gmail.com>
+
+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.
diff --git a/node_modules/chrome-remote-interface/README.md b/node_modules/chrome-remote-interface/README.md
new file mode 100644
index 0000000..9800abb
--- /dev/null
+++ b/node_modules/chrome-remote-interface/README.md
@@ -0,0 +1,992 @@
+# chrome-remote-interface
+
+[![CI status](https://github.com/cyrus-and/chrome-remote-interface/actions/workflows/ci.yml/badge.svg)](https://github.com/cyrus-and/chrome-remote-interface/actions?query=workflow:CI)
+
+[Build Status]: https://app.travis-ci.com/cyrus-and/chrome-remote-interface.svg?branch=master
+[travis]: https://app.travis-ci.com/cyrus-and/chrome-remote-interface
+
+[Chrome Debugging Protocol] interface that helps to instrument Chrome (or any
+other suitable [implementation](#implementations)) by providing a simple
+abstraction of commands and notifications using a straightforward JavaScript
+API.
+
+This module is one of the many [third-party protocol clients][3rd-party].
+
+[3rd-party]: https://developer.chrome.com/devtools/docs/debugging-clients#chrome-remote-interface
+
+## Sample API usage
+
+The following snippet loads `https://github.com` and dumps every request made:
+
+```js
+const CDP = require('chrome-remote-interface');
+
+async function example() {
+ let client;
+ try {
+ // connect to endpoint
+ client = await CDP();
+ // extract domains
+ const {Network, Page} = client;
+ // setup handlers
+ Network.requestWillBeSent((params) => {
+ console.log(params.request.url);
+ });
+ // enable events then start!
+ await Network.enable();
+ await Page.enable();
+ await Page.navigate({url: 'https://github.com'});
+ await Page.loadEventFired();
+ } catch (err) {
+ console.error(err);
+ } finally {
+ if (client) {
+ await client.close();
+ }
+ }
+}
+
+example();
+```
+
+Find more examples in the [wiki]. You may also want to take a look at the [FAQ].
+
+[wiki]: https://github.com/cyrus-and/chrome-remote-interface/wiki
+[async-await-example]: https://github.com/cyrus-and/chrome-remote-interface/wiki/Async-await-example
+[FAQ]: https://github.com/cyrus-and/chrome-remote-interface#faq
+
+## Installation
+
+ npm install chrome-remote-interface
+
+Install globally (`-g`) to just use the [bundled client](#bundled-client).
+
+## Implementations
+
+This module should work with every application implementing the
+[Chrome Debugging Protocol]. In particular, it has been tested against the
+following implementations:
+
+Implementation | Protocol version | [Protocol] | [List] | [New] | [Activate] | [Close] | [Version]
+---------------------------|--------------------|------------|--------|-------|------------|---------|-----------
+[Chrome][1.1] | [tip-of-tree][1.2] | yes¹ | yes | yes | yes | yes | yes
+[Opera][2.1] | [tip-of-tree][2.2] | yes | yes | yes | yes | yes | yes
+[Node.js][3.1] ([v6.3.0]+) | [node][3.2] | yes | no | no | no | no | yes
+[Safari (iOS)][4.1] | [*partial*][4.2] | no | yes | no | no | no | no
+[Edge][5.1] | [*partial*][5.2] | yes | yes | no | no | no | yes
+[Firefox (Nightly)][6.1] | [*partial*][6.2] | yes | yes | no | yes | yes | yes
+
+¹ Not available on [Chrome for Android][chrome-mobile-protocol], hence a local version of the protocol must be used.
+
+[chrome-mobile-protocol]: https://bugs.chromium.org/p/chromium/issues/detail?id=824626#c4
+
+[1.1]: #chromechromium
+[1.2]: https://chromedevtools.github.io/devtools-protocol/tot/
+
+[2.1]: #opera
+[2.2]: https://chromedevtools.github.io/devtools-protocol/tot/
+
+[3.1]: #nodejs
+[3.2]: https://chromedevtools.github.io/devtools-protocol/v8/
+
+[4.1]: #safari-ios
+[4.2]: http://trac.webkit.org/browser/trunk/Source/JavaScriptCore/inspector/protocol
+
+[5.1]: #edge
+[5.2]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/0.1/domains/
+
+[6.1]: #firefox-nightly
+[6.2]: https://firefox-source-docs.mozilla.org/remote/index.html
+
+[v6.3.0]: https://nodejs.org/en/blog/release/v6.3.0/
+
+[Protocol]: #cdpprotocoloptions-callback
+[List]: #cdplistoptions-callback
+[New]: #cdpnewoptions-callback
+[Activate]: #cdpactivateoptions-callback
+[Close]: #cdpcloseoptions-callback
+[Version]: #cdpversionoptions-callback
+
+The meaning of *target* varies according to the implementation, for example,
+each Chrome tab represents a target whereas for Node.js a target is the
+currently inspected script.
+
+## Setup
+
+An instance of either Chrome itself or another implementation needs to be
+running on a known port in order to use this module (defaults to
+`localhost:9222`).
+
+### Chrome/Chromium
+
+#### Desktop
+
+Start Chrome with the `--remote-debugging-port` option, for example:
+
+ google-chrome --remote-debugging-port=9222
+
+##### Headless
+
+Since version 59, additionally use the `--headless` option, for example:
+
+ google-chrome --headless --remote-debugging-port=9222
+
+#### Android
+
+Plug the device and enable the [port forwarding][adb], for example:
+
+ adb forward tcp:9222 localabstract:chrome_devtools_remote
+
+Note that in Android, Chrome does not have its own protocol available, a local
+version must be used. See [here](#chrome-debugging-protocol-versions) for more information.
+
+[adb]: https://developer.chrome.com/devtools/docs/remote-debugging-legacy
+
+##### WebView
+
+In order to be inspectable, a WebView must
+be [configured for debugging][webview] and the corresponding process ID must be
+known. There are several ways to obtain it, for example:
+
+ adb shell grep -a webview_devtools_remote /proc/net/unix
+
+Finally, port forwarding can be enabled as follows:
+
+ adb forward tcp:9222 localabstract:webview_devtools_remote_<pid>
+
+[webview]: https://developers.google.com/web/tools/chrome-devtools/remote-debugging/webviews#configure_webviews_for_debugging
+
+### Opera
+
+Start Opera with the `--remote-debugging-port` option, for example:
+
+ opera --remote-debugging-port=9222
+
+### Node.js
+
+Start Node.js with the `--inspect` option, for example:
+
+ node --inspect=9222 script.js
+
+### Safari (iOS)
+
+Install and run the [iOS WebKit Debug Proxy][iwdp]. Then use it with the `local`
+option set to `true` to use the local version of the protocol or pass a custom
+descriptor upon connection (`protocol` option).
+
+[iwdp]: https://github.com/google/ios-webkit-debug-proxy
+
+### Edge
+
+Start Edge with the `--devtools-server-port` option, for example:
+
+ MicrosoftEdge.exe --devtools-server-port 9222 about:blank
+
+Please find more information [here][edge-devtools].
+
+[edge-devtools]: https://docs.microsoft.com/en-us/microsoft-edge/devtools-protocol/
+
+### Firefox (Nightly)
+
+Start Firefox with the `--remote-debugging-port` option, for example:
+
+ firefox --remote-debugging-port 9222
+
+Bear in mind that this is an experimental feature of Firefox.
+
+## Bundled client
+
+This module comes with a bundled client application that can be used to
+interactively control a remote instance.
+
+### Target management
+
+The bundled client exposes subcommands to interact with the HTTP frontend
+(e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.),
+run with `--help` to display the list of available options.
+
+Here are some examples:
+
+```js
+$ chrome-remote-interface new 'http://example.com'
+{
+ "description": "",
+ "devtoolsFrontendUrl": "/devtools/inspector.html?ws=localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "id": "b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "thumbnailUrl": "/thumb/b049bb56-de7d-424c-a331-6ae44cf7ae01",
+ "title": "",
+ "type": "page",
+ "url": "http://example.com/",
+ "webSocketDebuggerUrl": "ws://localhost:9222/devtools/page/b049bb56-de7d-424c-a331-6ae44cf7ae01"
+}
+$ chrome-remote-interface close 'b049bb56-de7d-424c-a331-6ae44cf7ae01'
+```
+
+### Inspection
+
+Using the `inspect` subcommand it is possible to perform [command execution](#clientdomainmethodparams-callback)
+and [event binding](#clientdomaineventcallback) in a REPL fashion that provides completion.
+
+Here is a sample session:
+
+```js
+$ chrome-remote-interface inspect
+>>> Runtime.evaluate({expression: 'window.location.toString()'})
+{ result: { type: 'string', value: 'about:blank' } }
+>>> Page.enable()
+{}
+>>> Page.loadEventFired(console.log)
+[Function]
+>>> Page.navigate({url: 'https://github.com'})
+{ frameId: 'E1657E22F06E6E0BE13DFA8130C20298',
+ loaderId: '439236ADE39978F98C20E8939A32D3A5' }
+>>> { timestamp: 7454.721299 } // from Page.loadEventFired
+>>> Runtime.evaluate({expression: 'window.location.toString()'})
+{ result: { type: 'string', value: 'https://github.com/' } }
+```
+
+Additionally there are some custom commands available:
+
+```js
+>>> .help
+[...]
+.reset Remove all the registered event handlers
+.target Display the current target
+```
+
+## Embedded documentation
+
+In both the REPL and the regular API every object of the protocol is *decorated*
+with the meta information found within the descriptor. In addition The
+`category` field is added, which determines if the member is a `command`, an
+`event` or a `type`.
+
+For example to learn how to call `Page.navigate`:
+
+```js
+>>> Page.navigate
+{ [Function]
+ category: 'command',
+ parameters: { url: { type: 'string', description: 'URL to navigate the page to.' } },
+ returns:
+ [ { name: 'frameId',
+ '$ref': 'FrameId',
+ hidden: true,
+ description: 'Frame id that will be navigated.' } ],
+ description: 'Navigates current page to the given URL.',
+ handlers: [ 'browser', 'renderer' ] }
+```
+
+To learn about the parameters returned by the `Network.requestWillBeSent` event:
+
+```js
+>>> Network.requestWillBeSent
+{ [Function]
+ category: 'event',
+ description: 'Fired when page is about to send HTTP request.',
+ parameters:
+ { requestId: { '$ref': 'RequestId', description: 'Request identifier.' },
+ frameId:
+ { '$ref': 'Page.FrameId',
+ description: 'Frame identifier.',
+ hidden: true },
+ loaderId: { '$ref': 'LoaderId', description: 'Loader identifier.' },
+ documentURL:
+ { type: 'string',
+ description: 'URL of the document this request is loaded for.' },
+ request: { '$ref': 'Request', description: 'Request data.' },
+ timestamp: { '$ref': 'Timestamp', description: 'Timestamp.' },
+ wallTime:
+ { '$ref': 'Timestamp',
+ hidden: true,
+ description: 'UTC Timestamp.' },
+ initiator: { '$ref': 'Initiator', description: 'Request initiator.' },
+ redirectResponse:
+ { optional: true,
+ '$ref': 'Response',
+ description: 'Redirect response data.' },
+ type:
+ { '$ref': 'Page.ResourceType',
+ optional: true,
+ hidden: true,
+ description: 'Type of this resource.' } } }
+```
+
+To inspect the `Network.Request` (note that unlike commands and events, types
+are named in upper camel case) type:
+
+```js
+>>> Network.Request
+{ category: 'type',
+ id: 'Request',
+ type: 'object',
+ description: 'HTTP request data.',
+ properties:
+ { url: { type: 'string', description: 'Request URL.' },
+ method: { type: 'string', description: 'HTTP request method.' },
+ headers: { '$ref': 'Headers', description: 'HTTP request headers.' },
+ postData:
+ { type: 'string',
+ optional: true,
+ description: 'HTTP POST request data.' },
+ mixedContentType:
+ { optional: true,
+ type: 'string',
+ enum: [Object],
+ description: 'The mixed content status of the request, as defined in http://www.w3.org/TR/mixed-content/' },
+ initialPriority:
+ { '$ref': 'ResourcePriority',
+ description: 'Priority of the resource request at the time request is sent.' } } }
+```
+
+## Chrome Debugging Protocol versions
+
+By default `chrome-remote-interface` *asks* the remote instance to provide its
+own protocol.
+
+This behavior can be changed by setting the `local` option to `true`
+upon [connection](#cdpoptions-callback), in which case the [local version] of
+the protocol descriptor is used. This file is manually updated from time to time
+using `scripts/update-protocol.sh` and pushed to this repository.
+
+To further override the above behavior there are basically two options:
+
+- pass a custom protocol descriptor upon [connection](#cdpoptions-callback)
+ (`protocol` option);
+
+- use the *raw* version of the [commands](#clientsendmethod-params-callback)
+ and [events](#event-domainmethod) interface to use bleeding-edge features that
+ do not appear in the [local version] of the protocol descriptor;
+
+[local version]: lib/protocol.json
+
+## Browser usage
+
+This module is able to run within a web context, with obvious limitations
+though, namely external HTTP requests
+([List](#cdplistoptions-callback), [New](#cdpnewoptions-callback), etc.) cannot
+be performed directly, for this reason the user must provide a global
+`criRequest` in order to use them:
+
+```js
+function criRequest(options, callback) {}
+```
+
+`options` is the same object used by the Node.js `http` module and `callback` is
+a function taking two arguments: `err` (JavaScript `Error` object or `null`) and
+`data` (string result).
+
+### Using [webpack](https://webpack.github.io/)
+
+It just works, simply require this module:
+
+```js
+const CDP = require('chrome-remote-interface');
+```
+
+### Using *vanilla* JavaScript
+
+To generate a JavaScript file that can be used with a `<script>` element:
+
+1. run `npm install` from the root directory;
+
+2. manually run webpack with:
+
+ TARGET=var npm run webpack
+
+3. use as:
+
+ ```html
+ <script>
+ function criRequest(options, callback) { /*...*/ }
+ </script>
+ <script src="chrome-remote-interface.js"></script>
+ ```
+
+## TypeScript Support
+
+[TypeScript][] definitions are kindly provided by [Khairul Azhar Kasmiran][] and [Seth Westphal][], and can be installed from [DefinitelyTyped][]:
+
+```
+npm install --save-dev @types/chrome-remote-interface
+```
+
+Note that the TypeScript definitions are automatically generated from the npm package `devtools-protocol@0.0.927104`. For other versions of devtools-protocol:
+
+1. Install patch-package using [the instructions given](https://github.com/ds300/patch-package#set-up).
+2. Copy the contents of the corresponding https://github.com/ChromeDevTools/devtools-protocol/tree/master/types folder (according to commit) into `node_modules/devtools-protocol/types`.
+3. Run `npx patch-package devtools-protocol` so that the changes persist across an `npm install`.
+
+[TypeScript]: https://www.typescriptlang.org/
+[Khairul Azhar Kasmiran]: https://github.com/kazarmy
+[Seth Westphal]: https://github.com/westy92
+[DefinitelyTyped]: https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/chrome-remote-interface
+
+## API
+
+The API consists of three parts:
+
+- *DevTools* methods (for those [implementations](#implementations) that support
+ them, e.g., [List](#cdplistoptions-callback), [New](#cdpnewoptions-callback),
+ etc.);
+
+- [connection](#cdpoptions-callback) establishment;
+
+- the actual [protocol interaction](#class-cdp).
+
+### CDP([options], [callback])
+
+Connects to a remote instance using the [Chrome Debugging Protocol].
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `target`: determines which target this client should attach to. The behavior
+ changes according to the type:
+
+ - a `function` that takes the array returned by the `List` method and returns
+ a target or its numeric index relative to the array;
+ - a target `object` like those returned by the `New` and `List` methods;
+ - a `string` representing the raw WebSocket URL, in this case `host` and
+ `port` are not used to fetch the target list, yet they are used to complete
+ the URL if relative;
+ - a `string` representing the target id.
+
+ Defaults to a function which returns the first available target according to
+ the implementation (note that at most one connection can be established to the
+ same target);
+- `protocol`: [Chrome Debugging Protocol] descriptor object. Defaults to use the
+ protocol chosen according to the `local` option;
+- `local`: a boolean indicating whether the protocol must be fetched *remotely*
+ or if the local version must be used. It has no effect if the `protocol`
+ option is set. Defaults to `false`.
+
+These options are also valid properties of all the instances of the `CDP`
+class. In addition to that, the `webSocketUrl` field contains the currently used
+WebSocket URL.
+
+`callback` is a listener automatically added to the `connect` event of the
+returned `EventEmitter`. When `callback` is omitted a `Promise` object is
+returned which becomes fulfilled if the `connect` event is triggered and
+rejected if the `error` event is triggered.
+
+The `EventEmitter` supports the following events:
+
+#### Event: 'connect'
+
+```js
+function (client) {}
+```
+
+Emitted when the connection to the WebSocket is established.
+
+`client` is an instance of the `CDP` class.
+
+#### Event: 'error'
+
+```js
+function (err) {}
+```
+
+Emitted when `http://host:port/json` cannot be reached or if it is not possible
+to connect to the WebSocket.
+
+`err` is an instance of `Error`.
+
+### CDP.Protocol([options], [callback])
+
+Fetch the [Chrome Debugging Protocol] descriptor.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `local`: a boolean indicating whether the protocol must be fetched *remotely*
+ or if the local version must be returned. Defaults to `false`.
+
+`callback` is executed when the protocol is fetched, it gets the following
+arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `protocol`: the [Chrome Debugging Protocol] descriptor.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Protocol((err, protocol) => {
+ if (!err) {
+ console.log(JSON.stringify(protocol, null, 4));
+ }
+});
+```
+
+### CDP.List([options], [callback])
+
+Request the list of the available open targets/tabs of the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function.
+
+`callback` is executed when the list is correctly received, it gets the
+following arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `targets`: the array returned by `http://host:port/json/list` containing the
+ target list.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.List((err, targets) => {
+ if (!err) {
+ console.log(targets);
+ }
+});
+```
+
+### CDP.New([options], [callback])
+
+Create a new target/tab in the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `url`: URL to load in the new target/tab. Defaults to `about:blank`.
+
+`callback` is executed when the target is created, it gets the following
+arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `target`: the object returned by `http://host:port/json/new` containing the
+ target.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.New((err, target) => {
+ if (!err) {
+ console.log(target);
+ }
+});
+```
+
+### CDP.Activate([options], [callback])
+
+Activate an open target/tab of the remote instance.
+
+`options` is an object with the following properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `id`: Target id. Required, no default.
+
+`callback` is executed when the response to the activation request is
+received. It gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Activate({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
+ if (!err) {
+ console.log('target is activated');
+ }
+});
+```
+
+### CDP.Close([options], [callback])
+
+Close an open target/tab of the remote instance.
+
+`options` is an object with the following properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function;
+- `id`: Target id. Required, no default.
+
+`callback` is executed when the response to the close request is received. It
+gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Close({id: 'CC46FBFA-3BDA-493B-B2E4-2BE6EB0D97EC'}, (err) => {
+ if (!err) {
+ console.log('target is closing');
+ }
+});
+```
+
+Note that the callback is fired when the target is *queued* for removal, but the
+actual removal will occur asynchronously.
+
+### CDP.Version([options], [callback])
+
+Request version information from the remote instance.
+
+`options` is an object with the following optional properties:
+
+- `host`: HTTP frontend host. Defaults to `localhost`;
+- `port`: HTTP frontend port. Defaults to `9222`;
+- `secure`: HTTPS/WSS frontend. Defaults to `false`;
+- `useHostName`: do not perform a DNS lookup of the host. Defaults to `false`;
+- `alterPath`: a `function` taking and returning the path fragment of a URL
+ before that a request happens. Defaults to the identity function.
+
+`callback` is executed when the version information is correctly received, it
+gets the following arguments:
+
+- `err`: a `Error` object indicating the success status;
+- `info`: a JSON object returned by `http://host:port/json/version` containing
+ the version information.
+
+When `callback` is omitted a `Promise` object is returned.
+
+For example:
+
+```js
+const CDP = require('chrome-remote-interface');
+CDP.Version((err, info) => {
+ if (!err) {
+ console.log(info);
+ }
+});
+```
+
+### Class: CDP
+
+#### Event: 'event'
+
+```js
+function (message) {}
+```
+
+Emitted when the remote instance sends any notification through the WebSocket.
+
+`message` is the object received, it has the following properties:
+
+- `method`: a string describing the notification (e.g.,
+ `'Network.requestWillBeSent'`);
+- `params`: an object containing the payload;
+- `sessionId`: an optional string representing the session identifier.
+
+Refer to the [Chrome Debugging Protocol] specification for more information.
+
+For example:
+
+```js
+client.on('event', (message) => {
+ if (message.method === 'Network.requestWillBeSent') {
+ console.log(message.params);
+ }
+});
+```
+
+#### Event: '`<domain>`.`<method>`'
+
+```js
+function (params, sessionId) {}
+```
+
+Emitted when the remote instance sends a notification for `<domain>.<method>`
+through the WebSocket.
+
+`params` is an object containing the payload.
+
+`sessionId` is an optional string representing the session identifier.
+
+This is just a utility event which allows to easily listen for specific
+notifications (see [`'event'`](#event-event)), for example:
+
+```js
+client.on('Network.requestWillBeSent', console.log);
+```
+
+Additionally, the equivalent `<domain>.on('<method>', ...)` syntax is available, for example:
+
+```js
+client.Network.on('requestWillBeSent', console.log);
+```
+
+#### Event: '`<domain>`.`<method>`.`<sessionId>`'
+
+```js
+function (params, sessionId) {}
+```
+
+Equivalent to the following but only for those events belonging to the given `session`:
+
+```js
+client.on('<domain>.<event>', callback);
+```
+
+#### Event: 'ready'
+
+```js
+function () {}
+```
+
+Emitted every time that there are no more pending commands waiting for a
+response from the remote instance. The interaction is asynchronous so the only
+way to serialize a sequence of commands is to use the callback provided by
+the [`send`](#clientsendmethod-params-callback) method. This event acts as a
+barrier and it is useful to avoid the *callback hell* in certain simple
+situations.
+
+Users are encouraged to extensively check the response of each method and should
+prefer the promises API when dealing with complex asynchronous program flows.
+
+For example to load a URL only after having enabled the notifications of both
+`Network` and `Page` domains:
+
+```js
+client.Network.enable();
+client.Page.enable();
+client.once('ready', () => {
+ client.Page.navigate({url: 'https://github.com'});
+});
+```
+
+In this particular case, not enforcing this kind of serialization may cause that
+the remote instance does not properly deliver the desired notifications the
+client.
+
+
+#### Event: 'disconnect'
+
+```js
+function () {}
+```
+
+Emitted when the instance closes the WebSocket connection.
+
+This may happen for example when the user opens DevTools or when the tab is
+closed.
+
+#### client.send(method, [params], [sessionId], [callback])
+
+Issue a command to the remote instance.
+
+`method` is a string describing the command.
+
+`params` is an object containing the payload.
+
+`sessionId` is a string representing the session identifier.
+
+`callback` is executed when the remote instance sends a response to this
+command, it gets the following arguments:
+
+- `error`: a boolean value indicating the success status, as reported by the
+ remote instance;
+- `response`: an object containing either the response (`result` field, if
+ `error === false`) or the indication of the error (`error` field, if `error
+ === true`).
+
+When `callback` is omitted a `Promise` object is returned instead, with the
+fulfilled/rejected states implemented according to the `error` parameter. The
+`Error` object returned contains two additional parameters: `request` and
+`response` which contain the raw massages, useful for debugging purposes. In
+case of low-level WebSocket errors, the `error` parameter contains the
+originating `Error` object and no `response` is returned.
+
+Note that the field `id` mentioned in the [Chrome Debugging Protocol]
+specification is managed internally and it is not exposed to the user.
+
+For example:
+
+```js
+client.send('Page.navigate', {url: 'https://github.com'}, console.log);
+```
+
+#### client.`<domain>`.`<method>`([params], [sessionId], [callback])
+
+Just a shorthand for:
+
+```js
+client.send('<domain>.<method>', params, sessionId, callback);
+```
+
+For example:
+
+```js
+client.Page.navigate({url: 'https://github.com'}, console.log);
+```
+
+#### client.`<domain>`.`<event>`([sessionId], [callback])
+
+Just a shorthand for:
+
+```js
+client.on('<domain>.<event>[.<sessionId>]', callback);
+```
+
+When `callback` is omitted the event is registered only once and a `Promise`
+object is returned. Notice though that in this case the optional `sessionId` usually passed to `callback` is not returned.
+
+When `callback` is provided, it returns a function that can be used to
+unsubscribe `callback` from the event, it can be useful when anonymous functions
+are used as callbacks.
+
+For example:
+
+```js
+const unsubscribe = client.Network.requestWillBeSent((params, sessionId) => {
+ console.log(params.request.url);
+});
+unsubscribe();
+```
+
+#### client.close([callback])
+
+Close the connection to the remote instance.
+
+`callback` is executed when the WebSocket is successfully closed.
+
+When `callback` is omitted a `Promise` object is returned.
+
+#### client['`<domain>`.`<name>`']
+
+Just a shorthand for:
+
+```js
+client.<domain>.<name>
+```
+
+Where `<name>` can be a command, an event, or a type.
+
+## FAQ
+
+### Invoking `Domain.methodOrEvent` I obtain `Domain.methodOrEvent is not a function`
+
+This means that you are trying to use a method or an event that are not present
+in the protocol descriptor that you are using.
+
+If the protocol is fetched from Chrome directly, then it means that this version
+of Chrome does not support that feature. The solution is to update it.
+
+If you are using a local or custom version of the protocol, then it means that
+the version is obsolete. The solution is to provide an up-to-date one, or if you
+are using the protocol embedded in chrome-remote-interface, make sure to be
+running the latest version of this module. In case the embedded protocol is
+obsolete, please [file an issue](https://github.com/cyrus-and/chrome-remote-interface/issues/new).
+
+See [here](#chrome-debugging-protocol-versions) for more information.
+
+### Invoking `Domain.method` I obtain `Domain.method wasn't found`
+
+This means that you are providing a custom or local protocol descriptor
+(`CDP({protocol: customProtocol})`) which declares `Domain.method` while the
+Chrome version that you are using does not support it.
+
+To inspect the currently available protocol descriptor use:
+
+```
+$ chrome-remote-interface inspect
+```
+
+See [here](#chrome-debugging-protocol-versions) for more information.
+
+### Why my program stalls or behave unexpectedly if I run Chrome in a Docker container?
+
+This happens because the size of `/dev/shm` is set to 64MB by default in Docker
+and may not be enough for Chrome to navigate certain web pages.
+
+You can change this value by running your container with, say,
+`--shm-size=256m`.
+
+### Using `Runtime.evaluate` with `awaitPromise: true` I sometimes obtain `Error: Promise was collected`
+
+This is thrown by `Runtime.evaluate` when the browser-side promise gets
+*collected* by the Chrome's garbage collector, this happens when the whole
+JavaScript execution environment is invalidated, e.g., a when page is navigated
+or reloaded while a promise is still waiting to be resolved.
+
+Here is an example:
+
+```
+$ chrome-remote-interface inspect
+>>> Runtime.evaluate({expression: `new Promise(() => {})`, awaitPromise: true})
+>>> Page.reload() // then wait several seconds
+{ result: {} }
+{ error: { code: -32000, message: 'Promise was collected' } }
+```
+
+To fix this, just make sure there are no pending promises before closing,
+reloading, etc. a page.
+
+### How does this compare to Puppeteer?
+
+[Puppeteer] is an additional high-level API built upon the [Chrome Debugging
+Protocol] which, among the other things, may start and use a bundled version of
+Chromium instead of the one installed on your system. Use it if its API meets
+your needs as it would probably be easier to work with.
+
+chrome-remote-interface instead is just a general purpose 1:1 Node.js binding
+for the [Chrome Debugging Protocol]. Use it if you need all the power of the raw
+protocol, e.g., to implement your own high-level API.
+
+See [#240] for a more thorough discussion.
+
+[Puppeteer]: https://github.com/GoogleChrome/puppeteer
+[#240]: https://github.com/cyrus-and/chrome-remote-interface/issues/240
+
+## Contributors
+
+- [Andrey Sidorov](https://github.com/sidorares)
+- [Greg Cochard](https://github.com/gcochard)
+
+## Resources
+
+- [Chrome Debugging Protocol]
+- [Chrome Debugging Protocol Google group](https://groups.google.com/forum/#!forum/chrome-debugging-protocol)
+- [devtools-protocol official repo](https://github.com/ChromeDevTools/devtools-protocol)
+- [Showcase Chrome Debugging Protocol Clients](https://developer.chrome.com/devtools/docs/debugging-clients)
+- [Awesome chrome-devtools](https://github.com/ChromeDevTools/awesome-chrome-devtools)
+
+[Chrome Debugging Protocol]: https://chromedevtools.github.io/devtools-protocol/
diff --git a/node_modules/chrome-remote-interface/bin/client.js b/node_modules/chrome-remote-interface/bin/client.js
new file mode 100755
index 0000000..80b2d4d
--- /dev/null
+++ b/node_modules/chrome-remote-interface/bin/client.js
@@ -0,0 +1,311 @@
+#!/usr/bin/env node
+
+'use strict';
+
+const repl = require('repl');
+const util = require('util');
+const fs = require('fs');
+const path = require('path');
+
+const program = require('commander');
+
+const CDP = require('../');
+const packageInfo = require('../package.json');
+
+function display(object) {
+ return util.inspect(object, {
+ colors: process.stdout.isTTY,
+ depth: null
+ });
+}
+
+function toJSON(object) {
+ return JSON.stringify(object, null, 4);
+}
+
+///
+
+function inspect(target, args, options) {
+ options.local = args.local;
+ // otherwise the active target
+ if (target) {
+ if (args.webSocket) {
+ // by WebSocket URL
+ options.target = target;
+ } else {
+ // by target id
+ options.target = (targets) => {
+ return targets.findIndex((_target) => {
+ return _target.id === target;
+ });
+ };
+ }
+ }
+
+ if (args.protocol) {
+ options.protocol = JSON.parse(fs.readFileSync(args.protocol));
+ }
+
+ CDP(options, (client) => {
+ const cdpRepl = repl.start({
+ prompt: process.stdin.isTTY ? '\x1b[32m>>>\x1b[0m ' : '',
+ ignoreUndefined: true,
+ writer: display
+ });
+
+ // XXX always await promises on the REPL
+ const defaultEval = cdpRepl.eval;
+ cdpRepl.eval = (cmd, context, filename, callback) => {
+ defaultEval(cmd, context, filename, async (err, result) => {
+ if (err) {
+ // propagate errors from the eval
+ callback(err);
+ } else {
+ // awaits the promise and either return result or error
+ try {
+ callback(null, await Promise.resolve(result));
+ } catch (err) {
+ callback(err);
+ }
+ }
+ });
+ };
+
+ const homePath = process.env.HOME || process.env.USERPROFILE;
+ const historyFile = path.join(homePath, '.cri_history');
+ const historySize = 10000;
+
+ function loadHistory() {
+ // only if run from a terminal
+ if (!process.stdin.isTTY) {
+ return;
+ }
+ // attempt to open the history file
+ let fd;
+ try {
+ fd = fs.openSync(historyFile, 'r');
+ } catch (err) {
+ return; // no history file present
+ }
+ // populate the REPL history
+ fs.readFileSync(fd, 'utf8')
+ .split('\n')
+ .filter((entry) => {
+ return entry.trim();
+ })
+ .reverse() // to be compatible with repl.history files
+ .forEach((entry) => {
+ cdpRepl.history.push(entry);
+ });
+ }
+
+ function saveHistory() {
+ // only if run from a terminal
+ if (!process.stdin.isTTY) {
+ return;
+ }
+ // only store the last chunk
+ const entries = cdpRepl.history.slice(0, historySize).reverse().join('\n');
+ fs.writeFileSync(historyFile, entries + '\n');
+ }
+
+ // utility custom command
+ cdpRepl.defineCommand('target', {
+ help: 'Display the current target',
+ action: () => {
+ console.log(client.webSocketUrl);
+ cdpRepl.displayPrompt();
+ }
+ });
+
+ // utility to purge all the event handlers
+ cdpRepl.defineCommand('reset', {
+ help: 'Remove all the registered event handlers',
+ action: () => {
+ client.removeAllListeners();
+ cdpRepl.displayPrompt();
+ }
+ });
+
+ // enable history
+ loadHistory();
+
+ // disconnect on exit
+ cdpRepl.on('exit', () => {
+ if (process.stdin.isTTY) {
+ console.log();
+ }
+ client.close();
+ saveHistory();
+ });
+
+ // exit on disconnection
+ client.on('disconnect', () => {
+ console.error('Disconnected.');
+ saveHistory();
+ process.exit(1);
+ });
+
+ // add protocol API
+ for (const domainObject of client.protocol.domains) {
+ // walk the domain names
+ const domainName = domainObject.domain;
+ cdpRepl.context[domainName] = {};
+ // walk the items in the domain
+ for (const itemName in client[domainName]) {
+ // add CDP object to the REPL context
+ const cdpObject = client[domainName][itemName];
+ cdpRepl.context[domainName][itemName] = cdpObject;
+ }
+ }
+ }).on('error', (err) => {
+ console.error('Cannot connect to remote endpoint:', err.toString());
+ });
+}
+
+function list(options) {
+ CDP.List(options, (err, targets) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(targets));
+ });
+}
+
+function _new(url, options) {
+ options.url = url;
+ CDP.New(options, (err, target) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(target));
+ });
+}
+
+function activate(args, options) {
+ options.id = args;
+ CDP.Activate(options, (err) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ });
+}
+
+function close(args, options) {
+ options.id = args;
+ CDP.Close(options, (err) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ });
+}
+
+function version(options) {
+ CDP.Version(options, (err, info) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(info));
+ });
+}
+
+function protocol(args, options) {
+ options.local = args.local;
+ CDP.Protocol(options, (err, protocol) => {
+ if (err) {
+ console.error(err.toString());
+ process.exit(1);
+ }
+ console.log(toJSON(protocol));
+ });
+}
+
+///
+
+let action;
+
+program
+ .option('-v, --v', 'Show this module version')
+ .option('-t, --host <host>', 'HTTP frontend host')
+ .option('-p, --port <port>', 'HTTP frontend port')
+ .option('-s, --secure', 'HTTPS/WSS frontend')
+ .option('-n, --use-host-name', 'Do not perform a DNS lookup of the host');
+
+program
+ .command('inspect [<target>]')
+ .description('inspect a target (defaults to the first available target)')
+ .option('-w, --web-socket', 'interpret <target> as a WebSocket URL instead of a target id')
+ .option('-j, --protocol <file.json>', 'Chrome Debugging Protocol descriptor (overrides `--local`)')
+ .option('-l, --local', 'Use the local protocol descriptor')
+ .action((target, args) => {
+ action = inspect.bind(null, target, args);
+ });
+
+program
+ .command('list')
+ .description('list all the available targets/tabs')
+ .action(() => {
+ action = list;
+ });
+
+program
+ .command('new [<url>]')
+ .description('create a new target/tab')
+ .action((url) => {
+ action = _new.bind(null, url);
+ });
+
+program
+ .command('activate <id>')
+ .description('activate a target/tab by id')
+ .action((id) => {
+ action = activate.bind(null, id);
+ });
+
+program
+ .command('close <id>')
+ .description('close a target/tab by id')
+ .action((id) => {
+ action = close.bind(null, id);
+ });
+
+program
+ .command('version')
+ .description('show the browser version')
+ .action(() => {
+ action = version;
+ });
+
+program
+ .command('protocol')
+ .description('show the currently available protocol descriptor')
+ .option('-l, --local', 'Return the local protocol descriptor')
+ .action((args) => {
+ action = protocol.bind(null, args);
+ });
+
+program.parse(process.argv);
+
+// common options
+const options = {
+ host: program.host,
+ port: program.port,
+ secure: program.secure,
+ useHostName: program.useHostName
+};
+
+if (action) {
+ action(options);
+} else {
+ if (program.v) {
+ console.log(packageInfo.version);
+ } else {
+ program.outputHelp();
+ process.exit(1);
+ }
+}
diff --git a/node_modules/chrome-remote-interface/chrome-remote-interface.js b/node_modules/chrome-remote-interface/chrome-remote-interface.js
new file mode 100644
index 0000000..6835723
--- /dev/null
+++ b/node_modules/chrome-remote-interface/chrome-remote-interface.js
@@ -0,0 +1 @@
+(()=>{var e={6010:(e,t,r)=>{"use strict";var n=r(4155);const i=r(7187),o=r(4782),a=r(7996),s=r(8855);o.setDefaultResultOrder&&o.setDefaultResultOrder("ipv4first"),e.exports=function(e,t){"function"==typeof e&&(t=e,e=void 0);const r=new i;return"function"==typeof t?(n.nextTick((()=>{new s(e,r)})),r.once("connect",t)):new Promise(((t,n)=>{r.once("connect",t),r.once("error",n),new s(e,r)}))},e.exports.Protocol=a.Protocol,e.exports.List=a.List,e.exports.New=a.New,e.exports.Activate=a.Activate,e.exports.Close=a.Close,e.exports.Version=a.Version},7249:e=>{"use strict";function t(e,t,r){e.category=t,Object.keys(r).forEach((n=>{"name"!==n&&(e[n]="type"===t&&"properties"===n||"parameters"===n?function(e){const t={};return e.forEach((e=>{const r=e.name;delete e.name,t[r]=e})),t}(r[n]):r[n])}))}e.exports.prepare=function(e,r){e.protocol=r,r.domains.forEach((r=>{const n=r.domain;e[n]={},(r.commands||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.name}`,o=(t,r,n)=>e.send(i,t,r,n);t(o,"command",n),e[i]=e[r][n.name]=o}(e,n,r)})),(r.events||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.name}`,o=(t,r)=>{"function"==typeof t&&(r=t,t=void 0);const n=t?`${i}.${t}`:i;return"function"==typeof r?(e.on(n,r),()=>e.removeListener(n,r)):new Promise(((t,r)=>{e.once(n,t)}))};t(o,"event",n),e[i]=e[r][n.name]=o}(e,n,r)})),(r.types||[]).forEach((r=>{!function(e,r,n){const i=`${r}.${n.id}`,o={};t(o,"type",n),e[i]=e[r][n.id]=o}(e,n,r)})),e[n].on=(t,r)=>e[n][t](r)}))}},8855:(e,t,r)=>{"use strict";var n=r(4155);const i=r(7187),o=r(1588),a=r(8575).WU,s=r(8575).Qc,p=r(5529),d=r(7249),c=r(5372),l=r(7996);class u extends Error{constructor(e,t){let{message:r}=t;t.data&&(r+=` (${t.data})`),super(r),this.request=e,this.response=t}}e.exports=class extends i{constructor(e,t){super();e=e||{},this.host=e.host||c.HOST,this.port=e.port||c.PORT,this.secure=!!e.secure,this.useHostName=!!e.useHostName,this.alterPath=e.alterPath||(e=>e),this.protocol=e.protocol,this.local=!!e.local,this.target=e.target||(e=>{let t,r=e.find((e=>!!e.webSocketDebuggerUrl&&(t=t||e,"page"===e.type)));if(r=r||t,r)return r;throw new Error("No inspectable targets")}),this._notifier=t,this._callbacks={},this._nextCommandId=1,this.webSocketUrl=void 0,this._start()}inspect(e,t){return t.customInspect=!1,o.inspect(this,t)}send(e,t,r,n){const i=Array.from(arguments).slice(1);return t=i.find((e=>"object"==typeof e)),r=i.find((e=>"string"==typeof e)),"function"==typeof(n=i.find((e=>"function"==typeof e)))?void this._enqueueCommand(e,t,r,n):new Promise(((n,i)=>{this._enqueueCommand(e,t,r,((o,a)=>{if(o){const n={method:e,params:t,sessionId:r};i(o instanceof Error?o:new u(n,a))}else n(a)}))}))}close(e){const t=e=>{3===this._ws.readyState?e():(this._ws.removeAllListeners("close"),this._ws.once("close",(()=>{this._ws.removeAllListeners(),this._handleConnectionClose(),e()})),this._ws.close())};return"function"==typeof e?void t(e):new Promise(((e,r)=>{t(e)}))}async _start(){const e={host:this.host,port:this.port,secure:this.secure,useHostName:this.useHostName,alterPath:this.alterPath};try{const t=await this._fetchDebuggerURL(e),r=s(t);r.pathname=e.alterPath(r.pathname),this.webSocketUrl=a(r),e.host=r.hostname,e.port=r.port||e.port;const i=await this._fetchProtocol(e);d.prepare(this,i),await this._connectToWebSocket(),n.nextTick((()=>{this._notifier.emit("connect",this)}))}catch(e){this._notifier.emit("error",e)}}async _fetchDebuggerURL(e){const t=this.target;switch(typeof t){case"string":{let r=t;if(r.startsWith("/")&&(r=`ws://${this.host}:${this.port}${r}`),r.match(/^wss?:/i))return r;return(await l.List(e)).find((e=>e.id===r)).webSocketDebuggerUrl}case"object":return t.webSocketDebuggerUrl;case"function":{const r=t,n=await l.List(e),i=r(n);return("number"==typeof i?n[i]:i).webSocketDebuggerUrl}default:throw new Error(`Invalid target argument "${this.target}"`)}}async _fetchProtocol(e){return this.protocol?this.protocol:(e.local=this.local,await l.Protocol(e))}_connectToWebSocket(){return new Promise(((e,t)=>{try{this.secure&&(this.webSocketUrl=this.webSocketUrl.replace(/^ws:/i,"wss:")),this._ws=new p(this.webSocketUrl,[],{maxPayload:268435456,perMessageDeflate:!1,followRedirects:!0})}catch(e){return void t(e)}this._ws.on("open",(()=>{e()})),this._ws.on("message",(e=>{const t=JSON.parse(e);this._handleMessage(t)})),this._ws.on("close",(e=>{this._handleConnectionClose(),this.emit("disconnect")})),this._ws.on("error",(e=>{t(e)}))}))}_handleConnectionClose(){const e=new Error("WebSocket connection closed");for(const t of Object.values(this._callbacks))t(e);this._callbacks={}}_handleMessage(e){if(e.id){const t=this._callbacks[e.id];if(!t)return;e.error?t(!0,e.error):t(!1,e.result||{}),delete this._callbacks[e.id],0===Object.keys(this._callbacks).length&&this.emit("ready")}else if(e.method){const{method:t,params:r,sessionId:n}=e;this.emit("event",e),this.emit(t,r,n),this.emit(`${t}.${n}`,r,n)}}_enqueueCommand(e,t,r,n){const i=this._nextCommandId++,o={id:i,method:e,sessionId:r,params:t||{}};this._ws.send(JSON.stringify(o),(e=>{e?"function"==typeof n&&n(e):this._callbacks[i]=n}))}}},5372:e=>{"use strict";e.exports.HOST="localhost",e.exports.PORT=9222},7996:(e,t,r)=>{"use strict";const n=r(3423),i=r(8532),o=r(5372),a=r(4162);function s(e,t,r){const s=t.secure?i:n,p={method:t.method,host:t.host||o.HOST,port:t.port||o.PORT,useHostName:t.useHostName,path:t.alterPath?t.alterPath(e):e};a(s,p,r)}function p(e){return(t,r)=>("function"==typeof t&&(r=t,t=void 0),t=t||{},"function"==typeof r?void e(t,r):new Promise(((r,n)=>{e(t,((e,t)=>{e?n(e):r(t)}))})))}e.exports.Protocol=p((function(e,t){if(e.local){const e=r(4203);t(null,e)}else s("/json/protocol",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.List=p((function(e,t){s("/json/list",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.New=p((function(e,t){let r="/json/new";Object.prototype.hasOwnProperty.call(e,"url")&&(r+=`?${e.url}`),e.method=e.method||"PUT",s(r,e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))})),e.exports.Activate=p((function(e,t){s("/json/activate/"+e.id,e,(e=>{t(e||null)}))})),e.exports.Close=p((function(e,t){s("/json/close/"+e.id,e,(e=>{t(e||null)}))})),e.exports.Version=p((function(e,t){s("/json/version",e,((e,r)=>{e?t(e):t(null,JSON.parse(r))}))}))},5529:(e,t,r)=>{"use strict";const n=r(7187);e.exports=class extends n{constructor(e){super(),this._ws=new WebSocket(e),this._ws.onopen=()=>{this.emit("open")},this._ws.onclose=()=>{this.emit("close")},this._ws.onmessage=e=>{this.emit("message",e.data)},this._ws.onerror=()=>{this.emit("error",new Error("WebSocket error"))}}close(){this._ws.close()}send(e,t){try{this._ws.send(e),t()}catch(e){t(e)}}}},6124:(e,t,r)=>{"use strict";if(r(1934),r(5666),r(7694),r.g._babelPolyfill)throw new Error("only one instance of babel-polyfill is allowed");r.g._babelPolyfill=!0;function n(e,t,r){e[t]||Object.defineProperty(e,t,{writable:!0,configurable:!0,value:r})}n(String.prototype,"padLeft","".padStart),n(String.prototype,"padRight","".padEnd),"pop,reverse,shift,keys,values,entries,indexOf,every,some,forEach,map,filter,find,findIndex,includes,join,slice,concat,push,splice,unshift,sort,lastIndexOf,reduce,reduceRight,copyWithin,fill".split(",").forEach((function(e){[][e]&&n(Array,e,Function.call.bind([][e]))}))},1924:(e,t,r)=>{"use strict";var n=r(210),i=r(5559),o=i(n("String.prototype.indexOf"));e.exports=function(e,t){var r=n(e,!!t);return"function"==typeof r&&o(e,".prototype.")>-1?i(r):r}},5559:(e,t,r)=>{"use strict";var n=r(8612),i=r(210),o=i("%Function.prototype.apply%"),a=i("%Function.prototype.call%"),s=i("%Reflect.apply%",!0)||n.call(a,o),p=i("%Object.getOwnPropertyDescriptor%",!0),d=i("%Object.defineProperty%",!0),c=i("%Math.max%");if(d)try{d({},"a",{value:1})}catch(e){d=null}e.exports=function(e){var t=s(n,a,arguments);if(p&&d){var r=p(t,"length");r.configurable&&d(t,"length",{value:1+c(0,e.length-(arguments.length-1))})}return t};var l=function(){return s(n,o,arguments)};d?d(e.exports,"apply",{value:l}):e.exports.apply=l},7694:(e,t,r)=>{r(1761),e.exports=r(5645).RegExp.escape},4963:e=>{e.exports=function(e){if("function"!=typeof e)throw TypeError(e+" is not a function!");return e}},3365:(e,t,r)=>{var n=r(2032);e.exports=function(e,t){if("number"!=typeof e&&"Number"!=n(e))throw TypeError(t);return+e}},7722:(e,t,r)=>{var n=r(6314)("unscopables"),i=Array.prototype;null==i[n]&&r(7728)(i,n,{}),e.exports=function(e){i[n][e]=!0}},6793:(e,t,r)=>{"use strict";var n=r(4496)(!0);e.exports=function(e,t,r){return t+(r?n(e,t).length:1)}},3328:e=>{e.exports=function(e,t,r,n){if(!(e instanceof t)||void 0!==n&&n in e)throw TypeError(r+": incorrect invocation!");return e}},7007:(e,t,r)=>{var n=r(5286);e.exports=function(e){if(!n(e))throw TypeError(e+" is not an object!");return e}},5216:(e,t,r)=>{"use strict";var n=r(508),i=r(2337),o=r(875);e.exports=[].copyWithin||function(e,t){var r=n(this),a=o(r.length),s=i(e,a),p=i(t,a),d=arguments.length>2?arguments[2]:void 0,c=Math.min((void 0===d?a:i(d,a))-p,a-s),l=1;for(p<s&&s<p+c&&(l=-1,p+=c-1,s+=c-1);c-- >0;)p in r?r[s]=r[p]:delete r[s],s+=l,p+=l;return r}},6852:(e,t,r)=>{"use strict";var n=r(508),i=r(2337),o=r(875);e.exports=function(e){for(var t=n(this),r=o(t.length),a=arguments.length,s=i(a>1?arguments[1]:void 0,r),p=a>2?arguments[2]:void 0,d=void 0===p?r:i(p,r);d>s;)t[s++]=e;return t}},9490:(e,t,r)=>{var n=r(3531);e.exports=function(e,t){var r=[];return n(e,!1,r.push,r,t),r}},9315:(e,t,r)=>{var n=r(2110),i=r(875),o=r(2337);e.exports=function(e){return function(t,r,a){var s,p=n(t),d=i(p.length),c=o(a,d);if(e&&r!=r){for(;d>c;)if((s=p[c++])!=s)return!0}else for(;d>c;c++)if((e||c in p)&&p[c]===r)return e||c||0;return!e&&-1}}},50:(e,t,r)=>{var n=r(741),i=r(9797),o=r(508),a=r(875),s=r(6886);e.exports=function(e,t){var r=1==e,p=2==e,d=3==e,c=4==e,l=6==e,u=5==e||l,m=t||s;return function(t,s,h){for(var f,y,g=o(t),b=i(g),v=n(s,h,3),w=a(b.length),S=0,I=r?m(t,w):p?m(t,0):void 0;w>S;S++)if((u||S in b)&&(y=v(f=b[S],S,g),e))if(r)I[S]=y;else if(y)switch(e){case 3:return!0;case 5:return f;case 6:return S;case 2:I.push(f)}else if(c)return!1;return l?-1:d||c?c:I}}},7628:(e,t,r)=>{var n=r(4963),i=r(508),o=r(9797),a=r(875);e.exports=function(e,t,r,s,p){n(t);var d=i(e),c=o(d),l=a(d.length),u=p?l-1:0,m=p?-1:1;if(r<2)for(;;){if(u in c){s=c[u],u+=m;break}if(u+=m,p?u<0:l<=u)throw TypeError("Reduce of empty array with no initial value")}for(;p?u>=0:l>u;u+=m)u in c&&(s=t(s,c[u],u,d));return s}},2736:(e,t,r)=>{var n=r(5286),i=r(4302),o=r(6314)("species");e.exports=function(e){var t;return i(e)&&("function"!=typeof(t=e.constructor)||t!==Array&&!i(t.prototype)||(t=void 0),n(t)&&null===(t=t[o])&&(t=void 0)),void 0===t?Array:t}},6886:(e,t,r)=>{var n=r(2736);e.exports=function(e,t){return new(n(e))(t)}},4398:(e,t,r)=>{"use strict";var n=r(4963),i=r(5286),o=r(7242),a=[].slice,s={},p=function(e,t,r){if(!(t in s)){for(var n=[],i=0;i<t;i++)n[i]="a["+i+"]";s[t]=Function("F,a","return new F("+n.join(",")+")")}return s[t](e,r)};e.exports=Function.bind||function(e){var t=n(this),r=a.call(arguments,1),s=function(){var n=r.concat(a.call(arguments));return this instanceof s?p(t,n.length,n):o(t,n,e)};return i(t.prototype)&&(s.prototype=t.prototype),s}},1488:(e,t,r)=>{var n=r(2032),i=r(6314)("toStringTag"),o="Arguments"==n(function(){return arguments}());e.exports=function(e){var t,r,a;return void 0===e?"Undefined":null===e?"Null":"string"==typeof(r=function(e,t){try{return e[t]}catch(e){}}(t=Object(e),i))?r:o?n(t):"Object"==(a=n(t))&&"function"==typeof t.callee?"Arguments":a}},2032:e=>{var t={}.toString;e.exports=function(e){return t.call(e).slice(8,-1)}},9824:(e,t,r)=>{"use strict";var n=r(9275).f,i=r(2503),o=r(4408),a=r(741),s=r(3328),p=r(3531),d=r(2923),c=r(5436),l=r(2974),u=r(7057),m=r(4728).fastKey,h=r(1616),f=u?"_s":"size",y=function(e,t){var r,n=m(t);if("F"!==n)return e._i[n];for(r=e._f;r;r=r.n)if(r.k==t)return r};e.exports={getConstructor:function(e,t,r,d){var c=e((function(e,n){s(e,c,t,"_i"),e._t=t,e._i=i(null),e._f=void 0,e._l=void 0,e[f]=0,null!=n&&p(n,r,e[d],e)}));return o(c.prototype,{clear:function(){for(var e=h(this,t),r=e._i,n=e._f;n;n=n.n)n.r=!0,n.p&&(n.p=n.p.n=void 0),delete r[n.i];e._f=e._l=void 0,e[f]=0},delete:function(e){var r=h(this,t),n=y(r,e);if(n){var i=n.n,o=n.p;delete r._i[n.i],n.r=!0,o&&(o.n=i),i&&(i.p=o),r._f==n&&(r._f=i),r._l==n&&(r._l=o),r[f]--}return!!n},forEach:function(e){h(this,t);for(var r,n=a(e,arguments.length>1?arguments[1]:void 0,3);r=r?r.n:this._f;)for(n(r.v,r.k,this);r&&r.r;)r=r.p},has:function(e){return!!y(h(this,t),e)}}),u&&n(c.prototype,"size",{get:function(){return h(this,t)[f]}}),c},def:function(e,t,r){var n,i,o=y(e,t);return o?o.v=r:(e._l=o={i:i=m(t,!0),k:t,v:r,p:n=e._l,n:void 0,r:!1},e._f||(e._f=o),n&&(n.n=o),e[f]++,"F"!==i&&(e._i[i]=o)),e},getEntry:y,setStrong:function(e,t,r){d(e,t,(function(e,r){this._t=h(e,t),this._k=r,this._l=void 0}),(function(){for(var e=this,t=e._k,r=e._l;r&&r.r;)r=r.p;return e._t&&(e._l=r=r?r.n:e._t._f)?c(0,"keys"==t?r.k:"values"==t?r.v:[r.k,r.v]):(e._t=void 0,c(1))}),r?"entries":"values",!r,!0),l(t)}}},6132:(e,t,r)=>{var n=r(1488),i=r(9490);e.exports=function(e){return function(){if(n(this)!=e)throw TypeError(e+"#toJSON isn't generic");return i(this)}}},3657:(e,t,r)=>{"use strict";var n=r(4408),i=r(4728).getWeak,o=r(7007),a=r(5286),s=r(3328),p=r(3531),d=r(50),c=r(9181),l=r(1616),u=d(5),m=d(6),h=0,f=function(e){return e._l||(e._l=new y)},y=function(){this.a=[]},g=function(e,t){return u(e.a,(function(e){return e[0]===t}))};y.prototype={get:function(e){var t=g(this,e);if(t)return t[1]},has:function(e){return!!g(this,e)},set:function(e,t){var r=g(this,e);r?r[1]=t:this.a.push([e,t])},delete:function(e){var t=m(this.a,(function(t){return t[0]===e}));return~t&&this.a.splice(t,1),!!~t}},e.exports={getConstructor:function(e,t,r,o){var d=e((function(e,n){s(e,d,t,"_i"),e._t=t,e._i=h++,e._l=void 0,null!=n&&p(n,r,e[o],e)}));return n(d.prototype,{delete:function(e){if(!a(e))return!1;var r=i(e);return!0===r?f(l(this,t)).delete(e):r&&c(r,this._i)&&delete r[this._i]},has:function(e){if(!a(e))return!1;var r=i(e);return!0===r?f(l(this,t)).has(e):r&&c(r,this._i)}}),d},def:function(e,t,r){var n=i(o(t),!0);return!0===n?f(e).set(t,r):n[e._i]=r,e},ufstore:f}},5795:(e,t,r)=>{"use strict";var n=r(3816),i=r(2985),o=r(7234),a=r(4408),s=r(4728),p=r(3531),d=r(3328),c=r(5286),l=r(4253),u=r(7462),m=r(2943),h=r(266);e.exports=function(e,t,r,f,y,g){var b=n[e],v=b,w=y?"set":"add",S=v&&v.prototype,I={},x=function(e){var t=S[e];o(S,e,"delete"==e||"has"==e?function(e){return!(g&&!c(e))&&t.call(this,0===e?0:e)}:"get"==e?function(e){return g&&!c(e)?void 0:t.call(this,0===e?0:e)}:"add"==e?function(e){return t.call(this,0===e?0:e),this}:function(e,r){return t.call(this,0===e?0:e,r),this})};if("function"==typeof v&&(g||S.forEach&&!l((function(){(new v).entries().next()})))){var k=new v,T=k[w](g?{}:-0,1)!=k,R=l((function(){k.has(1)})),C=u((function(e){new v(e)})),$=!g&&l((function(){for(var e=new v,t=5;t--;)e[w](t,t);return!e.has(-0)}));C||((v=t((function(t,r){d(t,v,e);var n=h(new b,t,v);return null!=r&&p(r,y,n[w],n),n}))).prototype=S,S.constructor=v),(R||$)&&(x("delete"),x("has"),y&&x("get")),($||T)&&x(w),g&&S.clear&&delete S.clear}else v=f.getConstructor(t,e,y,w),a(v.prototype,r),s.NEED=!0;return m(v,e),I[e]=v,i(i.G+i.W+i.F*(v!=b),I),g||f.setStrong(v,e,y),v}},5645:e=>{var t=e.exports={version:"2.6.12"};"number"==typeof __e&&(__e=t)},2811:(e,t,r)=>{"use strict";var n=r(9275),i=r(681);e.exports=function(e,t,r){t in e?n.f(e,t,i(0,r)):e[t]=r}},741:(e,t,r)=>{var n=r(4963);e.exports=function(e,t,r){if(n(e),void 0===t)return e;switch(r){case 1:return function(r){return e.call(t,r)};case 2:return function(r,n){return e.call(t,r,n)};case 3:return function(r,n,i){return e.call(t,r,n,i)}}return function(){return e.apply(t,arguments)}}},3537:(e,t,r)=>{"use strict";var n=r(4253),i=Date.prototype.getTime,o=Date.prototype.toISOString,a=function(e){return e>9?e:"0"+e};e.exports=n((function(){return"0385-07-25T07:06:39.999Z"!=o.call(new Date(-50000000000001))}))||!n((function(){o.call(new Date(NaN))}))?function(){if(!isFinite(i.call(this)))throw RangeError("Invalid time value");var e=this,t=e.getUTCFullYear(),r=e.getUTCMilliseconds(),n=t<0?"-":t>9999?"+":"";return n+("00000"+Math.abs(t)).slice(n?-6:-4)+"-"+a(e.getUTCMonth()+1)+"-"+a(e.getUTCDate())+"T"+a(e.getUTCHours())+":"+a(e.getUTCMinutes())+":"+a(e.getUTCSeconds())+"."+(r>99?r:"0"+a(r))+"Z"}:o},870:(e,t,r)=>{"use strict";var n=r(7007),i=r(1689),o="number";e.exports=function(e){if("string"!==e&&e!==o&&"default"!==e)throw TypeError("Incorrect hint");return i(n(this),e!=o)}},1355:e=>{e.exports=function(e){if(null==e)throw TypeError("Can't call method on "+e);return e}},7057:(e,t,r)=>{e.exports=!r(4253)((function(){return 7!=Object.defineProperty({},"a",{get:function(){return 7}}).a}))},2457:(e,t,r)=>{var n=r(5286),i=r(3816).document,o=n(i)&&n(i.createElement);e.exports=function(e){return o?i.createElement(e):{}}},4430:e=>{e.exports="constructor,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,toLocaleString,toString,valueOf".split(",")},5541:(e,t,r)=>{var n=r(7184),i=r(4548),o=r(4682);e.exports=function(e){var t=n(e),r=i.f;if(r)for(var a,s=r(e),p=o.f,d=0;s.length>d;)p.call(e,a=s[d++])&&t.push(a);return t}},2985:(e,t,r)=>{var n=r(3816),i=r(5645),o=r(7728),a=r(7234),s=r(741),p=function(e,t,r){var d,c,l,u,m=e&p.F,h=e&p.G,f=e&p.S,y=e&p.P,g=e&p.B,b=h?n:f?n[t]||(n[t]={}):(n[t]||{}).prototype,v=h?i:i[t]||(i[t]={}),w=v.prototype||(v.prototype={});for(d in h&&(r=t),r)l=((c=!m&&b&&void 0!==b[d])?b:r)[d],u=g&&c?s(l,n):y&&"function"==typeof l?s(Function.call,l):l,b&&a(b,d,l,e&p.U),v[d]!=l&&o(v,d,u),y&&w[d]!=l&&(w[d]=l)};n.core=i,p.F=1,p.G=2,p.S=4,p.P=8,p.B=16,p.W=32,p.U=64,p.R=128,e.exports=p},8852:(e,t,r)=>{var n=r(6314)("match");e.exports=function(e){var t=/./;try{"/./"[e](t)}catch(r){try{return t[n]=!1,!"/./"[e](t)}catch(e){}}return!0}},4253:e=>{e.exports=function(e){try{return!!e()}catch(e){return!0}}},8082:(e,t,r)=>{"use strict";r(8269);var n=r(7234),i=r(7728),o=r(4253),a=r(1355),s=r(6314),p=r(1165),d=s("species"),c=!o((function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$<a>")})),l=function(){var e=/(?:)/,t=e.exec;e.exec=function(){return t.apply(this,arguments)};var r="ab".split(e);return 2===r.length&&"a"===r[0]&&"b"===r[1]}();e.exports=function(e,t,r){var u=s(e),m=!o((function(){var t={};return t[u]=function(){return 7},7!=""[e](t)})),h=m?!o((function(){var t=!1,r=/a/;return r.exec=function(){return t=!0,null},"split"===e&&(r.constructor={},r.constructor[d]=function(){return r}),r[u](""),!t})):void 0;if(!m||!h||"replace"===e&&!c||"split"===e&&!l){var f=/./[u],y=r(a,u,""[e],(function(e,t,r,n,i){return t.exec===p?m&&!i?{done:!0,value:f.call(t,r,n)}:{done:!0,value:e.call(r,t,n)}:{done:!1}})),g=y[0],b=y[1];n(String.prototype,e,g),i(RegExp.prototype,u,2==t?function(e,t){return b.call(e,this,t)}:function(e){return b.call(e,this)})}}},3218:(e,t,r)=>{"use strict";var n=r(7007);e.exports=function(){var e=n(this),t="";return e.global&&(t+="g"),e.ignoreCase&&(t+="i"),e.multiline&&(t+="m"),e.unicode&&(t+="u"),e.sticky&&(t+="y"),t}},3325:(e,t,r)=>{"use strict";var n=r(4302),i=r(5286),o=r(875),a=r(741),s=r(6314)("isConcatSpreadable");e.exports=function e(t,r,p,d,c,l,u,m){for(var h,f,y=c,g=0,b=!!u&&a(u,m,3);g<d;){if(g in p){if(h=b?b(p[g],g,r):p[g],f=!1,i(h)&&(f=void 0!==(f=h[s])?!!f:n(h)),f&&l>0)y=e(t,r,h,o(h.length),y,l-1)-1;else{if(y>=9007199254740991)throw TypeError();t[y]=h}y++}g++}return y}},3531:(e,t,r)=>{var n=r(741),i=r(8851),o=r(6555),a=r(7007),s=r(875),p=r(9002),d={},c={},l=e.exports=function(e,t,r,l,u){var m,h,f,y,g=u?function(){return e}:p(e),b=n(r,l,t?2:1),v=0;if("function"!=typeof g)throw TypeError(e+" is not iterable!");if(o(g)){for(m=s(e.length);m>v;v++)if((y=t?b(a(h=e[v])[0],h[1]):b(e[v]))===d||y===c)return y}else for(f=g.call(e);!(h=f.next()).done;)if((y=i(f,b,h.value,t))===d||y===c)return y};l.BREAK=d,l.RETURN=c},18:(e,t,r)=>{e.exports=r(3825)("native-function-to-string",Function.toString)},3816:e=>{var t=e.exports="undefined"!=typeof window&&window.Math==Math?window:"undefined"!=typeof self&&self.Math==Math?self:Function("return this")();"number"==typeof __g&&(__g=t)},9181:e=>{var t={}.hasOwnProperty;e.exports=function(e,r){return t.call(e,r)}},7728:(e,t,r)=>{var n=r(9275),i=r(681);e.exports=r(7057)?function(e,t,r){return n.f(e,t,i(1,r))}:function(e,t,r){return e[t]=r,e}},639:(e,t,r)=>{var n=r(3816).document;e.exports=n&&n.documentElement},1734:(e,t,r)=>{e.exports=!r(7057)&&!r(4253)((function(){return 7!=Object.defineProperty(r(2457)("div"),"a",{get:function(){return 7}}).a}))},266:(e,t,r)=>{var n=r(5286),i=r(7375).set;e.exports=function(e,t,r){var o,a=t.constructor;return a!==r&&"function"==typeof a&&(o=a.prototype)!==r.prototype&&n(o)&&i&&i(e,o),e}},7242:e=>{e.exports=function(e,t,r){var n=void 0===r;switch(t.length){case 0:return n?e():e.call(r);case 1:return n?e(t[0]):e.call(r,t[0]);case 2:return n?e(t[0],t[1]):e.call(r,t[0],t[1]);case 3:return n?e(t[0],t[1],t[2]):e.call(r,t[0],t[1],t[2]);case 4:return n?e(t[0],t[1],t[2],t[3]):e.call(r,t[0],t[1],t[2],t[3])}return e.apply(r,t)}},9797:(e,t,r)=>{var n=r(2032);e.exports=Object("z").propertyIsEnumerable(0)?Object:function(e){return"String"==n(e)?e.split(""):Object(e)}},6555:(e,t,r)=>{var n=r(2803),i=r(6314)("iterator"),o=Array.prototype;e.exports=function(e){return void 0!==e&&(n.Array===e||o[i]===e)}},4302:(e,t,r)=>{var n=r(2032);e.exports=Array.isArray||function(e){return"Array"==n(e)}},8367:(e,t,r)=>{var n=r(5286),i=Math.floor;e.exports=function(e){return!n(e)&&isFinite(e)&&i(e)===e}},5286:e=>{e.exports=function(e){return"object"==typeof e?null!==e:"function"==typeof e}},5364:(e,t,r)=>{var n=r(5286),i=r(2032),o=r(6314)("match");e.exports=function(e){var t;return n(e)&&(void 0!==(t=e[o])?!!t:"RegExp"==i(e))}},8851:(e,t,r)=>{var n=r(7007);e.exports=function(e,t,r,i){try{return i?t(n(r)[0],r[1]):t(r)}catch(t){var o=e.return;throw void 0!==o&&n(o.call(e)),t}}},9988:(e,t,r)=>{"use strict";var n=r(2503),i=r(681),o=r(2943),a={};r(7728)(a,r(6314)("iterator"),(function(){return this})),e.exports=function(e,t,r){e.prototype=n(a,{next:i(1,r)}),o(e,t+" Iterator")}},2923:(e,t,r)=>{"use strict";var n=r(4461),i=r(2985),o=r(7234),a=r(7728),s=r(2803),p=r(9988),d=r(2943),c=r(468),l=r(6314)("iterator"),u=!([].keys&&"next"in[].keys()),m="keys",h="values",f=function(){return this};e.exports=function(e,t,r,y,g,b,v){p(r,t,y);var w,S,I,x=function(e){if(!u&&e in C)return C[e];switch(e){case m:case h:return function(){return new r(this,e)}}return function(){return new r(this,e)}},k=t+" Iterator",T=g==h,R=!1,C=e.prototype,$=C[l]||C["@@iterator"]||g&&C[g],A=$||x(g),O=g?T?x("entries"):A:void 0,P="Array"==t&&C.entries||$;if(P&&(I=c(P.call(new e)))!==Object.prototype&&I.next&&(d(I,k,!0),n||"function"==typeof I[l]||a(I,l,f)),T&&$&&$.name!==h&&(R=!0,A=function(){return $.call(this)}),n&&!v||!u&&!R&&C[l]||a(C,l,A),s[t]=A,s[k]=f,g)if(w={values:T?A:x(h),keys:b?A:x(m),entries:O},v)for(S in w)S in C||o(C,S,w[S]);else i(i.P+i.F*(u||R),t,w);return w}},7462:(e,t,r)=>{var n=r(6314)("iterator"),i=!1;try{var o=[7][n]();o.return=function(){i=!0},Array.from(o,(function(){throw 2}))}catch(e){}e.exports=function(e,t){if(!t&&!i)return!1;var r=!1;try{var o=[7],a=o[n]();a.next=function(){return{done:r=!0}},o[n]=function(){return a},e(o)}catch(e){}return r}},5436:e=>{e.exports=function(e,t){return{value:t,done:!!e}}},2803:e=>{e.exports={}},4461:e=>{e.exports=!1},3086:e=>{var t=Math.expm1;e.exports=!t||t(10)>22025.465794806718||t(10)<22025.465794806718||-2e-17!=t(-2e-17)?function(e){return 0==(e=+e)?e:e>-1e-6&&e<1e-6?e+e*e/2:Math.exp(e)-1}:t},4934:(e,t,r)=>{var n=r(1801),i=Math.pow,o=i(2,-52),a=i(2,-23),s=i(2,127)*(2-a),p=i(2,-126);e.exports=Math.fround||function(e){var t,r,i=Math.abs(e),d=n(e);return i<p?d*(i/p/a+1/o-1/o)*p*a:(r=(t=(1+a/o)*i)-(t-i))>s||r!=r?d*(1/0):d*r}},6206:e=>{e.exports=Math.log1p||function(e){return(e=+e)>-1e-8&&e<1e-8?e-e*e/2:Math.log(1+e)}},8757:e=>{e.exports=Math.scale||function(e,t,r,n,i){return 0===arguments.length||e!=e||t!=t||r!=r||n!=n||i!=i?NaN:e===1/0||e===-1/0?e:(e-t)*(i-n)/(r-t)+n}},1801:e=>{e.exports=Math.sign||function(e){return 0==(e=+e)||e!=e?e:e<0?-1:1}},4728:(e,t,r)=>{var n=r(3953)("meta"),i=r(5286),o=r(9181),a=r(9275).f,s=0,p=Object.isExtensible||function(){return!0},d=!r(4253)((function(){return p(Object.preventExtensions({}))})),c=function(e){a(e,n,{value:{i:"O"+ ++s,w:{}}})},l=e.exports={KEY:n,NEED:!1,fastKey:function(e,t){if(!i(e))return"symbol"==typeof e?e:("string"==typeof e?"S":"P")+e;if(!o(e,n)){if(!p(e))return"F";if(!t)return"E";c(e)}return e[n].i},getWeak:function(e,t){if(!o(e,n)){if(!p(e))return!0;if(!t)return!1;c(e)}return e[n].w},onFreeze:function(e){return d&&l.NEED&&p(e)&&!o(e,n)&&c(e),e}}},133:(e,t,r)=>{var n=r(8416),i=r(2985),o=r(3825)("metadata"),a=o.store||(o.store=new(r(147))),s=function(e,t,r){var i=a.get(e);if(!i){if(!r)return;a.set(e,i=new n)}var o=i.get(t);if(!o){if(!r)return;i.set(t,o=new n)}return o};e.exports={store:a,map:s,has:function(e,t,r){var n=s(t,r,!1);return void 0!==n&&n.has(e)},get:function(e,t,r){var n=s(t,r,!1);return void 0===n?void 0:n.get(e)},set:function(e,t,r,n){s(r,n,!0).set(e,t)},keys:function(e,t){var r=s(e,t,!1),n=[];return r&&r.forEach((function(e,t){n.push(t)})),n},key:function(e){return void 0===e||"symbol"==typeof e?e:String(e)},exp:function(e){i(i.S,"Reflect",e)}}},4351:(e,t,r)=>{var n=r(3816),i=r(4193).set,o=n.MutationObserver||n.WebKitMutationObserver,a=n.process,s=n.Promise,p="process"==r(2032)(a);e.exports=function(){var e,t,r,d=function(){var n,i;for(p&&(n=a.domain)&&n.exit();e;){i=e.fn,e=e.next;try{i()}catch(n){throw e?r():t=void 0,n}}t=void 0,n&&n.enter()};if(p)r=function(){a.nextTick(d)};else if(!o||n.navigator&&n.navigator.standalone)if(s&&s.resolve){var c=s.resolve(void 0);r=function(){c.then(d)}}else r=function(){i.call(n,d)};else{var l=!0,u=document.createTextNode("");new o(d).observe(u,{characterData:!0}),r=function(){u.data=l=!l}}return function(n){var i={fn:n,next:void 0};t&&(t.next=i),e||(e=i,r()),t=i}}},3499:(e,t,r)=>{"use strict";var n=r(4963);function i(e){var t,r;this.promise=new e((function(e,n){if(void 0!==t||void 0!==r)throw TypeError("Bad Promise constructor");t=e,r=n})),this.resolve=n(t),this.reject=n(r)}e.exports.f=function(e){return new i(e)}},5345:(e,t,r)=>{"use strict";var n=r(7057),i=r(7184),o=r(4548),a=r(4682),s=r(508),p=r(9797),d=Object.assign;e.exports=!d||r(4253)((function(){var e={},t={},r=Symbol(),n="abcdefghijklmnopqrst";return e[r]=7,n.split("").forEach((function(e){t[e]=e})),7!=d({},e)[r]||Object.keys(d({},t)).join("")!=n}))?function(e,t){for(var r=s(e),d=arguments.length,c=1,l=o.f,u=a.f;d>c;)for(var m,h=p(arguments[c++]),f=l?i(h).concat(l(h)):i(h),y=f.length,g=0;y>g;)m=f[g++],n&&!u.call(h,m)||(r[m]=h[m]);return r}:d},2503:(e,t,r)=>{var n=r(7007),i=r(5588),o=r(4430),a=r(9335)("IE_PROTO"),s=function(){},p=function(){var e,t=r(2457)("iframe"),n=o.length;for(t.style.display="none",r(639).appendChild(t),t.src="javascript:",(e=t.contentWindow.document).open(),e.write("<script>document.F=Object<\/script>"),e.close(),p=e.F;n--;)delete p.prototype[o[n]];return p()};e.exports=Object.create||function(e,t){var r;return null!==e?(s.prototype=n(e),r=new s,s.prototype=null,r[a]=e):r=p(),void 0===t?r:i(r,t)}},9275:(e,t,r)=>{var n=r(7007),i=r(1734),o=r(1689),a=Object.defineProperty;t.f=r(7057)?Object.defineProperty:function(e,t,r){if(n(e),t=o(t,!0),n(r),i)try{return a(e,t,r)}catch(e){}if("get"in r||"set"in r)throw TypeError("Accessors not supported!");return"value"in r&&(e[t]=r.value),e}},5588:(e,t,r)=>{var n=r(9275),i=r(7007),o=r(7184);e.exports=r(7057)?Object.defineProperties:function(e,t){i(e);for(var r,a=o(t),s=a.length,p=0;s>p;)n.f(e,r=a[p++],t[r]);return e}},1670:(e,t,r)=>{"use strict";e.exports=r(4461)||!r(4253)((function(){var e=Math.random();__defineSetter__.call(null,e,(function(){})),delete r(3816)[e]}))},8693:(e,t,r)=>{var n=r(4682),i=r(681),o=r(2110),a=r(1689),s=r(9181),p=r(1734),d=Object.getOwnPropertyDescriptor;t.f=r(7057)?d:function(e,t){if(e=o(e),t=a(t,!0),p)try{return d(e,t)}catch(e){}if(s(e,t))return i(!n.f.call(e,t),e[t])}},9327:(e,t,r)=>{var n=r(2110),i=r(616).f,o={}.toString,a="object"==typeof window&&window&&Object.getOwnPropertyNames?Object.getOwnPropertyNames(window):[];e.exports.f=function(e){return a&&"[object Window]"==o.call(e)?function(e){try{return i(e)}catch(e){return a.slice()}}(e):i(n(e))}},616:(e,t,r)=>{var n=r(189),i=r(4430).concat("length","prototype");t.f=Object.getOwnPropertyNames||function(e){return n(e,i)}},4548:(e,t)=>{t.f=Object.getOwnPropertySymbols},468:(e,t,r)=>{var n=r(9181),i=r(508),o=r(9335)("IE_PROTO"),a=Object.prototype;e.exports=Object.getPrototypeOf||function(e){return e=i(e),n(e,o)?e[o]:"function"==typeof e.constructor&&e instanceof e.constructor?e.constructor.prototype:e instanceof Object?a:null}},189:(e,t,r)=>{var n=r(9181),i=r(2110),o=r(9315)(!1),a=r(9335)("IE_PROTO");e.exports=function(e,t){var r,s=i(e),p=0,d=[];for(r in s)r!=a&&n(s,r)&&d.push(r);for(;t.length>p;)n(s,r=t[p++])&&(~o(d,r)||d.push(r));return d}},7184:(e,t,r)=>{var n=r(189),i=r(4430);e.exports=Object.keys||function(e){return n(e,i)}},4682:(e,t)=>{t.f={}.propertyIsEnumerable},3160:(e,t,r)=>{var n=r(2985),i=r(5645),o=r(4253);e.exports=function(e,t){var r=(i.Object||{})[e]||Object[e],a={};a[e]=t(r),n(n.S+n.F*o((function(){r(1)})),"Object",a)}},1131:(e,t,r)=>{var n=r(7057),i=r(7184),o=r(2110),a=r(4682).f;e.exports=function(e){return function(t){for(var r,s=o(t),p=i(s),d=p.length,c=0,l=[];d>c;)r=p[c++],n&&!a.call(s,r)||l.push(e?[r,s[r]]:s[r]);return l}}},7643:(e,t,r)=>{var n=r(616),i=r(4548),o=r(7007),a=r(3816).Reflect;e.exports=a&&a.ownKeys||function(e){var t=n.f(o(e)),r=i.f;return r?t.concat(r(e)):t}},7743:(e,t,r)=>{var n=r(3816).parseFloat,i=r(9599).trim;e.exports=1/n(r(4644)+"-0")!=-1/0?function(e){var t=i(String(e),3),r=n(t);return 0===r&&"-"==t.charAt(0)?-0:r}:n},5960:(e,t,r)=>{var n=r(3816).parseInt,i=r(9599).trim,o=r(4644),a=/^[-+]?0[xX]/;e.exports=8!==n(o+"08")||22!==n(o+"0x16")?function(e,t){var r=i(String(e),3);return n(r,t>>>0||(a.test(r)?16:10))}:n},188:e=>{e.exports=function(e){try{return{e:!1,v:e()}}catch(e){return{e:!0,v:e}}}},94:(e,t,r)=>{var n=r(7007),i=r(5286),o=r(3499);e.exports=function(e,t){if(n(e),i(t)&&t.constructor===e)return t;var r=o.f(e);return(0,r.resolve)(t),r.promise}},681:e=>{e.exports=function(e,t){return{enumerable:!(1&e),configurable:!(2&e),writable:!(4&e),value:t}}},4408:(e,t,r)=>{var n=r(7234);e.exports=function(e,t,r){for(var i in t)n(e,i,t[i],r);return e}},7234:(e,t,r)=>{var n=r(3816),i=r(7728),o=r(9181),a=r(3953)("src"),s=r(18),p="toString",d=(""+s).split(p);r(5645).inspectSource=function(e){return s.call(e)},(e.exports=function(e,t,r,s){var p="function"==typeof r;p&&(o(r,"name")||i(r,"name",t)),e[t]!==r&&(p&&(o(r,a)||i(r,a,e[t]?""+e[t]:d.join(String(t)))),e===n?e[t]=r:s?e[t]?e[t]=r:i(e,t,r):(delete e[t],i(e,t,r)))})(Function.prototype,p,(function(){return"function"==typeof this&&this[a]||s.call(this)}))},7787:(e,t,r)=>{"use strict";var n=r(1488),i=RegExp.prototype.exec;e.exports=function(e,t){var r=e.exec;if("function"==typeof r){var o=r.call(e,t);if("object"!=typeof o)throw new TypeError("RegExp exec method returned something other than an Object or null");return o}if("RegExp"!==n(e))throw new TypeError("RegExp#exec called on incompatible receiver");return i.call(e,t)}},1165:(e,t,r)=>{"use strict";var n,i,o=r(3218),a=RegExp.prototype.exec,s=String.prototype.replace,p=a,d=(n=/a/,i=/b*/g,a.call(n,"a"),a.call(i,"a"),0!==n.lastIndex||0!==i.lastIndex),c=void 0!==/()??/.exec("")[1];(d||c)&&(p=function(e){var t,r,n,i,p=this;return c&&(r=new RegExp("^"+p.source+"$(?!\\s)",o.call(p))),d&&(t=p.lastIndex),n=a.call(p,e),d&&n&&(p.lastIndex=p.global?n.index+n[0].length:t),c&&n&&n.length>1&&s.call(n[0],r,(function(){for(i=1;i<arguments.length-2;i++)void 0===arguments[i]&&(n[i]=void 0)})),n}),e.exports=p},5496:e=>{e.exports=function(e,t){var r=t===Object(t)?function(e){return t[e]}:t;return function(t){return String(t).replace(e,r)}}},7195:e=>{e.exports=Object.is||function(e,t){return e===t?0!==e||1/e==1/t:e!=e&&t!=t}},1024:(e,t,r)=>{"use strict";var n=r(2985),i=r(4963),o=r(741),a=r(3531);e.exports=function(e){n(n.S,e,{from:function(e){var t,r,n,s,p=arguments[1];return i(this),(t=void 0!==p)&&i(p),null==e?new this:(r=[],t?(n=0,s=o(p,arguments[2],2),a(e,!1,(function(e){r.push(s(e,n++))}))):a(e,!1,r.push,r),new this(r))}})}},4881:(e,t,r)=>{"use strict";var n=r(2985);e.exports=function(e){n(n.S,e,{of:function(){for(var e=arguments.length,t=new Array(e);e--;)t[e]=arguments[e];return new this(t)}})}},7375:(e,t,r)=>{var n=r(5286),i=r(7007),o=function(e,t){if(i(e),!n(t)&&null!==t)throw TypeError(t+": can't set as prototype!")};e.exports={set:Object.setPrototypeOf||("__proto__"in{}?function(e,t,n){try{(n=r(741)(Function.call,r(8693).f(Object.prototype,"__proto__").set,2))(e,[]),t=!(e instanceof Array)}catch(e){t=!0}return function(e,r){return o(e,r),t?e.__proto__=r:n(e,r),e}}({},!1):void 0),check:o}},2974:(e,t,r)=>{"use strict";var n=r(3816),i=r(9275),o=r(7057),a=r(6314)("species");e.exports=function(e){var t=n[e];o&&t&&!t[a]&&i.f(t,a,{configurable:!0,get:function(){return this}})}},2943:(e,t,r)=>{var n=r(9275).f,i=r(9181),o=r(6314)("toStringTag");e.exports=function(e,t,r){e&&!i(e=r?e:e.prototype,o)&&n(e,o,{configurable:!0,value:t})}},9335:(e,t,r)=>{var n=r(3825)("keys"),i=r(3953);e.exports=function(e){return n[e]||(n[e]=i(e))}},3825:(e,t,r)=>{var n=r(5645),i=r(3816),o="__core-js_shared__",a=i[o]||(i[o]={});(e.exports=function(e,t){return a[e]||(a[e]=void 0!==t?t:{})})("versions",[]).push({version:n.version,mode:r(4461)?"pure":"global",copyright:"© 2020 Denis Pushkarev (zloirock.ru)"})},8364:(e,t,r)=>{var n=r(7007),i=r(4963),o=r(6314)("species");e.exports=function(e,t){var r,a=n(e).constructor;return void 0===a||null==(r=n(a)[o])?t:i(r)}},7717:(e,t,r)=>{"use strict";var n=r(4253);e.exports=function(e,t){return!!e&&n((function(){t?e.call(null,(function(){}),1):e.call(null)}))}},4496:(e,t,r)=>{var n=r(1467),i=r(1355);e.exports=function(e){return function(t,r){var o,a,s=String(i(t)),p=n(r),d=s.length;return p<0||p>=d?e?"":void 0:(o=s.charCodeAt(p))<55296||o>56319||p+1===d||(a=s.charCodeAt(p+1))<56320||a>57343?e?s.charAt(p):o:e?s.slice(p,p+2):a-56320+(o-55296<<10)+65536}}},2094:(e,t,r)=>{var n=r(5364),i=r(1355);e.exports=function(e,t,r){if(n(t))throw TypeError("String#"+r+" doesn't accept regex!");return String(i(e))}},9395:(e,t,r)=>{var n=r(2985),i=r(4253),o=r(1355),a=/"/g,s=function(e,t,r,n){var i=String(o(e)),s="<"+t;return""!==r&&(s+=" "+r+'="'+String(n).replace(a,"&quot;")+'"'),s+">"+i+"</"+t+">"};e.exports=function(e,t){var r={};r[e]=t(s),n(n.P+n.F*i((function(){var t=""[e]('"');return t!==t.toLowerCase()||t.split('"').length>3})),"String",r)}},5442:(e,t,r)=>{var n=r(875),i=r(8595),o=r(1355);e.exports=function(e,t,r,a){var s=String(o(e)),p=s.length,d=void 0===r?" ":String(r),c=n(t);if(c<=p||""==d)return s;var l=c-p,u=i.call(d,Math.ceil(l/d.length));return u.length>l&&(u=u.slice(0,l)),a?u+s:s+u}},8595:(e,t,r)=>{"use strict";var n=r(1467),i=r(1355);e.exports=function(e){var t=String(i(this)),r="",o=n(e);if(o<0||o==1/0)throw RangeError("Count can't be negative");for(;o>0;(o>>>=1)&&(t+=t))1&o&&(r+=t);return r}},9599:(e,t,r)=>{var n=r(2985),i=r(1355),o=r(4253),a=r(4644),s="["+a+"]",p=RegExp("^"+s+s+"*"),d=RegExp(s+s+"*$"),c=function(e,t,r){var i={},s=o((function(){return!!a[e]()||"​…"!="​…"[e]()})),p=i[e]=s?t(l):a[e];r&&(i[r]=p),n(n.P+n.F*s,"String",i)},l=c.trim=function(e,t){return e=String(i(e)),1&t&&(e=e.replace(p,"")),2&t&&(e=e.replace(d,"")),e};e.exports=c},4644:e=>{e.exports="\t\n\v\f\r   ᠎              \u2028\u2029\ufeff"},4193:(e,t,r)=>{var n,i,o,a=r(741),s=r(7242),p=r(639),d=r(2457),c=r(3816),l=c.process,u=c.setImmediate,m=c.clearImmediate,h=c.MessageChannel,f=c.Dispatch,y=0,g={},b="onreadystatechange",v=function(){var e=+this;if(g.hasOwnProperty(e)){var t=g[e];delete g[e],t()}},w=function(e){v.call(e.data)};u&&m||(u=function(e){for(var t=[],r=1;arguments.length>r;)t.push(arguments[r++]);return g[++y]=function(){s("function"==typeof e?e:Function(e),t)},n(y),y},m=function(e){delete g[e]},"process"==r(2032)(l)?n=function(e){l.nextTick(a(v,e,1))}:f&&f.now?n=function(e){f.now(a(v,e,1))}:h?(o=(i=new h).port2,i.port1.onmessage=w,n=a(o.postMessage,o,1)):c.addEventListener&&"function"==typeof postMessage&&!c.importScripts?(n=function(e){c.postMessage(e+"","*")},c.addEventListener("message",w,!1)):n=b in d("script")?function(e){p.appendChild(d("script")).onreadystatechange=function(){p.removeChild(this),v.call(e)}}:function(e){setTimeout(a(v,e,1),0)}),e.exports={set:u,clear:m}},2337:(e,t,r)=>{var n=r(1467),i=Math.max,o=Math.min;e.exports=function(e,t){return(e=n(e))<0?i(e+t,0):o(e,t)}},4843:(e,t,r)=>{var n=r(1467),i=r(875);e.exports=function(e){if(void 0===e)return 0;var t=n(e),r=i(t);if(t!==r)throw RangeError("Wrong length!");return r}},1467:e=>{var t=Math.ceil,r=Math.floor;e.exports=function(e){return isNaN(e=+e)?0:(e>0?r:t)(e)}},2110:(e,t,r)=>{var n=r(9797),i=r(1355);e.exports=function(e){return n(i(e))}},875:(e,t,r)=>{var n=r(1467),i=Math.min;e.exports=function(e){return e>0?i(n(e),9007199254740991):0}},508:(e,t,r)=>{var n=r(1355);e.exports=function(e){return Object(n(e))}},1689:(e,t,r)=>{var n=r(5286);e.exports=function(e,t){if(!n(e))return e;var r,i;if(t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;if("function"==typeof(r=e.valueOf)&&!n(i=r.call(e)))return i;if(!t&&"function"==typeof(r=e.toString)&&!n(i=r.call(e)))return i;throw TypeError("Can't convert object to primitive value")}},8440:(e,t,r)=>{"use strict";if(r(7057)){var n=r(4461),i=r(3816),o=r(4253),a=r(2985),s=r(9383),p=r(1125),d=r(741),c=r(3328),l=r(681),u=r(7728),m=r(4408),h=r(1467),f=r(875),y=r(4843),g=r(2337),b=r(1689),v=r(9181),w=r(1488),S=r(5286),I=r(508),x=r(6555),k=r(2503),T=r(468),R=r(616).f,C=r(9002),$=r(3953),A=r(6314),O=r(50),P=r(9315),D=r(8364),N=r(6997),j=r(2803),E=r(7462),M=r(2974),F=r(6852),q=r(5216),L=r(9275),B=r(8693),U=L.f,W=B.f,H=i.RangeError,_=i.TypeError,V=i.Uint8Array,G="ArrayBuffer",z="SharedArrayBuffer",J="BYTES_PER_ELEMENT",K=Array.prototype,X=p.ArrayBuffer,Y=p.DataView,Q=O(0),Z=O(2),ee=O(3),te=O(4),re=O(5),ne=O(6),ie=P(!0),oe=P(!1),ae=N.values,se=N.keys,pe=N.entries,de=K.lastIndexOf,ce=K.reduce,le=K.reduceRight,ue=K.join,me=K.sort,he=K.slice,fe=K.toString,ye=K.toLocaleString,ge=A("iterator"),be=A("toStringTag"),ve=$("typed_constructor"),we=$("def_constructor"),Se=s.CONSTR,Ie=s.TYPED,xe=s.VIEW,ke="Wrong length!",Te=O(1,(function(e,t){return Oe(D(e,e[we]),t)})),Re=o((function(){return 1===new V(new Uint16Array([1]).buffer)[0]})),Ce=!!V&&!!V.prototype.set&&o((function(){new V(1).set({})})),$e=function(e,t){var r=h(e);if(r<0||r%t)throw H("Wrong offset!");return r},Ae=function(e){if(S(e)&&Ie in e)return e;throw _(e+" is not a typed array!")},Oe=function(e,t){if(!S(e)||!(ve in e))throw _("It is not a typed array constructor!");return new e(t)},Pe=function(e,t){return De(D(e,e[we]),t)},De=function(e,t){for(var r=0,n=t.length,i=Oe(e,n);n>r;)i[r]=t[r++];return i},Ne=function(e,t,r){U(e,t,{get:function(){return this._d[r]}})},je=function(e){var t,r,n,i,o,a,s=I(e),p=arguments.length,c=p>1?arguments[1]:void 0,l=void 0!==c,u=C(s);if(null!=u&&!x(u)){for(a=u.call(s),n=[],t=0;!(o=a.next()).done;t++)n.push(o.value);s=n}for(l&&p>2&&(c=d(c,arguments[2],2)),t=0,r=f(s.length),i=Oe(this,r);r>t;t++)i[t]=l?c(s[t],t):s[t];return i},Ee=function(){for(var e=0,t=arguments.length,r=Oe(this,t);t>e;)r[e]=arguments[e++];return r},Me=!!V&&o((function(){ye.call(new V(1))})),Fe=function(){return ye.apply(Me?he.call(Ae(this)):Ae(this),arguments)},qe={copyWithin:function(e,t){return q.call(Ae(this),e,t,arguments.length>2?arguments[2]:void 0)},every:function(e){return te(Ae(this),e,arguments.length>1?arguments[1]:void 0)},fill:function(e){return F.apply(Ae(this),arguments)},filter:function(e){return Pe(this,Z(Ae(this),e,arguments.length>1?arguments[1]:void 0))},find:function(e){return re(Ae(this),e,arguments.length>1?arguments[1]:void 0)},findIndex:function(e){return ne(Ae(this),e,arguments.length>1?arguments[1]:void 0)},forEach:function(e){Q(Ae(this),e,arguments.length>1?arguments[1]:void 0)},indexOf:function(e){return oe(Ae(this),e,arguments.length>1?arguments[1]:void 0)},includes:function(e){return ie(Ae(this),e,arguments.length>1?arguments[1]:void 0)},join:function(e){return ue.apply(Ae(this),arguments)},lastIndexOf:function(e){return de.apply(Ae(this),arguments)},map:function(e){return Te(Ae(this),e,arguments.length>1?arguments[1]:void 0)},reduce:function(e){return ce.apply(Ae(this),arguments)},reduceRight:function(e){return le.apply(Ae(this),arguments)},reverse:function(){for(var e,t=this,r=Ae(t).length,n=Math.floor(r/2),i=0;i<n;)e=t[i],t[i++]=t[--r],t[r]=e;return t},some:function(e){return ee(Ae(this),e,arguments.length>1?arguments[1]:void 0)},sort:function(e){return me.call(Ae(this),e)},subarray:function(e,t){var r=Ae(this),n=r.length,i=g(e,n);return new(D(r,r[we]))(r.buffer,r.byteOffset+i*r.BYTES_PER_ELEMENT,f((void 0===t?n:g(t,n))-i))}},Le=function(e,t){return Pe(this,he.call(Ae(this),e,t))},Be=function(e){Ae(this);var t=$e(arguments[1],1),r=this.length,n=I(e),i=f(n.length),o=0;if(i+t>r)throw H(ke);for(;o<i;)this[t+o]=n[o++]},Ue={entries:function(){return pe.call(Ae(this))},keys:function(){return se.call(Ae(this))},values:function(){return ae.call(Ae(this))}},We=function(e,t){return S(e)&&e[Ie]&&"symbol"!=typeof t&&t in e&&String(+t)==String(t)},He=function(e,t){return We(e,t=b(t,!0))?l(2,e[t]):W(e,t)},_e=function(e,t,r){return!(We(e,t=b(t,!0))&&S(r)&&v(r,"value"))||v(r,"get")||v(r,"set")||r.configurable||v(r,"writable")&&!r.writable||v(r,"enumerable")&&!r.enumerable?U(e,t,r):(e[t]=r.value,e)};Se||(B.f=He,L.f=_e),a(a.S+a.F*!Se,"Object",{getOwnPropertyDescriptor:He,defineProperty:_e}),o((function(){fe.call({})}))&&(fe=ye=function(){return ue.call(this)});var Ve=m({},qe);m(Ve,Ue),u(Ve,ge,Ue.values),m(Ve,{slice:Le,set:Be,constructor:function(){},toString:fe,toLocaleString:Fe}),Ne(Ve,"buffer","b"),Ne(Ve,"byteOffset","o"),Ne(Ve,"byteLength","l"),Ne(Ve,"length","e"),U(Ve,be,{get:function(){return this[Ie]}}),e.exports=function(e,t,r,p){var d=e+((p=!!p)?"Clamped":"")+"Array",l="get"+e,m="set"+e,h=i[d],g=h||{},b=h&&T(h),v=!h||!s.ABV,I={},x=h&&h.prototype,C=function(e,r){U(e,r,{get:function(){return function(e,r){var n=e._d;return n.v[l](r*t+n.o,Re)}(this,r)},set:function(e){return function(e,r,n){var i=e._d;p&&(n=(n=Math.round(n))<0?0:n>255?255:255&n),i.v[m](r*t+i.o,n,Re)}(this,r,e)},enumerable:!0})};v?(h=r((function(e,r,n,i){c(e,h,d,"_d");var o,a,s,p,l=0,m=0;if(S(r)){if(!(r instanceof X||(p=w(r))==G||p==z))return Ie in r?De(h,r):je.call(h,r);o=r,m=$e(n,t);var g=r.byteLength;if(void 0===i){if(g%t)throw H(ke);if((a=g-m)<0)throw H(ke)}else if((a=f(i)*t)+m>g)throw H(ke);s=a/t}else s=y(r),o=new X(a=s*t);for(u(e,"_d",{b:o,o:m,l:a,e:s,v:new Y(o)});l<s;)C(e,l++)})),x=h.prototype=k(Ve),u(x,"constructor",h)):o((function(){h(1)}))&&o((function(){new h(-1)}))&&E((function(e){new h,new h(null),new h(1.5),new h(e)}),!0)||(h=r((function(e,r,n,i){var o;return c(e,h,d),S(r)?r instanceof X||(o=w(r))==G||o==z?void 0!==i?new g(r,$e(n,t),i):void 0!==n?new g(r,$e(n,t)):new g(r):Ie in r?De(h,r):je.call(h,r):new g(y(r))})),Q(b!==Function.prototype?R(g).concat(R(b)):R(g),(function(e){e in h||u(h,e,g[e])})),h.prototype=x,n||(x.constructor=h));var $=x[ge],A=!!$&&("values"==$.name||null==$.name),O=Ue.values;u(h,ve,!0),u(x,Ie,d),u(x,xe,!0),u(x,we,h),(p?new h(1)[be]==d:be in x)||U(x,be,{get:function(){return d}}),I[d]=h,a(a.G+a.W+a.F*(h!=g),I),a(a.S,d,{BYTES_PER_ELEMENT:t}),a(a.S+a.F*o((function(){g.of.call(h,1)})),d,{from:je,of:Ee}),J in x||u(x,J,t),a(a.P,d,qe),M(d),a(a.P+a.F*Ce,d,{set:Be}),a(a.P+a.F*!A,d,Ue),n||x.toString==fe||(x.toString=fe),a(a.P+a.F*o((function(){new h(1).slice()})),d,{slice:Le}),a(a.P+a.F*(o((function(){return[1,2].toLocaleString()!=new h([1,2]).toLocaleString()}))||!o((function(){x.toLocaleString.call([1,2])}))),d,{toLocaleString:Fe}),j[d]=A?$:O,n||A||u(x,ge,O)}}else e.exports=function(){}},1125:(e,t,r)=>{"use strict";var n=r(3816),i=r(7057),o=r(4461),a=r(9383),s=r(7728),p=r(4408),d=r(4253),c=r(3328),l=r(1467),u=r(875),m=r(4843),h=r(616).f,f=r(9275).f,y=r(6852),g=r(2943),b="ArrayBuffer",v="DataView",w="Wrong index!",S=n.ArrayBuffer,I=n.DataView,x=n.Math,k=n.RangeError,T=n.Infinity,R=S,C=x.abs,$=x.pow,A=x.floor,O=x.log,P=x.LN2,D="buffer",N="byteLength",j="byteOffset",E=i?"_b":D,M=i?"_l":N,F=i?"_o":j;function q(e,t,r){var n,i,o,a=new Array(r),s=8*r-t-1,p=(1<<s)-1,d=p>>1,c=23===t?$(2,-24)-$(2,-77):0,l=0,u=e<0||0===e&&1/e<0?1:0;for((e=C(e))!=e||e===T?(i=e!=e?1:0,n=p):(n=A(O(e)/P),e*(o=$(2,-n))<1&&(n--,o*=2),(e+=n+d>=1?c/o:c*$(2,1-d))*o>=2&&(n++,o/=2),n+d>=p?(i=0,n=p):n+d>=1?(i=(e*o-1)*$(2,t),n+=d):(i=e*$(2,d-1)*$(2,t),n=0));t>=8;a[l++]=255&i,i/=256,t-=8);for(n=n<<t|i,s+=t;s>0;a[l++]=255&n,n/=256,s-=8);return a[--l]|=128*u,a}function L(e,t,r){var n,i=8*r-t-1,o=(1<<i)-1,a=o>>1,s=i-7,p=r-1,d=e[p--],c=127&d;for(d>>=7;s>0;c=256*c+e[p],p--,s-=8);for(n=c&(1<<-s)-1,c>>=-s,s+=t;s>0;n=256*n+e[p],p--,s-=8);if(0===c)c=1-a;else{if(c===o)return n?NaN:d?-T:T;n+=$(2,t),c-=a}return(d?-1:1)*n*$(2,c-t)}function B(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]}function U(e){return[255&e]}function W(e){return[255&e,e>>8&255]}function H(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]}function _(e){return q(e,52,8)}function V(e){return q(e,23,4)}function G(e,t,r){f(e.prototype,t,{get:function(){return this[r]}})}function z(e,t,r,n){var i=m(+r);if(i+t>e[M])throw k(w);var o=e[E]._b,a=i+e[F],s=o.slice(a,a+t);return n?s:s.reverse()}function J(e,t,r,n,i,o){var a=m(+r);if(a+t>e[M])throw k(w);for(var s=e[E]._b,p=a+e[F],d=n(+i),c=0;c<t;c++)s[p+c]=d[o?c:t-c-1]}if(a.ABV){if(!d((function(){S(1)}))||!d((function(){new S(-1)}))||d((function(){return new S,new S(1.5),new S(NaN),S.name!=b}))){for(var K,X=(S=function(e){return c(this,S),new R(m(e))}).prototype=R.prototype,Y=h(R),Q=0;Y.length>Q;)(K=Y[Q++])in S||s(S,K,R[K]);o||(X.constructor=S)}var Z=new I(new S(2)),ee=I.prototype.setInt8;Z.setInt8(0,2147483648),Z.setInt8(1,2147483649),!Z.getInt8(0)&&Z.getInt8(1)||p(I.prototype,{setInt8:function(e,t){ee.call(this,e,t<<24>>24)},setUint8:function(e,t){ee.call(this,e,t<<24>>24)}},!0)}else S=function(e){c(this,S,b);var t=m(e);this._b=y.call(new Array(t),0),this[M]=t},I=function(e,t,r){c(this,I,v),c(e,S,v);var n=e[M],i=l(t);if(i<0||i>n)throw k("Wrong offset!");if(i+(r=void 0===r?n-i:u(r))>n)throw k("Wrong length!");this[E]=e,this[F]=i,this[M]=r},i&&(G(S,N,"_l"),G(I,D,"_b"),G(I,N,"_l"),G(I,j,"_o")),p(I.prototype,{getInt8:function(e){return z(this,1,e)[0]<<24>>24},getUint8:function(e){return z(this,1,e)[0]},getInt16:function(e){var t=z(this,2,e,arguments[1]);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=z(this,2,e,arguments[1]);return t[1]<<8|t[0]},getInt32:function(e){return B(z(this,4,e,arguments[1]))},getUint32:function(e){return B(z(this,4,e,arguments[1]))>>>0},getFloat32:function(e){return L(z(this,4,e,arguments[1]),23,4)},getFloat64:function(e){return L(z(this,8,e,arguments[1]),52,8)},setInt8:function(e,t){J(this,1,e,U,t)},setUint8:function(e,t){J(this,1,e,U,t)},setInt16:function(e,t){J(this,2,e,W,t,arguments[2])},setUint16:function(e,t){J(this,2,e,W,t,arguments[2])},setInt32:function(e,t){J(this,4,e,H,t,arguments[2])},setUint32:function(e,t){J(this,4,e,H,t,arguments[2])},setFloat32:function(e,t){J(this,4,e,V,t,arguments[2])},setFloat64:function(e,t){J(this,8,e,_,t,arguments[2])}});g(S,b),g(I,v),s(I.prototype,a.VIEW,!0),t.ArrayBuffer=S,t.DataView=I},9383:(e,t,r)=>{for(var n,i=r(3816),o=r(7728),a=r(3953),s=a("typed_array"),p=a("view"),d=!(!i.ArrayBuffer||!i.DataView),c=d,l=0,u="Int8Array,Uint8Array,Uint8ClampedArray,Int16Array,Uint16Array,Int32Array,Uint32Array,Float32Array,Float64Array".split(",");l<9;)(n=i[u[l++]])?(o(n.prototype,s,!0),o(n.prototype,p,!0)):c=!1;e.exports={ABV:d,CONSTR:c,TYPED:s,VIEW:p}},3953:e=>{var t=0,r=Math.random();e.exports=function(e){return"Symbol(".concat(void 0===e?"":e,")_",(++t+r).toString(36))}},575:(e,t,r)=>{var n=r(3816).navigator;e.exports=n&&n.userAgent||""},1616:(e,t,r)=>{var n=r(5286);e.exports=function(e,t){if(!n(e)||e._t!==t)throw TypeError("Incompatible receiver, "+t+" required!");return e}},6074:(e,t,r)=>{var n=r(3816),i=r(5645),o=r(4461),a=r(8787),s=r(9275).f;e.exports=function(e){var t=i.Symbol||(i.Symbol=o?{}:n.Symbol||{});"_"==e.charAt(0)||e in t||s(t,e,{value:a.f(e)})}},8787:(e,t,r)=>{t.f=r(6314)},6314:(e,t,r)=>{var n=r(3825)("wks"),i=r(3953),o=r(3816).Symbol,a="function"==typeof o;(e.exports=function(e){return n[e]||(n[e]=a&&o[e]||(a?o:i)("Symbol."+e))}).store=n},9002:(e,t,r)=>{var n=r(1488),i=r(6314)("iterator"),o=r(2803);e.exports=r(5645).getIteratorMethod=function(e){if(null!=e)return e[i]||e["@@iterator"]||o[n(e)]}},1761:(e,t,r)=>{var n=r(2985),i=r(5496)(/[\\^$*+?.()|[\]{}]/g,"\\$&");n(n.S,"RegExp",{escape:function(e){return i(e)}})},2e3:(e,t,r)=>{var n=r(2985);n(n.P,"Array",{copyWithin:r(5216)}),r(7722)("copyWithin")},5745:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(4);n(n.P+n.F*!r(7717)([].every,!0),"Array",{every:function(e){return i(this,e,arguments[1])}})},8977:(e,t,r)=>{var n=r(2985);n(n.P,"Array",{fill:r(6852)}),r(7722)("fill")},8837:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(2);n(n.P+n.F*!r(7717)([].filter,!0),"Array",{filter:function(e){return i(this,e,arguments[1])}})},4899:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(6),o="findIndex",a=!0;o in[]&&Array(1)[o]((function(){a=!1})),n(n.P+n.F*a,"Array",{findIndex:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)(o)},2310:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(5),o="find",a=!0;o in[]&&Array(1).find((function(){a=!1})),n(n.P+n.F*a,"Array",{find:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)(o)},4336:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(0),o=r(7717)([].forEach,!0);n(n.P+n.F*!o,"Array",{forEach:function(e){return i(this,e,arguments[1])}})},522:(e,t,r)=>{"use strict";var n=r(741),i=r(2985),o=r(508),a=r(8851),s=r(6555),p=r(875),d=r(2811),c=r(9002);i(i.S+i.F*!r(7462)((function(e){Array.from(e)})),"Array",{from:function(e){var t,r,i,l,u=o(e),m="function"==typeof this?this:Array,h=arguments.length,f=h>1?arguments[1]:void 0,y=void 0!==f,g=0,b=c(u);if(y&&(f=n(f,h>2?arguments[2]:void 0,2)),null==b||m==Array&&s(b))for(r=new m(t=p(u.length));t>g;g++)d(r,g,y?f(u[g],g):u[g]);else for(l=b.call(u),r=new m;!(i=l.next()).done;g++)d(r,g,y?a(l,f,[i.value,g],!0):i.value);return r.length=g,r}})},3369:(e,t,r)=>{"use strict";var n=r(2985),i=r(9315)(!1),o=[].indexOf,a=!!o&&1/[1].indexOf(1,-0)<0;n(n.P+n.F*(a||!r(7717)(o)),"Array",{indexOf:function(e){return a?o.apply(this,arguments)||0:i(this,e,arguments[1])}})},774:(e,t,r)=>{var n=r(2985);n(n.S,"Array",{isArray:r(4302)})},6997:(e,t,r)=>{"use strict";var n=r(7722),i=r(5436),o=r(2803),a=r(2110);e.exports=r(2923)(Array,"Array",(function(e,t){this._t=a(e),this._i=0,this._k=t}),(function(){var e=this._t,t=this._k,r=this._i++;return!e||r>=e.length?(this._t=void 0,i(1)):i(0,"keys"==t?r:"values"==t?e[r]:[r,e[r]])}),"values"),o.Arguments=o.Array,n("keys"),n("values"),n("entries")},7842:(e,t,r)=>{"use strict";var n=r(2985),i=r(2110),o=[].join;n(n.P+n.F*(r(9797)!=Object||!r(7717)(o)),"Array",{join:function(e){return o.call(i(this),void 0===e?",":e)}})},9564:(e,t,r)=>{"use strict";var n=r(2985),i=r(2110),o=r(1467),a=r(875),s=[].lastIndexOf,p=!!s&&1/[1].lastIndexOf(1,-0)<0;n(n.P+n.F*(p||!r(7717)(s)),"Array",{lastIndexOf:function(e){if(p)return s.apply(this,arguments)||0;var t=i(this),r=a(t.length),n=r-1;for(arguments.length>1&&(n=Math.min(n,o(arguments[1]))),n<0&&(n=r+n);n>=0;n--)if(n in t&&t[n]===e)return n||0;return-1}})},1802:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(1);n(n.P+n.F*!r(7717)([].map,!0),"Array",{map:function(e){return i(this,e,arguments[1])}})},8295:(e,t,r)=>{"use strict";var n=r(2985),i=r(2811);n(n.S+n.F*r(4253)((function(){function e(){}return!(Array.of.call(e)instanceof e)})),"Array",{of:function(){for(var e=0,t=arguments.length,r=new("function"==typeof this?this:Array)(t);t>e;)i(r,e,arguments[e++]);return r.length=t,r}})},3750:(e,t,r)=>{"use strict";var n=r(2985),i=r(7628);n(n.P+n.F*!r(7717)([].reduceRight,!0),"Array",{reduceRight:function(e){return i(this,e,arguments.length,arguments[1],!0)}})},3057:(e,t,r)=>{"use strict";var n=r(2985),i=r(7628);n(n.P+n.F*!r(7717)([].reduce,!0),"Array",{reduce:function(e){return i(this,e,arguments.length,arguments[1],!1)}})},110:(e,t,r)=>{"use strict";var n=r(2985),i=r(639),o=r(2032),a=r(2337),s=r(875),p=[].slice;n(n.P+n.F*r(4253)((function(){i&&p.call(i)})),"Array",{slice:function(e,t){var r=s(this.length),n=o(this);if(t=void 0===t?r:t,"Array"==n)return p.call(this,e,t);for(var i=a(e,r),d=a(t,r),c=s(d-i),l=new Array(c),u=0;u<c;u++)l[u]="String"==n?this.charAt(i+u):this[i+u];return l}})},6773:(e,t,r)=>{"use strict";var n=r(2985),i=r(50)(3);n(n.P+n.F*!r(7717)([].some,!0),"Array",{some:function(e){return i(this,e,arguments[1])}})},75:(e,t,r)=>{"use strict";var n=r(2985),i=r(4963),o=r(508),a=r(4253),s=[].sort,p=[1,2,3];n(n.P+n.F*(a((function(){p.sort(void 0)}))||!a((function(){p.sort(null)}))||!r(7717)(s)),"Array",{sort:function(e){return void 0===e?s.call(o(this)):s.call(o(this),i(e))}})},1842:(e,t,r)=>{r(2974)("Array")},1822:(e,t,r)=>{var n=r(2985);n(n.S,"Date",{now:function(){return(new Date).getTime()}})},1031:(e,t,r)=>{var n=r(2985),i=r(3537);n(n.P+n.F*(Date.prototype.toISOString!==i),"Date",{toISOString:i})},9977:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689);n(n.P+n.F*r(4253)((function(){return null!==new Date(NaN).toJSON()||1!==Date.prototype.toJSON.call({toISOString:function(){return 1}})})),"Date",{toJSON:function(e){var t=i(this),r=o(t);return"number"!=typeof r||isFinite(r)?t.toISOString():null}})},1560:(e,t,r)=>{var n=r(6314)("toPrimitive"),i=Date.prototype;n in i||r(7728)(i,n,r(870))},6331:(e,t,r)=>{var n=Date.prototype,i="Invalid Date",o="toString",a=n.toString,s=n.getTime;new Date(NaN)+""!=i&&r(7234)(n,o,(function(){var e=s.call(this);return e==e?a.call(this):i}))},9730:(e,t,r)=>{var n=r(2985);n(n.P,"Function",{bind:r(4398)})},8377:(e,t,r)=>{"use strict";var n=r(5286),i=r(468),o=r(6314)("hasInstance"),a=Function.prototype;o in a||r(9275).f(a,o,{value:function(e){if("function"!=typeof this||!n(e))return!1;if(!n(this.prototype))return e instanceof this;for(;e=i(e);)if(this.prototype===e)return!0;return!1}})},6059:(e,t,r)=>{var n=r(9275).f,i=Function.prototype,o=/^\s*function ([^ (]*)/,a="name";a in i||r(7057)&&n(i,a,{configurable:!0,get:function(){try{return(""+this).match(o)[1]}catch(e){return""}}})},8416:(e,t,r)=>{"use strict";var n=r(9824),i=r(1616),o="Map";e.exports=r(5795)(o,(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{get:function(e){var t=n.getEntry(i(this,o),e);return t&&t.v},set:function(e,t){return n.def(i(this,o),0===e?0:e,t)}},n,!0)},6503:(e,t,r)=>{var n=r(2985),i=r(6206),o=Math.sqrt,a=Math.acosh;n(n.S+n.F*!(a&&710==Math.floor(a(Number.MAX_VALUE))&&a(1/0)==1/0),"Math",{acosh:function(e){return(e=+e)<1?NaN:e>94906265.62425156?Math.log(e)+Math.LN2:i(e-1+o(e-1)*o(e+1))}})},6786:(e,t,r)=>{var n=r(2985),i=Math.asinh;n(n.S+n.F*!(i&&1/i(0)>0),"Math",{asinh:function e(t){return isFinite(t=+t)&&0!=t?t<0?-e(-t):Math.log(t+Math.sqrt(t*t+1)):t}})},932:(e,t,r)=>{var n=r(2985),i=Math.atanh;n(n.S+n.F*!(i&&1/i(-0)<0),"Math",{atanh:function(e){return 0==(e=+e)?e:Math.log((1+e)/(1-e))/2}})},7526:(e,t,r)=>{var n=r(2985),i=r(1801);n(n.S,"Math",{cbrt:function(e){return i(e=+e)*Math.pow(Math.abs(e),1/3)}})},1591:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{clz32:function(e){return(e>>>=0)?31-Math.floor(Math.log(e+.5)*Math.LOG2E):32}})},9073:(e,t,r)=>{var n=r(2985),i=Math.exp;n(n.S,"Math",{cosh:function(e){return(i(e=+e)+i(-e))/2}})},347:(e,t,r)=>{var n=r(2985),i=r(3086);n(n.S+n.F*(i!=Math.expm1),"Math",{expm1:i})},579:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{fround:r(4934)})},4669:(e,t,r)=>{var n=r(2985),i=Math.abs;n(n.S,"Math",{hypot:function(e,t){for(var r,n,o=0,a=0,s=arguments.length,p=0;a<s;)p<(r=i(arguments[a++]))?(o=o*(n=p/r)*n+1,p=r):o+=r>0?(n=r/p)*n:r;return p===1/0?1/0:p*Math.sqrt(o)}})},7710:(e,t,r)=>{var n=r(2985),i=Math.imul;n(n.S+n.F*r(4253)((function(){return-5!=i(4294967295,5)||2!=i.length})),"Math",{imul:function(e,t){var r=65535,n=+e,i=+t,o=r&n,a=r&i;return 0|o*a+((r&n>>>16)*a+o*(r&i>>>16)<<16>>>0)}})},5789:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log10:function(e){return Math.log(e)*Math.LOG10E}})},3514:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log1p:r(6206)})},9978:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{log2:function(e){return Math.log(e)/Math.LN2}})},8472:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{sign:r(1801)})},6946:(e,t,r)=>{var n=r(2985),i=r(3086),o=Math.exp;n(n.S+n.F*r(4253)((function(){return-2e-17!=!Math.sinh(-2e-17)})),"Math",{sinh:function(e){return Math.abs(e=+e)<1?(i(e)-i(-e))/2:(o(e-1)-o(-e-1))*(Math.E/2)}})},5068:(e,t,r)=>{var n=r(2985),i=r(3086),o=Math.exp;n(n.S,"Math",{tanh:function(e){var t=i(e=+e),r=i(-e);return t==1/0?1:r==1/0?-1:(t-r)/(o(e)+o(-e))}})},413:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{trunc:function(e){return(e>0?Math.floor:Math.ceil)(e)}})},1246:(e,t,r)=>{"use strict";var n=r(3816),i=r(9181),o=r(2032),a=r(266),s=r(1689),p=r(4253),d=r(616).f,c=r(8693).f,l=r(9275).f,u=r(9599).trim,m="Number",h=n.Number,f=h,y=h.prototype,g=o(r(2503)(y))==m,b="trim"in String.prototype,v=function(e){var t=s(e,!1);if("string"==typeof t&&t.length>2){var r,n,i,o=(t=b?t.trim():u(t,3)).charCodeAt(0);if(43===o||45===o){if(88===(r=t.charCodeAt(2))||120===r)return NaN}else if(48===o){switch(t.charCodeAt(1)){case 66:case 98:n=2,i=49;break;case 79:case 111:n=8,i=55;break;default:return+t}for(var a,p=t.slice(2),d=0,c=p.length;d<c;d++)if((a=p.charCodeAt(d))<48||a>i)return NaN;return parseInt(p,n)}}return+t};if(!h(" 0o1")||!h("0b1")||h("+0x1")){h=function(e){var t=arguments.length<1?0:e,r=this;return r instanceof h&&(g?p((function(){y.valueOf.call(r)})):o(r)!=m)?a(new f(v(t)),r,h):v(t)};for(var w,S=r(7057)?d(f):"MAX_VALUE,MIN_VALUE,NaN,NEGATIVE_INFINITY,POSITIVE_INFINITY,EPSILON,isFinite,isInteger,isNaN,isSafeInteger,MAX_SAFE_INTEGER,MIN_SAFE_INTEGER,parseFloat,parseInt,isInteger".split(","),I=0;S.length>I;I++)i(f,w=S[I])&&!i(h,w)&&l(h,w,c(f,w));h.prototype=y,y.constructor=h,r(7234)(n,m,h)}},5972:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{EPSILON:Math.pow(2,-52)})},3403:(e,t,r)=>{var n=r(2985),i=r(3816).isFinite;n(n.S,"Number",{isFinite:function(e){return"number"==typeof e&&i(e)}})},2516:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{isInteger:r(8367)})},9371:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{isNaN:function(e){return e!=e}})},6479:(e,t,r)=>{var n=r(2985),i=r(8367),o=Math.abs;n(n.S,"Number",{isSafeInteger:function(e){return i(e)&&o(e)<=9007199254740991}})},1736:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{MAX_SAFE_INTEGER:9007199254740991})},1889:(e,t,r)=>{var n=r(2985);n(n.S,"Number",{MIN_SAFE_INTEGER:-9007199254740991})},5177:(e,t,r)=>{var n=r(2985),i=r(7743);n(n.S+n.F*(Number.parseFloat!=i),"Number",{parseFloat:i})},6943:(e,t,r)=>{var n=r(2985),i=r(5960);n(n.S+n.F*(Number.parseInt!=i),"Number",{parseInt:i})},726:(e,t,r)=>{"use strict";var n=r(2985),i=r(1467),o=r(3365),a=r(8595),s=1..toFixed,p=Math.floor,d=[0,0,0,0,0,0],c="Number.toFixed: incorrect invocation!",l="0",u=function(e,t){for(var r=-1,n=t;++r<6;)n+=e*d[r],d[r]=n%1e7,n=p(n/1e7)},m=function(e){for(var t=6,r=0;--t>=0;)r+=d[t],d[t]=p(r/e),r=r%e*1e7},h=function(){for(var e=6,t="";--e>=0;)if(""!==t||0===e||0!==d[e]){var r=String(d[e]);t=""===t?r:t+a.call(l,7-r.length)+r}return t},f=function(e,t,r){return 0===t?r:t%2==1?f(e,t-1,r*e):f(e*e,t/2,r)};n(n.P+n.F*(!!s&&("0.000"!==8e-5.toFixed(3)||"1"!==.9.toFixed(0)||"1.25"!==1.255.toFixed(2)||"1000000000000000128"!==(0xde0b6b3a7640080).toFixed(0))||!r(4253)((function(){s.call({})}))),"Number",{toFixed:function(e){var t,r,n,s,p=o(this,c),d=i(e),y="",g=l;if(d<0||d>20)throw RangeError(c);if(p!=p)return"NaN";if(p<=-1e21||p>=1e21)return String(p);if(p<0&&(y="-",p=-p),p>1e-21)if(t=function(e){for(var t=0,r=e;r>=4096;)t+=12,r/=4096;for(;r>=2;)t+=1,r/=2;return t}(p*f(2,69,1))-69,r=t<0?p*f(2,-t,1):p/f(2,t,1),r*=4503599627370496,(t=52-t)>0){for(u(0,r),n=d;n>=7;)u(1e7,0),n-=7;for(u(f(10,n,1),0),n=t-1;n>=23;)m(1<<23),n-=23;m(1<<n),u(1,1),m(2),g=h()}else u(0,r),u(1<<-t,0),g=h()+a.call(l,d);return g=d>0?y+((s=g.length)<=d?"0."+a.call(l,d-s)+g:g.slice(0,s-d)+"."+g.slice(s-d)):y+g}})},1901:(e,t,r)=>{"use strict";var n=r(2985),i=r(4253),o=r(3365),a=1..toPrecision;n(n.P+n.F*(i((function(){return"1"!==a.call(1,void 0)}))||!i((function(){a.call({})}))),"Number",{toPrecision:function(e){var t=o(this,"Number#toPrecision: incorrect invocation!");return void 0===e?a.call(t):a.call(t,e)}})},5115:(e,t,r)=>{var n=r(2985);n(n.S+n.F,"Object",{assign:r(5345)})},8132:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{create:r(2503)})},7470:(e,t,r)=>{var n=r(2985);n(n.S+n.F*!r(7057),"Object",{defineProperties:r(5588)})},8388:(e,t,r)=>{var n=r(2985);n(n.S+n.F*!r(7057),"Object",{defineProperty:r(9275).f})},9375:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("freeze",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},4882:(e,t,r)=>{var n=r(2110),i=r(8693).f;r(3160)("getOwnPropertyDescriptor",(function(){return function(e,t){return i(n(e),t)}}))},9622:(e,t,r)=>{r(3160)("getOwnPropertyNames",(function(){return r(9327).f}))},1520:(e,t,r)=>{var n=r(508),i=r(468);r(3160)("getPrototypeOf",(function(){return function(e){return i(n(e))}}))},9892:(e,t,r)=>{var n=r(5286);r(3160)("isExtensible",(function(e){return function(t){return!!n(t)&&(!e||e(t))}}))},4157:(e,t,r)=>{var n=r(5286);r(3160)("isFrozen",(function(e){return function(t){return!n(t)||!!e&&e(t)}}))},5095:(e,t,r)=>{var n=r(5286);r(3160)("isSealed",(function(e){return function(t){return!n(t)||!!e&&e(t)}}))},9176:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{is:r(7195)})},7476:(e,t,r)=>{var n=r(508),i=r(7184);r(3160)("keys",(function(){return function(e){return i(n(e))}}))},4672:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("preventExtensions",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},3533:(e,t,r)=>{var n=r(5286),i=r(4728).onFreeze;r(3160)("seal",(function(e){return function(t){return e&&n(t)?e(i(t)):t}}))},8838:(e,t,r)=>{var n=r(2985);n(n.S,"Object",{setPrototypeOf:r(7375).set})},6253:(e,t,r)=>{"use strict";var n=r(1488),i={};i[r(6314)("toStringTag")]="z",i+""!="[object z]"&&r(7234)(Object.prototype,"toString",(function(){return"[object "+n(this)+"]"}),!0)},4299:(e,t,r)=>{var n=r(2985),i=r(7743);n(n.G+n.F*(parseFloat!=i),{parseFloat:i})},1084:(e,t,r)=>{var n=r(2985),i=r(5960);n(n.G+n.F*(parseInt!=i),{parseInt:i})},851:(e,t,r)=>{"use strict";var n,i,o,a,s=r(4461),p=r(3816),d=r(741),c=r(1488),l=r(2985),u=r(5286),m=r(4963),h=r(3328),f=r(3531),y=r(8364),g=r(4193).set,b=r(4351)(),v=r(3499),w=r(188),S=r(575),I=r(94),x="Promise",k=p.TypeError,T=p.process,R=T&&T.versions,C=R&&R.v8||"",$=p.Promise,A="process"==c(T),O=function(){},P=i=v.f,D=!!function(){try{var e=$.resolve(1),t=(e.constructor={})[r(6314)("species")]=function(e){e(O,O)};return(A||"function"==typeof PromiseRejectionEvent)&&e.then(O)instanceof t&&0!==C.indexOf("6.6")&&-1===S.indexOf("Chrome/66")}catch(e){}}(),N=function(e){var t;return!(!u(e)||"function"!=typeof(t=e.then))&&t},j=function(e,t){if(!e._n){e._n=!0;var r=e._c;b((function(){for(var n=e._v,i=1==e._s,o=0,a=function(t){var r,o,a,s=i?t.ok:t.fail,p=t.resolve,d=t.reject,c=t.domain;try{s?(i||(2==e._h&&F(e),e._h=1),!0===s?r=n:(c&&c.enter(),r=s(n),c&&(c.exit(),a=!0)),r===t.promise?d(k("Promise-chain cycle")):(o=N(r))?o.call(r,p,d):p(r)):d(n)}catch(e){c&&!a&&c.exit(),d(e)}};r.length>o;)a(r[o++]);e._c=[],e._n=!1,t&&!e._h&&E(e)}))}},E=function(e){g.call(p,(function(){var t,r,n,i=e._v,o=M(e);if(o&&(t=w((function(){A?T.emit("unhandledRejection",i,e):(r=p.onunhandledrejection)?r({promise:e,reason:i}):(n=p.console)&&n.error&&n.error("Unhandled promise rejection",i)})),e._h=A||M(e)?2:1),e._a=void 0,o&&t.e)throw t.v}))},M=function(e){return 1!==e._h&&0===(e._a||e._c).length},F=function(e){g.call(p,(function(){var t;A?T.emit("rejectionHandled",e):(t=p.onrejectionhandled)&&t({promise:e,reason:e._v})}))},q=function(e){var t=this;t._d||(t._d=!0,(t=t._w||t)._v=e,t._s=2,t._a||(t._a=t._c.slice()),j(t,!0))},L=function(e){var t,r=this;if(!r._d){r._d=!0,r=r._w||r;try{if(r===e)throw k("Promise can't be resolved itself");(t=N(e))?b((function(){var n={_w:r,_d:!1};try{t.call(e,d(L,n,1),d(q,n,1))}catch(e){q.call(n,e)}})):(r._v=e,r._s=1,j(r,!1))}catch(e){q.call({_w:r,_d:!1},e)}}};D||($=function(e){h(this,$,x,"_h"),m(e),n.call(this);try{e(d(L,this,1),d(q,this,1))}catch(e){q.call(this,e)}},(n=function(e){this._c=[],this._a=void 0,this._s=0,this._d=!1,this._v=void 0,this._h=0,this._n=!1}).prototype=r(4408)($.prototype,{then:function(e,t){var r=P(y(this,$));return r.ok="function"!=typeof e||e,r.fail="function"==typeof t&&t,r.domain=A?T.domain:void 0,this._c.push(r),this._a&&this._a.push(r),this._s&&j(this,!1),r.promise},catch:function(e){return this.then(void 0,e)}}),o=function(){var e=new n;this.promise=e,this.resolve=d(L,e,1),this.reject=d(q,e,1)},v.f=P=function(e){return e===$||e===a?new o(e):i(e)}),l(l.G+l.W+l.F*!D,{Promise:$}),r(2943)($,x),r(2974)(x),a=r(5645).Promise,l(l.S+l.F*!D,x,{reject:function(e){var t=P(this);return(0,t.reject)(e),t.promise}}),l(l.S+l.F*(s||!D),x,{resolve:function(e){return I(s&&this===a?$:this,e)}}),l(l.S+l.F*!(D&&r(7462)((function(e){$.all(e).catch(O)}))),x,{all:function(e){var t=this,r=P(t),n=r.resolve,i=r.reject,o=w((function(){var r=[],o=0,a=1;f(e,!1,(function(e){var s=o++,p=!1;r.push(void 0),a++,t.resolve(e).then((function(e){p||(p=!0,r[s]=e,--a||n(r))}),i)})),--a||n(r)}));return o.e&&i(o.v),r.promise},race:function(e){var t=this,r=P(t),n=r.reject,i=w((function(){f(e,!1,(function(e){t.resolve(e).then(r.resolve,n)}))}));return i.e&&n(i.v),r.promise}})},1572:(e,t,r)=>{var n=r(2985),i=r(4963),o=r(7007),a=(r(3816).Reflect||{}).apply,s=Function.apply;n(n.S+n.F*!r(4253)((function(){a((function(){}))})),"Reflect",{apply:function(e,t,r){var n=i(e),p=o(r);return a?a(n,t,p):s.call(n,t,p)}})},2139:(e,t,r)=>{var n=r(2985),i=r(2503),o=r(4963),a=r(7007),s=r(5286),p=r(4253),d=r(4398),c=(r(3816).Reflect||{}).construct,l=p((function(){function e(){}return!(c((function(){}),[],e)instanceof e)})),u=!p((function(){c((function(){}))}));n(n.S+n.F*(l||u),"Reflect",{construct:function(e,t){o(e),a(t);var r=arguments.length<3?e:o(arguments[2]);if(u&&!l)return c(e,t,r);if(e==r){switch(t.length){case 0:return new e;case 1:return new e(t[0]);case 2:return new e(t[0],t[1]);case 3:return new e(t[0],t[1],t[2]);case 4:return new e(t[0],t[1],t[2],t[3])}var n=[null];return n.push.apply(n,t),new(d.apply(e,n))}var p=r.prototype,m=i(s(p)?p:Object.prototype),h=Function.apply.call(e,m,t);return s(h)?h:m}})},685:(e,t,r)=>{var n=r(9275),i=r(2985),o=r(7007),a=r(1689);i(i.S+i.F*r(4253)((function(){Reflect.defineProperty(n.f({},1,{value:1}),1,{value:2})})),"Reflect",{defineProperty:function(e,t,r){o(e),t=a(t,!0),o(r);try{return n.f(e,t,r),!0}catch(e){return!1}}})},5535:(e,t,r)=>{var n=r(2985),i=r(8693).f,o=r(7007);n(n.S,"Reflect",{deleteProperty:function(e,t){var r=i(o(e),t);return!(r&&!r.configurable)&&delete e[t]}})},7347:(e,t,r)=>{"use strict";var n=r(2985),i=r(7007),o=function(e){this._t=i(e),this._i=0;var t,r=this._k=[];for(t in e)r.push(t)};r(9988)(o,"Object",(function(){var e,t=this,r=t._k;do{if(t._i>=r.length)return{value:void 0,done:!0}}while(!((e=r[t._i++])in t._t));return{value:e,done:!1}})),n(n.S,"Reflect",{enumerate:function(e){return new o(e)}})},6633:(e,t,r)=>{var n=r(8693),i=r(2985),o=r(7007);i(i.S,"Reflect",{getOwnPropertyDescriptor:function(e,t){return n.f(o(e),t)}})},8989:(e,t,r)=>{var n=r(2985),i=r(468),o=r(7007);n(n.S,"Reflect",{getPrototypeOf:function(e){return i(o(e))}})},3049:(e,t,r)=>{var n=r(8693),i=r(468),o=r(9181),a=r(2985),s=r(5286),p=r(7007);a(a.S,"Reflect",{get:function e(t,r){var a,d,c=arguments.length<3?t:arguments[2];return p(t)===c?t[r]:(a=n.f(t,r))?o(a,"value")?a.value:void 0!==a.get?a.get.call(c):void 0:s(d=i(t))?e(d,r,c):void 0}})},8270:(e,t,r)=>{var n=r(2985);n(n.S,"Reflect",{has:function(e,t){return t in e}})},4510:(e,t,r)=>{var n=r(2985),i=r(7007),o=Object.isExtensible;n(n.S,"Reflect",{isExtensible:function(e){return i(e),!o||o(e)}})},3984:(e,t,r)=>{var n=r(2985);n(n.S,"Reflect",{ownKeys:r(7643)})},5769:(e,t,r)=>{var n=r(2985),i=r(7007),o=Object.preventExtensions;n(n.S,"Reflect",{preventExtensions:function(e){i(e);try{return o&&o(e),!0}catch(e){return!1}}})},6014:(e,t,r)=>{var n=r(2985),i=r(7375);i&&n(n.S,"Reflect",{setPrototypeOf:function(e,t){i.check(e,t);try{return i.set(e,t),!0}catch(e){return!1}}})},55:(e,t,r)=>{var n=r(9275),i=r(8693),o=r(468),a=r(9181),s=r(2985),p=r(681),d=r(7007),c=r(5286);s(s.S,"Reflect",{set:function e(t,r,s){var l,u,m=arguments.length<4?t:arguments[3],h=i.f(d(t),r);if(!h){if(c(u=o(t)))return e(u,r,s,m);h=p(0)}if(a(h,"value")){if(!1===h.writable||!c(m))return!1;if(l=i.f(m,r)){if(l.get||l.set||!1===l.writable)return!1;l.value=s,n.f(m,r,l)}else n.f(m,r,p(0,s));return!0}return void 0!==h.set&&(h.set.call(m,s),!0)}})},3946:(e,t,r)=>{var n=r(3816),i=r(266),o=r(9275).f,a=r(616).f,s=r(5364),p=r(3218),d=n.RegExp,c=d,l=d.prototype,u=/a/g,m=/a/g,h=new d(u)!==u;if(r(7057)&&(!h||r(4253)((function(){return m[r(6314)("match")]=!1,d(u)!=u||d(m)==m||"/a/i"!=d(u,"i")})))){d=function(e,t){var r=this instanceof d,n=s(e),o=void 0===t;return!r&&n&&e.constructor===d&&o?e:i(h?new c(n&&!o?e.source:e,t):c((n=e instanceof d)?e.source:e,n&&o?p.call(e):t),r?this:l,d)};for(var f=function(e){e in d||o(d,e,{configurable:!0,get:function(){return c[e]},set:function(t){c[e]=t}})},y=a(c),g=0;y.length>g;)f(y[g++]);l.constructor=d,d.prototype=l,r(7234)(n,"RegExp",d)}r(2974)("RegExp")},8269:(e,t,r)=>{"use strict";var n=r(1165);r(2985)({target:"RegExp",proto:!0,forced:n!==/./.exec},{exec:n})},6774:(e,t,r)=>{r(7057)&&"g"!=/./g.flags&&r(9275).f(RegExp.prototype,"flags",{configurable:!0,get:r(3218)})},1466:(e,t,r)=>{"use strict";var n=r(7007),i=r(875),o=r(6793),a=r(7787);r(8082)("match",1,(function(e,t,r,s){return[function(r){var n=e(this),i=null==r?void 0:r[t];return void 0!==i?i.call(r,n):new RegExp(r)[t](String(n))},function(e){var t=s(r,e,this);if(t.done)return t.value;var p=n(e),d=String(this);if(!p.global)return a(p,d);var c=p.unicode;p.lastIndex=0;for(var l,u=[],m=0;null!==(l=a(p,d));){var h=String(l[0]);u[m]=h,""===h&&(p.lastIndex=o(d,i(p.lastIndex),c)),m++}return 0===m?null:u}]}))},9357:(e,t,r)=>{"use strict";var n=r(7007),i=r(508),o=r(875),a=r(1467),s=r(6793),p=r(7787),d=Math.max,c=Math.min,l=Math.floor,u=/\$([$&`']|\d\d?|<[^>]*>)/g,m=/\$([$&`']|\d\d?)/g;r(8082)("replace",2,(function(e,t,r,h){return[function(n,i){var o=e(this),a=null==n?void 0:n[t];return void 0!==a?a.call(n,o,i):r.call(String(o),n,i)},function(e,t){var i=h(r,e,this,t);if(i.done)return i.value;var l=n(e),u=String(this),m="function"==typeof t;m||(t=String(t));var y=l.global;if(y){var g=l.unicode;l.lastIndex=0}for(var b=[];;){var v=p(l,u);if(null===v)break;if(b.push(v),!y)break;""===String(v[0])&&(l.lastIndex=s(u,o(l.lastIndex),g))}for(var w,S="",I=0,x=0;x<b.length;x++){v=b[x];for(var k=String(v[0]),T=d(c(a(v.index),u.length),0),R=[],C=1;C<v.length;C++)R.push(void 0===(w=v[C])?w:String(w));var $=v.groups;if(m){var A=[k].concat(R,T,u);void 0!==$&&A.push($);var O=String(t.apply(void 0,A))}else O=f(k,u,T,R,$,t);T>=I&&(S+=u.slice(I,T)+O,I=T+k.length)}return S+u.slice(I)}];function f(e,t,n,o,a,s){var p=n+e.length,d=o.length,c=m;return void 0!==a&&(a=i(a),c=u),r.call(s,c,(function(r,i){var s;switch(i.charAt(0)){case"$":return"$";case"&":return e;case"`":return t.slice(0,n);case"'":return t.slice(p);case"<":s=a[i.slice(1,-1)];break;default:var c=+i;if(0===c)return r;if(c>d){var u=l(c/10);return 0===u?r:u<=d?void 0===o[u-1]?i.charAt(1):o[u-1]+i.charAt(1):r}s=o[c-1]}return void 0===s?"":s}))}}))},6142:(e,t,r)=>{"use strict";var n=r(7007),i=r(7195),o=r(7787);r(8082)("search",1,(function(e,t,r,a){return[function(r){var n=e(this),i=null==r?void 0:r[t];return void 0!==i?i.call(r,n):new RegExp(r)[t](String(n))},function(e){var t=a(r,e,this);if(t.done)return t.value;var s=n(e),p=String(this),d=s.lastIndex;i(d,0)||(s.lastIndex=0);var c=o(s,p);return i(s.lastIndex,d)||(s.lastIndex=d),null===c?-1:c.index}]}))},1876:(e,t,r)=>{"use strict";var n=r(5364),i=r(7007),o=r(8364),a=r(6793),s=r(875),p=r(7787),d=r(1165),c=r(4253),l=Math.min,u=[].push,m=4294967295,h=!c((function(){RegExp(m,"y")}));r(8082)("split",2,(function(e,t,r,c){var f;return f="c"=="abbc".split(/(b)*/)[1]||4!="test".split(/(?:)/,-1).length||2!="ab".split(/(?:ab)*/).length||4!=".".split(/(.?)(.?)/).length||".".split(/()()/).length>1||"".split(/.?/).length?function(e,t){var i=String(this);if(void 0===e&&0===t)return[];if(!n(e))return r.call(i,e,t);for(var o,a,s,p=[],c=(e.ignoreCase?"i":"")+(e.multiline?"m":"")+(e.unicode?"u":"")+(e.sticky?"y":""),l=0,h=void 0===t?m:t>>>0,f=new RegExp(e.source,c+"g");(o=d.call(f,i))&&!((a=f.lastIndex)>l&&(p.push(i.slice(l,o.index)),o.length>1&&o.index<i.length&&u.apply(p,o.slice(1)),s=o[0].length,l=a,p.length>=h));)f.lastIndex===o.index&&f.lastIndex++;return l===i.length?!s&&f.test("")||p.push(""):p.push(i.slice(l)),p.length>h?p.slice(0,h):p}:"0".split(void 0,0).length?function(e,t){return void 0===e&&0===t?[]:r.call(this,e,t)}:r,[function(r,n){var i=e(this),o=null==r?void 0:r[t];return void 0!==o?o.call(r,i,n):f.call(String(i),r,n)},function(e,t){var n=c(f,e,this,t,f!==r);if(n.done)return n.value;var d=i(e),u=String(this),y=o(d,RegExp),g=d.unicode,b=(d.ignoreCase?"i":"")+(d.multiline?"m":"")+(d.unicode?"u":"")+(h?"y":"g"),v=new y(h?d:"^(?:"+d.source+")",b),w=void 0===t?m:t>>>0;if(0===w)return[];if(0===u.length)return null===p(v,u)?[u]:[];for(var S=0,I=0,x=[];I<u.length;){v.lastIndex=h?I:0;var k,T=p(v,h?u:u.slice(I));if(null===T||(k=l(s(v.lastIndex+(h?0:I)),u.length))===S)I=a(u,I,g);else{if(x.push(u.slice(S,I)),x.length===w)return x;for(var R=1;R<=T.length-1;R++)if(x.push(T[R]),x.length===w)return x;I=S=k}}return x.push(u.slice(S)),x}]}))},6108:(e,t,r)=>{"use strict";r(6774);var n=r(7007),i=r(3218),o=r(7057),a="toString",s=/./.toString,p=function(e){r(7234)(RegExp.prototype,a,e,!0)};r(4253)((function(){return"/a/b"!=s.call({source:"a",flags:"b"})}))?p((function(){var e=n(this);return"/".concat(e.source,"/","flags"in e?e.flags:!o&&e instanceof RegExp?i.call(e):void 0)})):s.name!=a&&p((function(){return s.call(this)}))},8184:(e,t,r)=>{"use strict";var n=r(9824),i=r(1616);e.exports=r(5795)("Set",(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{add:function(e){return n.def(i(this,"Set"),e=0===e?0:e,e)}},n)},856:(e,t,r)=>{"use strict";r(9395)("anchor",(function(e){return function(t){return e(this,"a","name",t)}}))},703:(e,t,r)=>{"use strict";r(9395)("big",(function(e){return function(){return e(this,"big","","")}}))},1539:(e,t,r)=>{"use strict";r(9395)("blink",(function(e){return function(){return e(this,"blink","","")}}))},5292:(e,t,r)=>{"use strict";r(9395)("bold",(function(e){return function(){return e(this,"b","","")}}))},9539:(e,t,r)=>{"use strict";var n=r(2985),i=r(4496)(!1);n(n.P,"String",{codePointAt:function(e){return i(this,e)}})},6620:(e,t,r)=>{"use strict";var n=r(2985),i=r(875),o=r(2094),a="endsWith",s="".endsWith;n(n.P+n.F*r(8852)(a),"String",{endsWith:function(e){var t=o(this,e,a),r=arguments.length>1?arguments[1]:void 0,n=i(t.length),p=void 0===r?n:Math.min(i(r),n),d=String(e);return s?s.call(t,d,p):t.slice(p-d.length,p)===d}})},6629:(e,t,r)=>{"use strict";r(9395)("fixed",(function(e){return function(){return e(this,"tt","","")}}))},3694:(e,t,r)=>{"use strict";r(9395)("fontcolor",(function(e){return function(t){return e(this,"font","color",t)}}))},7648:(e,t,r)=>{"use strict";r(9395)("fontsize",(function(e){return function(t){return e(this,"font","size",t)}}))},191:(e,t,r)=>{var n=r(2985),i=r(2337),o=String.fromCharCode,a=String.fromCodePoint;n(n.S+n.F*(!!a&&1!=a.length),"String",{fromCodePoint:function(e){for(var t,r=[],n=arguments.length,a=0;n>a;){if(t=+arguments[a++],i(t,1114111)!==t)throw RangeError(t+" is not a valid code point");r.push(t<65536?o(t):o(55296+((t-=65536)>>10),t%1024+56320))}return r.join("")}})},2850:(e,t,r)=>{"use strict";var n=r(2985),i=r(2094),o="includes";n(n.P+n.F*r(8852)(o),"String",{includes:function(e){return!!~i(this,e,o).indexOf(e,arguments.length>1?arguments[1]:void 0)}})},7795:(e,t,r)=>{"use strict";r(9395)("italics",(function(e){return function(){return e(this,"i","","")}}))},9115:(e,t,r)=>{"use strict";var n=r(4496)(!0);r(2923)(String,"String",(function(e){this._t=String(e),this._i=0}),(function(){var e,t=this._t,r=this._i;return r>=t.length?{value:void 0,done:!0}:(e=n(t,r),this._i+=e.length,{value:e,done:!1})}))},4531:(e,t,r)=>{"use strict";r(9395)("link",(function(e){return function(t){return e(this,"a","href",t)}}))},8306:(e,t,r)=>{var n=r(2985),i=r(2110),o=r(875);n(n.S,"String",{raw:function(e){for(var t=i(e.raw),r=o(t.length),n=arguments.length,a=[],s=0;r>s;)a.push(String(t[s++])),s<n&&a.push(String(arguments[s]));return a.join("")}})},823:(e,t,r)=>{var n=r(2985);n(n.P,"String",{repeat:r(8595)})},3605:(e,t,r)=>{"use strict";r(9395)("small",(function(e){return function(){return e(this,"small","","")}}))},7732:(e,t,r)=>{"use strict";var n=r(2985),i=r(875),o=r(2094),a="startsWith",s="".startsWith;n(n.P+n.F*r(8852)(a),"String",{startsWith:function(e){var t=o(this,e,a),r=i(Math.min(arguments.length>1?arguments[1]:void 0,t.length)),n=String(e);return s?s.call(t,n,r):t.slice(r,r+n.length)===n}})},6780:(e,t,r)=>{"use strict";r(9395)("strike",(function(e){return function(){return e(this,"strike","","")}}))},9937:(e,t,r)=>{"use strict";r(9395)("sub",(function(e){return function(){return e(this,"sub","","")}}))},511:(e,t,r)=>{"use strict";r(9395)("sup",(function(e){return function(){return e(this,"sup","","")}}))},4564:(e,t,r)=>{"use strict";r(9599)("trim",(function(e){return function(){return e(this,3)}}))},5767:(e,t,r)=>{"use strict";var n=r(3816),i=r(9181),o=r(7057),a=r(2985),s=r(7234),p=r(4728).KEY,d=r(4253),c=r(3825),l=r(2943),u=r(3953),m=r(6314),h=r(8787),f=r(6074),y=r(5541),g=r(4302),b=r(7007),v=r(5286),w=r(508),S=r(2110),I=r(1689),x=r(681),k=r(2503),T=r(9327),R=r(8693),C=r(4548),$=r(9275),A=r(7184),O=R.f,P=$.f,D=T.f,N=n.Symbol,j=n.JSON,E=j&&j.stringify,M=m("_hidden"),F=m("toPrimitive"),q={}.propertyIsEnumerable,L=c("symbol-registry"),B=c("symbols"),U=c("op-symbols"),W=Object.prototype,H="function"==typeof N&&!!C.f,_=n.QObject,V=!_||!_.prototype||!_.prototype.findChild,G=o&&d((function(){return 7!=k(P({},"a",{get:function(){return P(this,"a",{value:7}).a}})).a}))?function(e,t,r){var n=O(W,t);n&&delete W[t],P(e,t,r),n&&e!==W&&P(W,t,n)}:P,z=function(e){var t=B[e]=k(N.prototype);return t._k=e,t},J=H&&"symbol"==typeof N.iterator?function(e){return"symbol"==typeof e}:function(e){return e instanceof N},K=function(e,t,r){return e===W&&K(U,t,r),b(e),t=I(t,!0),b(r),i(B,t)?(r.enumerable?(i(e,M)&&e[M][t]&&(e[M][t]=!1),r=k(r,{enumerable:x(0,!1)})):(i(e,M)||P(e,M,x(1,{})),e[M][t]=!0),G(e,t,r)):P(e,t,r)},X=function(e,t){b(e);for(var r,n=y(t=S(t)),i=0,o=n.length;o>i;)K(e,r=n[i++],t[r]);return e},Y=function(e){var t=q.call(this,e=I(e,!0));return!(this===W&&i(B,e)&&!i(U,e))&&(!(t||!i(this,e)||!i(B,e)||i(this,M)&&this[M][e])||t)},Q=function(e,t){if(e=S(e),t=I(t,!0),e!==W||!i(B,t)||i(U,t)){var r=O(e,t);return!r||!i(B,t)||i(e,M)&&e[M][t]||(r.enumerable=!0),r}},Z=function(e){for(var t,r=D(S(e)),n=[],o=0;r.length>o;)i(B,t=r[o++])||t==M||t==p||n.push(t);return n},ee=function(e){for(var t,r=e===W,n=D(r?U:S(e)),o=[],a=0;n.length>a;)!i(B,t=n[a++])||r&&!i(W,t)||o.push(B[t]);return o};H||(s((N=function(){if(this instanceof N)throw TypeError("Symbol is not a constructor!");var e=u(arguments.length>0?arguments[0]:void 0),t=function(r){this===W&&t.call(U,r),i(this,M)&&i(this[M],e)&&(this[M][e]=!1),G(this,e,x(1,r))};return o&&V&&G(W,e,{configurable:!0,set:t}),z(e)}).prototype,"toString",(function(){return this._k})),R.f=Q,$.f=K,r(616).f=T.f=Z,r(4682).f=Y,C.f=ee,o&&!r(4461)&&s(W,"propertyIsEnumerable",Y,!0),h.f=function(e){return z(m(e))}),a(a.G+a.W+a.F*!H,{Symbol:N});for(var te="hasInstance,isConcatSpreadable,iterator,match,replace,search,species,split,toPrimitive,toStringTag,unscopables".split(","),re=0;te.length>re;)m(te[re++]);for(var ne=A(m.store),ie=0;ne.length>ie;)f(ne[ie++]);a(a.S+a.F*!H,"Symbol",{for:function(e){return i(L,e+="")?L[e]:L[e]=N(e)},keyFor:function(e){if(!J(e))throw TypeError(e+" is not a symbol!");for(var t in L)if(L[t]===e)return t},useSetter:function(){V=!0},useSimple:function(){V=!1}}),a(a.S+a.F*!H,"Object",{create:function(e,t){return void 0===t?k(e):X(k(e),t)},defineProperty:K,defineProperties:X,getOwnPropertyDescriptor:Q,getOwnPropertyNames:Z,getOwnPropertySymbols:ee});var oe=d((function(){C.f(1)}));a(a.S+a.F*oe,"Object",{getOwnPropertySymbols:function(e){return C.f(w(e))}}),j&&a(a.S+a.F*(!H||d((function(){var e=N();return"[null]"!=E([e])||"{}"!=E({a:e})||"{}"!=E(Object(e))}))),"JSON",{stringify:function(e){for(var t,r,n=[e],i=1;arguments.length>i;)n.push(arguments[i++]);if(r=t=n[1],(v(t)||void 0!==e)&&!J(e))return g(t)||(t=function(e,t){if("function"==typeof r&&(t=r.call(this,e,t)),!J(t))return t}),n[1]=t,E.apply(j,n)}}),N.prototype[F]||r(7728)(N.prototype,F,N.prototype.valueOf),l(N,"Symbol"),l(Math,"Math",!0),l(n.JSON,"JSON",!0)},142:(e,t,r)=>{"use strict";var n=r(2985),i=r(9383),o=r(1125),a=r(7007),s=r(2337),p=r(875),d=r(5286),c=r(3816).ArrayBuffer,l=r(8364),u=o.ArrayBuffer,m=o.DataView,h=i.ABV&&c.isView,f=u.prototype.slice,y=i.VIEW,g="ArrayBuffer";n(n.G+n.W+n.F*(c!==u),{ArrayBuffer:u}),n(n.S+n.F*!i.CONSTR,g,{isView:function(e){return h&&h(e)||d(e)&&y in e}}),n(n.P+n.U+n.F*r(4253)((function(){return!new u(2).slice(1,void 0).byteLength})),g,{slice:function(e,t){if(void 0!==f&&void 0===t)return f.call(a(this),e);for(var r=a(this).byteLength,n=s(e,r),i=s(void 0===t?r:t,r),o=new(l(this,u))(p(i-n)),d=new m(this),c=new m(o),h=0;n<i;)c.setUint8(h++,d.getUint8(n++));return o}}),r(2974)(g)},1786:(e,t,r)=>{var n=r(2985);n(n.G+n.W+n.F*!r(9383).ABV,{DataView:r(1125).DataView})},162:(e,t,r)=>{r(8440)("Float32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},3834:(e,t,r)=>{r(8440)("Float64",8,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},4821:(e,t,r)=>{r(8440)("Int16",2,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},1303:(e,t,r)=>{r(8440)("Int32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},5368:(e,t,r)=>{r(8440)("Int8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},9103:(e,t,r)=>{r(8440)("Uint16",2,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},3318:(e,t,r)=>{r(8440)("Uint32",4,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},6964:(e,t,r)=>{r(8440)("Uint8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}))},2152:(e,t,r)=>{r(8440)("Uint8",1,(function(e){return function(t,r,n){return e(this,t,r,n)}}),!0)},147:(e,t,r)=>{"use strict";var n,i=r(3816),o=r(50)(0),a=r(7234),s=r(4728),p=r(5345),d=r(3657),c=r(5286),l=r(1616),u=r(1616),m=!i.ActiveXObject&&"ActiveXObject"in i,h="WeakMap",f=s.getWeak,y=Object.isExtensible,g=d.ufstore,b=function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}},v={get:function(e){if(c(e)){var t=f(e);return!0===t?g(l(this,h)).get(e):t?t[this._i]:void 0}},set:function(e,t){return d.def(l(this,h),e,t)}},w=e.exports=r(5795)(h,b,v,d,!0,!0);u&&m&&(p((n=d.getConstructor(b,h)).prototype,v),s.NEED=!0,o(["delete","has","get","set"],(function(e){var t=w.prototype,r=t[e];a(t,e,(function(t,i){if(c(t)&&!y(t)){this._f||(this._f=new n);var o=this._f[e](t,i);return"set"==e?this:o}return r.call(this,t,i)}))})))},9192:(e,t,r)=>{"use strict";var n=r(3657),i=r(1616),o="WeakSet";r(5795)(o,(function(e){return function(){return e(this,arguments.length>0?arguments[0]:void 0)}}),{add:function(e){return n.def(i(this,o),e,!0)}},n,!1,!0)},1268:(e,t,r)=>{"use strict";var n=r(2985),i=r(3325),o=r(508),a=r(875),s=r(4963),p=r(6886);n(n.P,"Array",{flatMap:function(e){var t,r,n=o(this);return s(e),t=a(n.length),r=p(n,0),i(r,n,n,t,0,1,e,arguments[1]),r}}),r(7722)("flatMap")},4692:(e,t,r)=>{"use strict";var n=r(2985),i=r(3325),o=r(508),a=r(875),s=r(1467),p=r(6886);n(n.P,"Array",{flatten:function(){var e=arguments[0],t=o(this),r=a(t.length),n=p(t,0);return i(n,t,t,r,0,void 0===e?1:s(e)),n}}),r(7722)("flatten")},2773:(e,t,r)=>{"use strict";var n=r(2985),i=r(9315)(!0);n(n.P,"Array",{includes:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0)}}),r(7722)("includes")},8267:(e,t,r)=>{var n=r(2985),i=r(4351)(),o=r(3816).process,a="process"==r(2032)(o);n(n.G,{asap:function(e){var t=a&&o.domain;i(t?t.bind(e):e)}})},2559:(e,t,r)=>{var n=r(2985),i=r(2032);n(n.S,"Error",{isError:function(e){return"Error"===i(e)}})},5575:(e,t,r)=>{var n=r(2985);n(n.G,{global:r(3816)})},525:(e,t,r)=>{r(1024)("Map")},8211:(e,t,r)=>{r(4881)("Map")},7698:(e,t,r)=>{var n=r(2985);n(n.P+n.R,"Map",{toJSON:r(6132)("Map")})},8865:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{clamp:function(e,t,r){return Math.min(r,Math.max(t,e))}})},368:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{DEG_PER_RAD:Math.PI/180})},6427:(e,t,r)=>{var n=r(2985),i=180/Math.PI;n(n.S,"Math",{degrees:function(e){return e*i}})},286:(e,t,r)=>{var n=r(2985),i=r(8757),o=r(4934);n(n.S,"Math",{fscale:function(e,t,r,n,a){return o(i(e,t,r,n,a))}})},2816:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{iaddh:function(e,t,r,n){var i=e>>>0,o=r>>>0;return(t>>>0)+(n>>>0)+((i&o|(i|o)&~(i+o>>>0))>>>31)|0}})},2082:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{imulh:function(e,t){var r=65535,n=+e,i=+t,o=n&r,a=i&r,s=n>>16,p=i>>16,d=(s*a>>>0)+(o*a>>>16);return s*p+(d>>16)+((o*p>>>0)+(d&r)>>16)}})},5986:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{isubh:function(e,t,r,n){var i=e>>>0,o=r>>>0;return(t>>>0)-(n>>>0)-((~i&o|~(i^o)&i-o>>>0)>>>31)|0}})},6308:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{RAD_PER_DEG:180/Math.PI})},9221:(e,t,r)=>{var n=r(2985),i=Math.PI/180;n(n.S,"Math",{radians:function(e){return e*i}})},3570:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{scale:r(8757)})},3776:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{signbit:function(e){return(e=+e)!=e?e:0==e?1/e==1/0:e>0}})},6754:(e,t,r)=>{var n=r(2985);n(n.S,"Math",{umulh:function(e,t){var r=65535,n=+e,i=+t,o=n&r,a=i&r,s=n>>>16,p=i>>>16,d=(s*a>>>0)+(o*a>>>16);return s*p+(d>>>16)+((o*p>>>0)+(d&r)>>>16)}})},8646:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(4963),a=r(9275);r(7057)&&n(n.P+r(1670),"Object",{__defineGetter__:function(e,t){a.f(i(this),e,{get:o(t),enumerable:!0,configurable:!0})}})},2658:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(4963),a=r(9275);r(7057)&&n(n.P+r(1670),"Object",{__defineSetter__:function(e,t){a.f(i(this),e,{set:o(t),enumerable:!0,configurable:!0})}})},3276:(e,t,r)=>{var n=r(2985),i=r(1131)(!0);n(n.S,"Object",{entries:function(e){return i(e)}})},8351:(e,t,r)=>{var n=r(2985),i=r(7643),o=r(2110),a=r(8693),s=r(2811);n(n.S,"Object",{getOwnPropertyDescriptors:function(e){for(var t,r,n=o(e),p=a.f,d=i(n),c={},l=0;d.length>l;)void 0!==(r=p(n,t=d[l++]))&&s(c,t,r);return c}})},6917:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689),a=r(468),s=r(8693).f;r(7057)&&n(n.P+r(1670),"Object",{__lookupGetter__:function(e){var t,r=i(this),n=o(e,!0);do{if(t=s(r,n))return t.get}while(r=a(r))}})},372:(e,t,r)=>{"use strict";var n=r(2985),i=r(508),o=r(1689),a=r(468),s=r(8693).f;r(7057)&&n(n.P+r(1670),"Object",{__lookupSetter__:function(e){var t,r=i(this),n=o(e,!0);do{if(t=s(r,n))return t.set}while(r=a(r))}})},6409:(e,t,r)=>{var n=r(2985),i=r(1131)(!1);n(n.S,"Object",{values:function(e){return i(e)}})},6534:(e,t,r)=>{"use strict";var n=r(2985),i=r(3816),o=r(5645),a=r(4351)(),s=r(6314)("observable"),p=r(4963),d=r(7007),c=r(3328),l=r(4408),u=r(7728),m=r(3531),h=m.RETURN,f=function(e){return null==e?void 0:p(e)},y=function(e){var t=e._c;t&&(e._c=void 0,t())},g=function(e){return void 0===e._o},b=function(e){g(e)||(e._o=void 0,y(e))},v=function(e,t){d(e),this._c=void 0,this._o=e,e=new w(this);try{var r=t(e),n=r;null!=r&&("function"==typeof r.unsubscribe?r=function(){n.unsubscribe()}:p(r),this._c=r)}catch(t){return void e.error(t)}g(this)&&y(this)};v.prototype=l({},{unsubscribe:function(){b(this)}});var w=function(e){this._s=e};w.prototype=l({},{next:function(e){var t=this._s;if(!g(t)){var r=t._o;try{var n=f(r.next);if(n)return n.call(r,e)}catch(e){try{b(t)}finally{throw e}}}},error:function(e){var t=this._s;if(g(t))throw e;var r=t._o;t._o=void 0;try{var n=f(r.error);if(!n)throw e;e=n.call(r,e)}catch(e){try{y(t)}finally{throw e}}return y(t),e},complete:function(e){var t=this._s;if(!g(t)){var r=t._o;t._o=void 0;try{var n=f(r.complete);e=n?n.call(r,e):void 0}catch(e){try{y(t)}finally{throw e}}return y(t),e}}});var S=function(e){c(this,S,"Observable","_f")._f=p(e)};l(S.prototype,{subscribe:function(e){return new v(e,this._f)},forEach:function(e){var t=this;return new(o.Promise||i.Promise)((function(r,n){p(e);var i=t.subscribe({next:function(t){try{return e(t)}catch(e){n(e),i.unsubscribe()}},error:n,complete:r})}))}}),l(S,{from:function(e){var t="function"==typeof this?this:S,r=f(d(e)[s]);if(r){var n=d(r.call(e));return n.constructor===t?n:new t((function(e){return n.subscribe(e)}))}return new t((function(t){var r=!1;return a((function(){if(!r){try{if(m(e,!1,(function(e){if(t.next(e),r)return h}))===h)return}catch(e){if(r)throw e;return void t.error(e)}t.complete()}})),function(){r=!0}}))},of:function(){for(var e=0,t=arguments.length,r=new Array(t);e<t;)r[e]=arguments[e++];return new("function"==typeof this?this:S)((function(e){var t=!1;return a((function(){if(!t){for(var n=0;n<r.length;++n)if(e.next(r[n]),t)return;e.complete()}})),function(){t=!0}}))}}),u(S.prototype,s,(function(){return this})),n(n.G,{Observable:S}),r(2974)("Observable")},9865:(e,t,r)=>{"use strict";var n=r(2985),i=r(5645),o=r(3816),a=r(8364),s=r(94);n(n.P+n.R,"Promise",{finally:function(e){var t=a(this,i.Promise||o.Promise),r="function"==typeof e;return this.then(r?function(r){return s(t,e()).then((function(){return r}))}:e,r?function(r){return s(t,e()).then((function(){throw r}))}:e)}})},1898:(e,t,r)=>{"use strict";var n=r(2985),i=r(3499),o=r(188);n(n.S,"Promise",{try:function(e){var t=i.f(this),r=o(e);return(r.e?t.reject:t.resolve)(r.v),t.promise}})},3364:(e,t,r)=>{var n=r(133),i=r(7007),o=n.key,a=n.set;n.exp({defineMetadata:function(e,t,r,n){a(e,t,i(r),o(n))}})},1432:(e,t,r)=>{var n=r(133),i=r(7007),o=n.key,a=n.map,s=n.store;n.exp({deleteMetadata:function(e,t){var r=arguments.length<3?void 0:o(arguments[2]),n=a(i(t),r,!1);if(void 0===n||!n.delete(e))return!1;if(n.size)return!0;var p=s.get(t);return p.delete(r),!!p.size||s.delete(t)}})},4416:(e,t,r)=>{var n=r(8184),i=r(9490),o=r(133),a=r(7007),s=r(468),p=o.keys,d=o.key,c=function(e,t){var r=p(e,t),o=s(e);if(null===o)return r;var a=c(o,t);return a.length?r.length?i(new n(r.concat(a))):a:r};o.exp({getMetadataKeys:function(e){return c(a(e),arguments.length<2?void 0:d(arguments[1]))}})},6562:(e,t,r)=>{var n=r(133),i=r(7007),o=r(468),a=n.has,s=n.get,p=n.key,d=function(e,t,r){if(a(e,t,r))return s(e,t,r);var n=o(t);return null!==n?d(e,n,r):void 0};n.exp({getMetadata:function(e,t){return d(e,i(t),arguments.length<3?void 0:p(arguments[2]))}})},2213:(e,t,r)=>{var n=r(133),i=r(7007),o=n.keys,a=n.key;n.exp({getOwnMetadataKeys:function(e){return o(i(e),arguments.length<2?void 0:a(arguments[1]))}})},8681:(e,t,r)=>{var n=r(133),i=r(7007),o=n.get,a=n.key;n.exp({getOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},3471:(e,t,r)=>{var n=r(133),i=r(7007),o=r(468),a=n.has,s=n.key,p=function(e,t,r){if(a(e,t,r))return!0;var n=o(t);return null!==n&&p(e,n,r)};n.exp({hasMetadata:function(e,t){return p(e,i(t),arguments.length<3?void 0:s(arguments[2]))}})},4329:(e,t,r)=>{var n=r(133),i=r(7007),o=n.has,a=n.key;n.exp({hasOwnMetadata:function(e,t){return o(e,i(t),arguments.length<3?void 0:a(arguments[2]))}})},5159:(e,t,r)=>{var n=r(133),i=r(7007),o=r(4963),a=n.key,s=n.set;n.exp({metadata:function(e,t){return function(r,n){s(e,t,(void 0!==n?i:o)(r),a(n))}}})},9467:(e,t,r)=>{r(1024)("Set")},4837:(e,t,r)=>{r(4881)("Set")},8739:(e,t,r)=>{var n=r(2985);n(n.P+n.R,"Set",{toJSON:r(6132)("Set")})},7220:(e,t,r)=>{"use strict";var n=r(2985),i=r(4496)(!0),o=r(4253)((function(){return"𠮷"!=="𠮷".at(0)}));n(n.P+n.F*o,"String",{at:function(e){return i(this,e)}})},4208:(e,t,r)=>{"use strict";var n=r(2985),i=r(1355),o=r(875),a=r(5364),s=r(3218),p=RegExp.prototype,d=function(e,t){this._r=e,this._s=t};r(9988)(d,"RegExp String",(function(){var e=this._r.exec(this._s);return{value:e,done:null===e}})),n(n.P,"String",{matchAll:function(e){if(i(this),!a(e))throw TypeError(e+" is not a regexp!");var t=String(this),r="flags"in p?String(e.flags):s.call(e),n=new RegExp(e.source,~r.indexOf("g")?r:"g"+r);return n.lastIndex=o(e.lastIndex),new d(n,t)}})},2770:(e,t,r)=>{"use strict";var n=r(2985),i=r(5442),o=r(575),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);n(n.P+n.F*a,"String",{padEnd:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!1)}})},1784:(e,t,r)=>{"use strict";var n=r(2985),i=r(5442),o=r(575),a=/Version\/10\.\d+(\.\d+)?( Mobile\/\w+)? Safari\//.test(o);n(n.P+n.F*a,"String",{padStart:function(e){return i(this,e,arguments.length>1?arguments[1]:void 0,!0)}})},5869:(e,t,r)=>{"use strict";r(9599)("trimLeft",(function(e){return function(){return e(this,1)}}),"trimStart")},4325:(e,t,r)=>{"use strict";r(9599)("trimRight",(function(e){return function(){return e(this,2)}}),"trimEnd")},9665:(e,t,r)=>{r(6074)("asyncIterator")},9593:(e,t,r)=>{r(6074)("observable")},8967:(e,t,r)=>{var n=r(2985);n(n.S,"System",{global:r(3816)})},4188:(e,t,r)=>{r(1024)("WeakMap")},7594:(e,t,r)=>{r(4881)("WeakMap")},3495:(e,t,r)=>{r(1024)("WeakSet")},9550:(e,t,r)=>{r(4881)("WeakSet")},1181:(e,t,r)=>{for(var n=r(6997),i=r(7184),o=r(7234),a=r(3816),s=r(7728),p=r(2803),d=r(6314),c=d("iterator"),l=d("toStringTag"),u=p.Array,m={CSSRuleList:!0,CSSStyleDeclaration:!1,CSSValueList:!1,ClientRectList:!1,DOMRectList:!1,DOMStringList:!1,DOMTokenList:!0,DataTransferItemList:!1,FileList:!1,HTMLAllCollection:!1,HTMLCollection:!1,HTMLFormElement:!1,HTMLSelectElement:!1,MediaList:!0,MimeTypeArray:!1,NamedNodeMap:!1,NodeList:!0,PaintRequestList:!1,Plugin:!1,PluginArray:!1,SVGLengthList:!1,SVGNumberList:!1,SVGPathSegList:!1,SVGPointList:!1,SVGStringList:!1,SVGTransformList:!1,SourceBufferList:!1,StyleSheetList:!0,TextTrackCueList:!1,TextTrackList:!1,TouchList:!1},h=i(m),f=0;f<h.length;f++){var y,g=h[f],b=m[g],v=a[g],w=v&&v.prototype;if(w&&(w[c]||s(w,c,u),w[l]||s(w,l,g),p[g]=u,b))for(y in n)w[y]||o(w,y,n[y],!0)}},4633:(e,t,r)=>{var n=r(2985),i=r(4193);n(n.G+n.B,{setImmediate:i.set,clearImmediate:i.clear})},2564:(e,t,r)=>{var n=r(3816),i=r(2985),o=r(575),a=[].slice,s=/MSIE .\./.test(o),p=function(e){return function(t,r){var n=arguments.length>2,i=!!n&&a.call(arguments,2);return e(n?function(){("function"==typeof t?t:Function(t)).apply(this,i)}:t,r)}};i(i.G+i.B+i.F*s,{setTimeout:p(n.setTimeout),setInterval:p(n.setInterval)})},1934:(e,t,r)=>{r(5767),r(8132),r(8388),r(7470),r(4882),r(1520),r(7476),r(9622),r(9375),r(3533),r(4672),r(4157),r(5095),r(9892),r(5115),r(9176),r(8838),r(6253),r(9730),r(6059),r(8377),r(1084),r(4299),r(1246),r(726),r(1901),r(5972),r(3403),r(2516),r(9371),r(6479),r(1736),r(1889),r(5177),r(6943),r(6503),r(6786),r(932),r(7526),r(1591),r(9073),r(347),r(579),r(4669),r(7710),r(5789),r(3514),r(9978),r(8472),r(6946),r(5068),r(413),r(191),r(8306),r(4564),r(9115),r(9539),r(6620),r(2850),r(823),r(7732),r(856),r(703),r(1539),r(5292),r(6629),r(3694),r(7648),r(7795),r(4531),r(3605),r(6780),r(9937),r(511),r(1822),r(9977),r(1031),r(6331),r(1560),r(774),r(522),r(8295),r(7842),r(110),r(75),r(4336),r(1802),r(8837),r(6773),r(5745),r(3057),r(3750),r(3369),r(9564),r(2e3),r(8977),r(2310),r(4899),r(1842),r(6997),r(3946),r(8269),r(6108),r(6774),r(1466),r(9357),r(6142),r(1876),r(851),r(8416),r(8184),r(147),r(9192),r(142),r(1786),r(5368),r(6964),r(2152),r(4821),r(9103),r(1303),r(3318),r(162),r(3834),r(1572),r(2139),r(685),r(5535),r(7347),r(3049),r(6633),r(8989),r(8270),r(4510),r(3984),r(5769),r(55),r(6014),r(2773),r(1268),r(4692),r(7220),r(1784),r(2770),r(5869),r(4325),r(4208),r(9665),r(9593),r(8351),r(6409),r(3276),r(8646),r(2658),r(6917),r(372),r(7698),r(8739),r(8211),r(4837),r(7594),r(9550),r(525),r(9467),r(4188),r(3495),r(5575),r(8967),r(2559),r(8865),r(368),r(6427),r(286),r(2816),r(5986),r(2082),r(6308),r(9221),r(3570),r(6754),r(3776),r(9865),r(1898),r(3364),r(1432),r(6562),r(4416),r(8681),r(2213),r(3471),r(4329),r(5159),r(8267),r(6534),r(2564),r(4633),r(1181),e.exports=r(5645)},7187:e=>{"use strict";var t,r="object"==typeof Reflect?Reflect:null,n=r&&"function"==typeof r.apply?r.apply:function(e,t,r){return Function.prototype.apply.call(e,t,r)};t=r&&"function"==typeof r.ownKeys?r.ownKeys:Object.getOwnPropertySymbols?function(e){return Object.getOwnPropertyNames(e).concat(Object.getOwnPropertySymbols(e))}:function(e){return Object.getOwnPropertyNames(e)};var i=Number.isNaN||function(e){return e!=e};function o(){o.init.call(this)}e.exports=o,e.exports.once=function(e,t){return new Promise((function(r,n){function i(r){e.removeListener(t,o),n(r)}function o(){"function"==typeof e.removeListener&&e.removeListener("error",i),r([].slice.call(arguments))}f(e,t,o,{once:!0}),"error"!==t&&function(e,t,r){"function"==typeof e.on&&f(e,"error",t,r)}(e,i,{once:!0})}))},o.EventEmitter=o,o.prototype._events=void 0,o.prototype._eventsCount=0,o.prototype._maxListeners=void 0;var a=10;function s(e){if("function"!=typeof e)throw new TypeError('The "listener" argument must be of type Function. Received type '+typeof e)}function p(e){return void 0===e._maxListeners?o.defaultMaxListeners:e._maxListeners}function d(e,t,r,n){var i,o,a,d;if(s(r),void 0===(o=e._events)?(o=e._events=Object.create(null),e._eventsCount=0):(void 0!==o.newListener&&(e.emit("newListener",t,r.listener?r.listener:r),o=e._events),a=o[t]),void 0===a)a=o[t]=r,++e._eventsCount;else if("function"==typeof a?a=o[t]=n?[r,a]:[a,r]:n?a.unshift(r):a.push(r),(i=p(e))>0&&a.length>i&&!a.warned){a.warned=!0;var c=new Error("Possible EventEmitter memory leak detected. "+a.length+" "+String(t)+" listeners added. Use emitter.setMaxListeners() to increase limit");c.name="MaxListenersExceededWarning",c.emitter=e,c.type=t,c.count=a.length,d=c,console&&console.warn&&console.warn(d)}return e}function c(){if(!this.fired)return this.target.removeListener(this.type,this.wrapFn),this.fired=!0,0===arguments.length?this.listener.call(this.target):this.listener.apply(this.target,arguments)}function l(e,t,r){var n={fired:!1,wrapFn:void 0,target:e,type:t,listener:r},i=c.bind(n);return i.listener=r,n.wrapFn=i,i}function u(e,t,r){var n=e._events;if(void 0===n)return[];var i=n[t];return void 0===i?[]:"function"==typeof i?r?[i.listener||i]:[i]:r?function(e){for(var t=new Array(e.length),r=0;r<t.length;++r)t[r]=e[r].listener||e[r];return t}(i):h(i,i.length)}function m(e){var t=this._events;if(void 0!==t){var r=t[e];if("function"==typeof r)return 1;if(void 0!==r)return r.length}return 0}function h(e,t){for(var r=new Array(t),n=0;n<t;++n)r[n]=e[n];return r}function f(e,t,r,n){if("function"==typeof e.on)n.once?e.once(t,r):e.on(t,r);else{if("function"!=typeof e.addEventListener)throw new TypeError('The "emitter" argument must be of type EventEmitter. Received type '+typeof e);e.addEventListener(t,(function i(o){n.once&&e.removeEventListener(t,i),r(o)}))}}Object.defineProperty(o,"defaultMaxListeners",{enumerable:!0,get:function(){return a},set:function(e){if("number"!=typeof e||e<0||i(e))throw new RangeError('The value of "defaultMaxListeners" is out of range. It must be a non-negative number. Received '+e+".");a=e}}),o.init=function(){void 0!==this._events&&this._events!==Object.getPrototypeOf(this)._events||(this._events=Object.create(null),this._eventsCount=0),this._maxListeners=this._maxListeners||void 0},o.prototype.setMaxListeners=function(e){if("number"!=typeof e||e<0||i(e))throw new RangeError('The value of "n" is out of range. It must be a non-negative number. Received '+e+".");return this._maxListeners=e,this},o.prototype.getMaxListeners=function(){return p(this)},o.prototype.emit=function(e){for(var t=[],r=1;r<arguments.length;r++)t.push(arguments[r]);var i="error"===e,o=this._events;if(void 0!==o)i=i&&void 0===o.error;else if(!i)return!1;if(i){var a;if(t.length>0&&(a=t[0]),a instanceof Error)throw a;var s=new Error("Unhandled error."+(a?" ("+a.message+")":""));throw s.context=a,s}var p=o[e];if(void 0===p)return!1;if("function"==typeof p)n(p,this,t);else{var d=p.length,c=h(p,d);for(r=0;r<d;++r)n(c[r],this,t)}return!0},o.prototype.addListener=function(e,t){return d(this,e,t,!1)},o.prototype.on=o.prototype.addListener,o.prototype.prependListener=function(e,t){return d(this,e,t,!0)},o.prototype.once=function(e,t){return s(t),this.on(e,l(this,e,t)),this},o.prototype.prependOnceListener=function(e,t){return s(t),this.prependListener(e,l(this,e,t)),this},o.prototype.removeListener=function(e,t){var r,n,i,o,a;if(s(t),void 0===(n=this._events))return this;if(void 0===(r=n[e]))return this;if(r===t||r.listener===t)0==--this._eventsCount?this._events=Object.create(null):(delete n[e],n.removeListener&&this.emit("removeListener",e,r.listener||t));else if("function"!=typeof r){for(i=-1,o=r.length-1;o>=0;o--)if(r[o]===t||r[o].listener===t){a=r[o].listener,i=o;break}if(i<0)return this;0===i?r.shift():function(e,t){for(;t+1<e.length;t++)e[t]=e[t+1];e.pop()}(r,i),1===r.length&&(n[e]=r[0]),void 0!==n.removeListener&&this.emit("removeListener",e,a||t)}return this},o.prototype.off=o.prototype.removeListener,o.prototype.removeAllListeners=function(e){var t,r,n;if(void 0===(r=this._events))return this;if(void 0===r.removeListener)return 0===arguments.length?(this._events=Object.create(null),this._eventsCount=0):void 0!==r[e]&&(0==--this._eventsCount?this._events=Object.create(null):delete r[e]),this;if(0===arguments.length){var i,o=Object.keys(r);for(n=0;n<o.length;++n)"removeListener"!==(i=o[n])&&this.removeAllListeners(i);return this.removeAllListeners("removeListener"),this._events=Object.create(null),this._eventsCount=0,this}if("function"==typeof(t=r[e]))this.removeListener(e,t);else if(void 0!==t)for(n=t.length-1;n>=0;n--)this.removeListener(e,t[n]);return this},o.prototype.listeners=function(e){return u(this,e,!0)},o.prototype.rawListeners=function(e){return u(this,e,!1)},o.listenerCount=function(e,t){return"function"==typeof e.listenerCount?e.listenerCount(t):m.call(e,t)},o.prototype.listenerCount=m,o.prototype.eventNames=function(){return this._eventsCount>0?t(this._events):[]}},4029:(e,t,r)=>{"use strict";var n=r(5320),i=Object.prototype.toString,o=Object.prototype.hasOwnProperty,a=function(e,t,r){for(var n=0,i=e.length;n<i;n++)o.call(e,n)&&(null==r?t(e[n],n,e):t.call(r,e[n],n,e))},s=function(e,t,r){for(var n=0,i=e.length;n<i;n++)null==r?t(e.charAt(n),n,e):t.call(r,e.charAt(n),n,e)},p=function(e,t,r){for(var n in e)o.call(e,n)&&(null==r?t(e[n],n,e):t.call(r,e[n],n,e))};e.exports=function(e,t,r){if(!n(t))throw new TypeError("iterator must be a function");var o;arguments.length>=3&&(o=r),"[object Array]"===i.call(e)?a(e,t,o):"string"==typeof e?s(e,t,o):p(e,t,o)}},9092:e=>{"use strict";var t="Function.prototype.bind called on incompatible ",r=Array.prototype.slice,n=Object.prototype.toString,i="[object Function]";e.exports=function(e){var o=this;if("function"!=typeof o||n.call(o)!==i)throw new TypeError(t+o);for(var a,s=r.call(arguments,1),p=function(){if(this instanceof a){var t=o.apply(this,s.concat(r.call(arguments)));return Object(t)===t?t:this}return o.apply(e,s.concat(r.call(arguments)))},d=Math.max(0,o.length-s.length),c=[],l=0;l<d;l++)c.push("$"+l);if(a=Function("binder","return function ("+c.join(",")+"){ return binder.apply(this,arguments); }")(p),o.prototype){var u=function(){};u.prototype=o.prototype,a.prototype=new u,u.prototype=null}return a}},8612:(e,t,r)=>{"use strict";var n=r(9092);e.exports=Function.prototype.bind||n},210:(e,t,r)=>{"use strict";var n,i=SyntaxError,o=Function,a=TypeError,s=function(e){try{return o('"use strict"; return ('+e+").constructor;")()}catch(e){}},p=Object.getOwnPropertyDescriptor;if(p)try{p({},"")}catch(e){p=null}var d=function(){throw new a},c=p?function(){try{return d}catch(e){try{return p(arguments,"callee").get}catch(e){return d}}}():d,l=r(1405)(),u=Object.getPrototypeOf||function(e){return e.__proto__},m={},h="undefined"==typeof Uint8Array?n:u(Uint8Array),f={"%AggregateError%":"undefined"==typeof AggregateError?n:AggregateError,"%Array%":Array,"%ArrayBuffer%":"undefined"==typeof ArrayBuffer?n:ArrayBuffer,"%ArrayIteratorPrototype%":l?u([][Symbol.iterator]()):n,"%AsyncFromSyncIteratorPrototype%":n,"%AsyncFunction%":m,"%AsyncGenerator%":m,"%AsyncGeneratorFunction%":m,"%AsyncIteratorPrototype%":m,"%Atomics%":"undefined"==typeof Atomics?n:Atomics,"%BigInt%":"undefined"==typeof BigInt?n:BigInt,"%Boolean%":Boolean,"%DataView%":"undefined"==typeof DataView?n:DataView,"%Date%":Date,"%decodeURI%":decodeURI,"%decodeURIComponent%":decodeURIComponent,"%encodeURI%":encodeURI,"%encodeURIComponent%":encodeURIComponent,"%Error%":Error,"%eval%":eval,"%EvalError%":EvalError,"%Float32Array%":"undefined"==typeof Float32Array?n:Float32Array,"%Float64Array%":"undefined"==typeof Float64Array?n:Float64Array,"%FinalizationRegistry%":"undefined"==typeof FinalizationRegistry?n:FinalizationRegistry,"%Function%":o,"%GeneratorFunction%":m,"%Int8Array%":"undefined"==typeof Int8Array?n:Int8Array,"%Int16Array%":"undefined"==typeof Int16Array?n:Int16Array,"%Int32Array%":"undefined"==typeof Int32Array?n:Int32Array,"%isFinite%":isFinite,"%isNaN%":isNaN,"%IteratorPrototype%":l?u(u([][Symbol.iterator]())):n,"%JSON%":"object"==typeof JSON?JSON:n,"%Map%":"undefined"==typeof Map?n:Map,"%MapIteratorPrototype%":"undefined"!=typeof Map&&l?u((new Map)[Symbol.iterator]()):n,"%Math%":Math,"%Number%":Number,"%Object%":Object,"%parseFloat%":parseFloat,"%parseInt%":parseInt,"%Promise%":"undefined"==typeof Promise?n:Promise,"%Proxy%":"undefined"==typeof Proxy?n:Proxy,"%RangeError%":RangeError,"%ReferenceError%":ReferenceError,"%Reflect%":"undefined"==typeof Reflect?n:Reflect,"%RegExp%":RegExp,"%Set%":"undefined"==typeof Set?n:Set,"%SetIteratorPrototype%":"undefined"!=typeof Set&&l?u((new Set)[Symbol.iterator]()):n,"%SharedArrayBuffer%":"undefined"==typeof SharedArrayBuffer?n:SharedArrayBuffer,"%String%":String,"%StringIteratorPrototype%":l?u(""[Symbol.iterator]()):n,"%Symbol%":l?Symbol:n,"%SyntaxError%":i,"%ThrowTypeError%":c,"%TypedArray%":h,"%TypeError%":a,"%Uint8Array%":"undefined"==typeof Uint8Array?n:Uint8Array,"%Uint8ClampedArray%":"undefined"==typeof Uint8ClampedArray?n:Uint8ClampedArray,"%Uint16Array%":"undefined"==typeof Uint16Array?n:Uint16Array,"%Uint32Array%":"undefined"==typeof Uint32Array?n:Uint32Array,"%URIError%":URIError,"%WeakMap%":"undefined"==typeof WeakMap?n:WeakMap,"%WeakRef%":"undefined"==typeof WeakRef?n:WeakRef,"%WeakSet%":"undefined"==typeof WeakSet?n:WeakSet},y=function e(t){var r;if("%AsyncFunction%"===t)r=s("async function () {}");else if("%GeneratorFunction%"===t)r=s("function* () {}");else if("%AsyncGeneratorFunction%"===t)r=s("async function* () {}");else if("%AsyncGenerator%"===t){var n=e("%AsyncGeneratorFunction%");n&&(r=n.prototype)}else if("%AsyncIteratorPrototype%"===t){var i=e("%AsyncGenerator%");i&&(r=u(i.prototype))}return f[t]=r,r},g={"%ArrayBufferPrototype%":["ArrayBuffer","prototype"],"%ArrayPrototype%":["Array","prototype"],"%ArrayProto_entries%":["Array","prototype","entries"],"%ArrayProto_forEach%":["Array","prototype","forEach"],"%ArrayProto_keys%":["Array","prototype","keys"],"%ArrayProto_values%":["Array","prototype","values"],"%AsyncFunctionPrototype%":["AsyncFunction","prototype"],"%AsyncGenerator%":["AsyncGeneratorFunction","prototype"],"%AsyncGeneratorPrototype%":["AsyncGeneratorFunction","prototype","prototype"],"%BooleanPrototype%":["Boolean","prototype"],"%DataViewPrototype%":["DataView","prototype"],"%DatePrototype%":["Date","prototype"],"%ErrorPrototype%":["Error","prototype"],"%EvalErrorPrototype%":["EvalError","prototype"],"%Float32ArrayPrototype%":["Float32Array","prototype"],"%Float64ArrayPrototype%":["Float64Array","prototype"],"%FunctionPrototype%":["Function","prototype"],"%Generator%":["GeneratorFunction","prototype"],"%GeneratorPrototype%":["GeneratorFunction","prototype","prototype"],"%Int8ArrayPrototype%":["Int8Array","prototype"],"%Int16ArrayPrototype%":["Int16Array","prototype"],"%Int32ArrayPrototype%":["Int32Array","prototype"],"%JSONParse%":["JSON","parse"],"%JSONStringify%":["JSON","stringify"],"%MapPrototype%":["Map","prototype"],"%NumberPrototype%":["Number","prototype"],"%ObjectPrototype%":["Object","prototype"],"%ObjProto_toString%":["Object","prototype","toString"],"%ObjProto_valueOf%":["Object","prototype","valueOf"],"%PromisePrototype%":["Promise","prototype"],"%PromiseProto_then%":["Promise","prototype","then"],"%Promise_all%":["Promise","all"],"%Promise_reject%":["Promise","reject"],"%Promise_resolve%":["Promise","resolve"],"%RangeErrorPrototype%":["RangeError","prototype"],"%ReferenceErrorPrototype%":["ReferenceError","prototype"],"%RegExpPrototype%":["RegExp","prototype"],"%SetPrototype%":["Set","prototype"],"%SharedArrayBufferPrototype%":["SharedArrayBuffer","prototype"],"%StringPrototype%":["String","prototype"],"%SymbolPrototype%":["Symbol","prototype"],"%SyntaxErrorPrototype%":["SyntaxError","prototype"],"%TypedArrayPrototype%":["TypedArray","prototype"],"%TypeErrorPrototype%":["TypeError","prototype"],"%Uint8ArrayPrototype%":["Uint8Array","prototype"],"%Uint8ClampedArrayPrototype%":["Uint8ClampedArray","prototype"],"%Uint16ArrayPrototype%":["Uint16Array","prototype"],"%Uint32ArrayPrototype%":["Uint32Array","prototype"],"%URIErrorPrototype%":["URIError","prototype"],"%WeakMapPrototype%":["WeakMap","prototype"],"%WeakSetPrototype%":["WeakSet","prototype"]},b=r(8612),v=r(7642),w=b.call(Function.call,Array.prototype.concat),S=b.call(Function.apply,Array.prototype.splice),I=b.call(Function.call,String.prototype.replace),x=b.call(Function.call,String.prototype.slice),k=b.call(Function.call,RegExp.prototype.exec),T=/[^%.[\]]+|\[(?:(-?\d+(?:\.\d+)?)|(["'])((?:(?!\2)[^\\]|\\.)*?)\2)\]|(?=(?:\.|\[\])(?:\.|\[\]|%$))/g,R=/\\(\\)?/g,C=function(e){var t=x(e,0,1),r=x(e,-1);if("%"===t&&"%"!==r)throw new i("invalid intrinsic syntax, expected closing `%`");if("%"===r&&"%"!==t)throw new i("invalid intrinsic syntax, expected opening `%`");var n=[];return I(e,T,(function(e,t,r,i){n[n.length]=r?I(i,R,"$1"):t||e})),n},$=function(e,t){var r,n=e;if(v(g,n)&&(n="%"+(r=g[n])[0]+"%"),v(f,n)){var o=f[n];if(o===m&&(o=y(n)),void 0===o&&!t)throw new a("intrinsic "+e+" exists, but is not available. Please file an issue!");return{alias:r,name:n,value:o}}throw new i("intrinsic "+e+" does not exist!")};e.exports=function(e,t){if("string"!=typeof e||0===e.length)throw new a("intrinsic name must be a non-empty string");if(arguments.length>1&&"boolean"!=typeof t)throw new a('"allowMissing" argument must be a boolean');if(null===k(/^%?[^%]*%?$/g,e))throw new i("`%` may not be present anywhere but at the beginning and end of the intrinsic name");var r=C(e),n=r.length>0?r[0]:"",o=$("%"+n+"%",t),s=o.name,d=o.value,c=!1,l=o.alias;l&&(n=l[0],S(r,w([0,1],l)));for(var u=1,m=!0;u<r.length;u+=1){var h=r[u],y=x(h,0,1),g=x(h,-1);if(('"'===y||"'"===y||"`"===y||'"'===g||"'"===g||"`"===g)&&y!==g)throw new i("property names with quotes must have matching quotes");if("constructor"!==h&&m||(c=!0),v(f,s="%"+(n+="."+h)+"%"))d=f[s];else if(null!=d){if(!(h in d)){if(!t)throw new a("base intrinsic for "+e+" exists, but the property is not available.");return}if(p&&u+1>=r.length){var b=p(d,h);d=(m=!!b)&&"get"in b&&!("originalValue"in b.get)?b.get:d[h]}else m=v(d,h),d=d[h];m&&!c&&(f[s]=d)}}return d}},1405:(e,t,r)=>{"use strict";var n="undefined"!=typeof Symbol&&Symbol,i=r(5419);e.exports=function(){return"function"==typeof n&&("function"==typeof Symbol&&("symbol"==typeof n("foo")&&("symbol"==typeof Symbol("bar")&&i())))}},5419:e=>{"use strict";e.exports=function(){if("function"!=typeof Symbol||"function"!=typeof Object.getOwnPropertySymbols)return!1;if("symbol"==typeof Symbol.iterator)return!0;var e={},t=Symbol("test"),r=Object(t);if("string"==typeof t)return!1;if("[object Symbol]"!==Object.prototype.toString.call(t))return!1;if("[object Symbol]"!==Object.prototype.toString.call(r))return!1;for(t in e[t]=42,e)return!1;if("function"==typeof Object.keys&&0!==Object.keys(e).length)return!1;if("function"==typeof Object.getOwnPropertyNames&&0!==Object.getOwnPropertyNames(e).length)return!1;var n=Object.getOwnPropertySymbols(e);if(1!==n.length||n[0]!==t)return!1;if(!Object.prototype.propertyIsEnumerable.call(e,t))return!1;if("function"==typeof Object.getOwnPropertyDescriptor){var i=Object.getOwnPropertyDescriptor(e,t);if(42!==i.value||!0!==i.enumerable)return!1}return!0}},6410:(e,t,r)=>{"use strict";var n=r(5419);e.exports=function(){return n()&&!!Symbol.toStringTag}},7642:(e,t,r)=>{"use strict";var n=r(8612);e.exports=n.call(Function.call,Object.prototype.hasOwnProperty)},5717:e=>{"function"==typeof Object.create?e.exports=function(e,t){t&&(e.super_=t,e.prototype=Object.create(t.prototype,{constructor:{value:e,enumerable:!1,writable:!0,configurable:!0}}))}:e.exports=function(e,t){if(t){e.super_=t;var r=function(){};r.prototype=t.prototype,e.prototype=new r,e.prototype.constructor=e}}},2584:(e,t,r)=>{"use strict";var n=r(6410)(),i=r(1924)("Object.prototype.toString"),o=function(e){return!(n&&e&&"object"==typeof e&&Symbol.toStringTag in e)&&"[object Arguments]"===i(e)},a=function(e){return!!o(e)||null!==e&&"object"==typeof e&&"number"==typeof e.length&&e.length>=0&&"[object Array]"!==i(e)&&"[object Function]"===i(e.callee)},s=function(){return o(arguments)}();o.isLegacyArguments=a,e.exports=s?o:a},5320:e=>{"use strict";var t,r,n=Function.prototype.toString,i="object"==typeof Reflect&&null!==Reflect&&Reflect.apply;if("function"==typeof i&&"function"==typeof Object.defineProperty)try{t=Object.defineProperty({},"length",{get:function(){throw r}}),r={},i((function(){throw 42}),null,t)}catch(e){e!==r&&(i=null)}else i=null;var o=/^\s*class\b/,a=function(e){try{var t=n.call(e);return o.test(t)}catch(e){return!1}},s=Object.prototype.toString,p="function"==typeof Symbol&&!!Symbol.toStringTag,d="object"==typeof document&&void 0===document.all&&void 0!==document.all?document.all:{};e.exports=i?function(e){if(e===d)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;try{i(e,null,t)}catch(e){if(e!==r)return!1}return!a(e)}:function(e){if(e===d)return!0;if(!e)return!1;if("function"!=typeof e&&"object"!=typeof e)return!1;if("function"==typeof e&&!e.prototype)return!0;if(p)return function(e){try{return!a(e)&&(n.call(e),!0)}catch(e){return!1}}(e);if(a(e))return!1;var t=s.call(e);return"[object Function]"===t||"[object GeneratorFunction]"===t}},8662:(e,t,r)=>{"use strict";var n,i=Object.prototype.toString,o=Function.prototype.toString,a=/^\s*(?:function)?\*/,s=r(6410)(),p=Object.getPrototypeOf;e.exports=function(e){if("function"!=typeof e)return!1;if(a.test(o.call(e)))return!0;if(!s)return"[object GeneratorFunction]"===i.call(e);if(!p)return!1;if(void 0===n){var t=function(){if(!s)return!1;try{return Function("return function*() {}")()}catch(e){}}();n=!!t&&p(t)}return p(e)===n}},5692:(e,t,r)=>{"use strict";var n=r(4029),i=r(3083),o=r(1924),a=o("Object.prototype.toString"),s=r(6410)(),p="undefined"==typeof globalThis?r.g:globalThis,d=i(),c=o("Array.prototype.indexOf",!0)||function(e,t){for(var r=0;r<e.length;r+=1)if(e[r]===t)return r;return-1},l=o("String.prototype.slice"),u={},m=r(882),h=Object.getPrototypeOf;s&&m&&h&&n(d,(function(e){var t=new p[e];if(Symbol.toStringTag in t){var r=h(t),n=m(r,Symbol.toStringTag);if(!n){var i=h(r);n=m(i,Symbol.toStringTag)}u[e]=n.get}}));e.exports=function(e){if(!e||"object"!=typeof e)return!1;if(!s||!(Symbol.toStringTag in e)){var t=l(a(e),8,-1);return c(d,t)>-1}return!!m&&function(e){var t=!1;return n(u,(function(r,n){if(!t)try{t=r.call(e)===n}catch(e){}})),t}(e)}},4155:e=>{var t,r,n=e.exports={};function i(){throw new Error("setTimeout has not been defined")}function o(){throw new Error("clearTimeout has not been defined")}function a(e){if(t===setTimeout)return setTimeout(e,0);if((t===i||!t)&&setTimeout)return t=setTimeout,setTimeout(e,0);try{return t(e,0)}catch(r){try{return t.call(null,e,0)}catch(r){return t.call(this,e,0)}}}!function(){try{t="function"==typeof setTimeout?setTimeout:i}catch(e){t=i}try{r="function"==typeof clearTimeout?clearTimeout:o}catch(e){r=o}}();var s,p=[],d=!1,c=-1;function l(){d&&s&&(d=!1,s.length?p=s.concat(p):c=-1,p.length&&u())}function u(){if(!d){var e=a(l);d=!0;for(var t=p.length;t;){for(s=p,p=[];++c<t;)s&&s[c].run();c=-1,t=p.length}s=null,d=!1,function(e){if(r===clearTimeout)return clearTimeout(e);if((r===o||!r)&&clearTimeout)return r=clearTimeout,clearTimeout(e);try{r(e)}catch(t){try{return r.call(null,e)}catch(t){return r.call(this,e)}}}(e)}}function m(e,t){this.fun=e,this.array=t}function h(){}n.nextTick=function(e){var t=new Array(arguments.length-1);if(arguments.length>1)for(var r=1;r<arguments.length;r++)t[r-1]=arguments[r];p.push(new m(e,t)),1!==p.length||d||a(u)},m.prototype.run=function(){this.fun.apply(null,this.array)},n.title="browser",n.browser=!0,n.env={},n.argv=[],n.version="",n.versions={},n.on=h,n.addListener=h,n.once=h,n.off=h,n.removeListener=h,n.removeAllListeners=h,n.emit=h,n.prependListener=h,n.prependOnceListener=h,n.listeners=function(e){return[]},n.binding=function(e){throw new Error("process.binding is not supported")},n.cwd=function(){return"/"},n.chdir=function(e){throw new Error("process.chdir is not supported")},n.umask=function(){return 0}},2587:e=>{"use strict";function t(e,t){return Object.prototype.hasOwnProperty.call(e,t)}e.exports=function(e,r,n,i){r=r||"&",n=n||"=";var o={};if("string"!=typeof e||0===e.length)return o;var a=/\+/g;e=e.split(r);var s=1e3;i&&"number"==typeof i.maxKeys&&(s=i.maxKeys);var p=e.length;s>0&&p>s&&(p=s);for(var d=0;d<p;++d){var c,l,u,m,h=e[d].replace(a,"%20"),f=h.indexOf(n);f>=0?(c=h.substr(0,f),l=h.substr(f+1)):(c=h,l=""),u=decodeURIComponent(c),m=decodeURIComponent(l),t(o,u)?Array.isArray(o[u])?o[u].push(m):o[u]=[o[u],m]:o[u]=m}return o}},2361:e=>{"use strict";var t=function(e){switch(typeof e){case"string":return e;case"boolean":return e?"true":"false";case"number":return isFinite(e)?e:"";default:return""}};e.exports=function(e,r,n,i){return r=r||"&",n=n||"=",null===e&&(e=void 0),"object"==typeof e?Object.keys(e).map((function(i){var o=encodeURIComponent(t(i))+n;return Array.isArray(e[i])?e[i].map((function(e){return o+encodeURIComponent(t(e))})).join(r):o+encodeURIComponent(t(e[i]))})).join(r):i?encodeURIComponent(t(i))+n+encodeURIComponent(t(e)):""}},7673:(e,t,r)=>{"use strict";t.decode=t.parse=r(2587),t.encode=t.stringify=r(2361)},5666:function(e,t,r){!function(t){"use strict";var r,n=Object.prototype,i=n.hasOwnProperty,o="function"==typeof Symbol?Symbol:{},a=o.iterator||"@@iterator",s=o.asyncIterator||"@@asyncIterator",p=o.toStringTag||"@@toStringTag",d=t.regeneratorRuntime;if(d)e.exports=d;else{(d=t.regeneratorRuntime=e.exports).wrap=v;var c="suspendedStart",l="suspendedYield",u="executing",m="completed",h={},f={};f[a]=function(){return this};var y=Object.getPrototypeOf,g=y&&y(y(O([])));g&&g!==n&&i.call(g,a)&&(f=g);var b=x.prototype=S.prototype=Object.create(f);I.prototype=b.constructor=x,x.constructor=I,x[p]=I.displayName="GeneratorFunction",d.isGeneratorFunction=function(e){var t="function"==typeof e&&e.constructor;return!!t&&(t===I||"GeneratorFunction"===(t.displayName||t.name))},d.mark=function(e){return Object.setPrototypeOf?Object.setPrototypeOf(e,x):(e.__proto__=x,p in e||(e[p]="GeneratorFunction")),e.prototype=Object.create(b),e},d.awrap=function(e){return{__await:e}},k(T.prototype),T.prototype[s]=function(){return this},d.AsyncIterator=T,d.async=function(e,t,r,n){var i=new T(v(e,t,r,n));return d.isGeneratorFunction(t)?i:i.next().then((function(e){return e.done?e.value:i.next()}))},k(b),b[p]="Generator",b[a]=function(){return this},b.toString=function(){return"[object Generator]"},d.keys=function(e){var t=[];for(var r in e)t.push(r);return t.reverse(),function r(){for(;t.length;){var n=t.pop();if(n in e)return r.value=n,r.done=!1,r}return r.done=!0,r}},d.values=O,A.prototype={constructor:A,reset:function(e){if(this.prev=0,this.next=0,this.sent=this._sent=r,this.done=!1,this.delegate=null,this.method="next",this.arg=r,this.tryEntries.forEach($),!e)for(var t in this)"t"===t.charAt(0)&&i.call(this,t)&&!isNaN(+t.slice(1))&&(this[t]=r)},stop:function(){this.done=!0;var e=this.tryEntries[0].completion;if("throw"===e.type)throw e.arg;return this.rval},dispatchException:function(e){if(this.done)throw e;var t=this;function n(n,i){return s.type="throw",s.arg=e,t.next=n,i&&(t.method="next",t.arg=r),!!i}for(var o=this.tryEntries.length-1;o>=0;--o){var a=this.tryEntries[o],s=a.completion;if("root"===a.tryLoc)return n("end");if(a.tryLoc<=this.prev){var p=i.call(a,"catchLoc"),d=i.call(a,"finallyLoc");if(p&&d){if(this.prev<a.catchLoc)return n(a.catchLoc,!0);if(this.prev<a.finallyLoc)return n(a.finallyLoc)}else if(p){if(this.prev<a.catchLoc)return n(a.catchLoc,!0)}else{if(!d)throw new Error("try statement without catch or finally");if(this.prev<a.finallyLoc)return n(a.finallyLoc)}}}},abrupt:function(e,t){for(var r=this.tryEntries.length-1;r>=0;--r){var n=this.tryEntries[r];if(n.tryLoc<=this.prev&&i.call(n,"finallyLoc")&&this.prev<n.finallyLoc){var o=n;break}}o&&("break"===e||"continue"===e)&&o.tryLoc<=t&&t<=o.finallyLoc&&(o=null);var a=o?o.completion:{};return a.type=e,a.arg=t,o?(this.method="next",this.next=o.finallyLoc,h):this.complete(a)},complete:function(e,t){if("throw"===e.type)throw e.arg;return"break"===e.type||"continue"===e.type?this.next=e.arg:"return"===e.type?(this.rval=this.arg=e.arg,this.method="return",this.next="end"):"normal"===e.type&&t&&(this.next=t),h},finish:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.finallyLoc===e)return this.complete(r.completion,r.afterLoc),$(r),h}},catch:function(e){for(var t=this.tryEntries.length-1;t>=0;--t){var r=this.tryEntries[t];if(r.tryLoc===e){var n=r.completion;if("throw"===n.type){var i=n.arg;$(r)}return i}}throw new Error("illegal catch attempt")},delegateYield:function(e,t,n){return this.delegate={iterator:O(e),resultName:t,nextLoc:n},"next"===this.method&&(this.arg=r),h}}}function v(e,t,r,n){var i=t&&t.prototype instanceof S?t:S,o=Object.create(i.prototype),a=new A(n||[]);return o._invoke=function(e,t,r){var n=c;return function(i,o){if(n===u)throw new Error("Generator is already running");if(n===m){if("throw"===i)throw o;return P()}for(r.method=i,r.arg=o;;){var a=r.delegate;if(a){var s=R(a,r);if(s){if(s===h)continue;return s}}if("next"===r.method)r.sent=r._sent=r.arg;else if("throw"===r.method){if(n===c)throw n=m,r.arg;r.dispatchException(r.arg)}else"return"===r.method&&r.abrupt("return",r.arg);n=u;var p=w(e,t,r);if("normal"===p.type){if(n=r.done?m:l,p.arg===h)continue;return{value:p.arg,done:r.done}}"throw"===p.type&&(n=m,r.method="throw",r.arg=p.arg)}}}(e,r,a),o}function w(e,t,r){try{return{type:"normal",arg:e.call(t,r)}}catch(e){return{type:"throw",arg:e}}}function S(){}function I(){}function x(){}function k(e){["next","throw","return"].forEach((function(t){e[t]=function(e){return this._invoke(t,e)}}))}function T(e){function r(t,n,o,a){var s=w(e[t],e,n);if("throw"!==s.type){var p=s.arg,d=p.value;return d&&"object"==typeof d&&i.call(d,"__await")?Promise.resolve(d.__await).then((function(e){r("next",e,o,a)}),(function(e){r("throw",e,o,a)})):Promise.resolve(d).then((function(e){p.value=e,o(p)}),a)}a(s.arg)}var n;"object"==typeof t.process&&t.process.domain&&(r=t.process.domain.bind(r)),this._invoke=function(e,t){function i(){return new Promise((function(n,i){r(e,t,n,i)}))}return n=n?n.then(i,i):i()}}function R(e,t){var n=e.iterator[t.method];if(n===r){if(t.delegate=null,"throw"===t.method){if(e.iterator.return&&(t.method="return",t.arg=r,R(e,t),"throw"===t.method))return h;t.method="throw",t.arg=new TypeError("The iterator does not provide a 'throw' method")}return h}var i=w(n,e.iterator,t.arg);if("throw"===i.type)return t.method="throw",t.arg=i.arg,t.delegate=null,h;var o=i.arg;return o?o.done?(t[e.resultName]=o.value,t.next=e.nextLoc,"return"!==t.method&&(t.method="next",t.arg=r),t.delegate=null,h):o:(t.method="throw",t.arg=new TypeError("iterator result is not an object"),t.delegate=null,h)}function C(e){var t={tryLoc:e[0]};1 in e&&(t.catchLoc=e[1]),2 in e&&(t.finallyLoc=e[2],t.afterLoc=e[3]),this.tryEntries.push(t)}function $(e){var t=e.completion||{};t.type="normal",delete t.arg,e.completion=t}function A(e){this.tryEntries=[{tryLoc:"root"}],e.forEach(C,this),this.reset(!0)}function O(e){if(e){var t=e[a];if(t)return t.call(e);if("function"==typeof e.next)return e;if(!isNaN(e.length)){var n=-1,o=function t(){for(;++n<e.length;)if(i.call(e,n))return t.value=e[n],t.done=!1,t;return t.value=r,t.done=!0,t};return o.next=o}}return{next:P}}function P(){return{value:r,done:!0}}}("object"==typeof r.g?r.g:"object"==typeof window?window:"object"==typeof self?self:this)},2511:function(e,t,r){var n;/*! https://mths.be/punycode v1.3.2 by @mathias */e=r.nmd(e),function(i){t&&t.nodeType,e&&e.nodeType;var o="object"==typeof r.g&&r.g;o.global!==o&&o.window!==o&&o.self;var a,s=2147483647,p=36,d=/^xn--/,c=/[^\x20-\x7E]/,l=/[\x2E\u3002\uFF0E\uFF61]/g,u={overflow:"Overflow: input needs wider integers to process","not-basic":"Illegal input >= 0x80 (not a basic code point)","invalid-input":"Invalid input"},m=Math.floor,h=String.fromCharCode;function f(e){throw RangeError(u[e])}function y(e,t){for(var r=e.length,n=[];r--;)n[r]=t(e[r]);return n}function g(e,t){var r=e.split("@"),n="";return r.length>1&&(n=r[0]+"@",e=r[1]),n+y((e=e.replace(l,".")).split("."),t).join(".")}function b(e){for(var t,r,n=[],i=0,o=e.length;i<o;)(t=e.charCodeAt(i++))>=55296&&t<=56319&&i<o?56320==(64512&(r=e.charCodeAt(i++)))?n.push(((1023&t)<<10)+(1023&r)+65536):(n.push(t),i--):n.push(t);return n}function v(e){return y(e,(function(e){var t="";return e>65535&&(t+=h((e-=65536)>>>10&1023|55296),e=56320|1023&e),t+=h(e)})).join("")}function w(e,t){return e+22+75*(e<26)-((0!=t)<<5)}function S(e,t,r){var n=0;for(e=r?m(e/700):e>>1,e+=m(e/t);e>455;n+=p)e=m(e/35);return m(n+36*e/(e+38))}function I(e){var t,r,n,i,o,a,d,c,l,u,h,y=[],g=e.length,b=0,w=128,I=72;for((r=e.lastIndexOf("-"))<0&&(r=0),n=0;n<r;++n)e.charCodeAt(n)>=128&&f("not-basic"),y.push(e.charCodeAt(n));for(i=r>0?r+1:0;i<g;){for(o=b,a=1,d=p;i>=g&&f("invalid-input"),((c=(h=e.charCodeAt(i++))-48<10?h-22:h-65<26?h-65:h-97<26?h-97:p)>=p||c>m((s-b)/a))&&f("overflow"),b+=c*a,!(c<(l=d<=I?1:d>=I+26?26:d-I));d+=p)a>m(s/(u=p-l))&&f("overflow"),a*=u;I=S(b-o,t=y.length+1,0==o),m(b/t)>s-w&&f("overflow"),w+=m(b/t),b%=t,y.splice(b++,0,w)}return v(y)}function x(e){var t,r,n,i,o,a,d,c,l,u,y,g,v,I,x,k=[];for(g=(e=b(e)).length,t=128,r=0,o=72,a=0;a<g;++a)(y=e[a])<128&&k.push(h(y));for(n=i=k.length,i&&k.push("-");n<g;){for(d=s,a=0;a<g;++a)(y=e[a])>=t&&y<d&&(d=y);for(d-t>m((s-r)/(v=n+1))&&f("overflow"),r+=(d-t)*v,t=d,a=0;a<g;++a)if((y=e[a])<t&&++r>s&&f("overflow"),y==t){for(c=r,l=p;!(c<(u=l<=o?1:l>=o+26?26:l-o));l+=p)x=c-u,I=p-u,k.push(h(w(u+x%I,0))),c=m(x/I);k.push(h(w(c,0))),o=S(r,v,n==i),r=0,++n}++r,++t}return k.join("")}a={version:"1.3.2",ucs2:{decode:b,encode:v},decode:I,encode:x,toASCII:function(e){return g(e,(function(e){return c.test(e)?"xn--"+x(e):e}))},toUnicode:function(e){return g(e,(function(e){return d.test(e)?I(e.slice(4).toLowerCase()):e}))}},void 0===(n=function(){return a}.call(t,r,t,e))||(e.exports=n)}()},8575:(e,t,r)=>{"use strict";var n=r(2511),i=r(2502);function o(){this.protocol=null,this.slashes=null,this.auth=null,this.host=null,this.port=null,this.hostname=null,this.hash=null,this.search=null,this.query=null,this.pathname=null,this.path=null,this.href=null}t.Qc=v,t.WU=function(e){i.isString(e)&&(e=v(e));return e instanceof o?e.format():o.prototype.format.call(e)};var a=/^([a-z0-9.+-]+:)/i,s=/:[0-9]*$/,p=/^(\/\/?(?!\/)[^\?\s]*)(\?[^\s]*)?$/,d=["{","}","|","\\","^","`"].concat(["<",">",'"',"`"," ","\r","\n","\t"]),c=["'"].concat(d),l=["%","/","?",";","#"].concat(c),u=["/","?","#"],m=/^[+a-z0-9A-Z_-]{0,63}$/,h=/^([+a-z0-9A-Z_-]{0,63})(.*)$/,f={javascript:!0,"javascript:":!0},y={javascript:!0,"javascript:":!0},g={http:!0,https:!0,ftp:!0,gopher:!0,file:!0,"http:":!0,"https:":!0,"ftp:":!0,"gopher:":!0,"file:":!0},b=r(7673);function v(e,t,r){if(e&&i.isObject(e)&&e instanceof o)return e;var n=new o;return n.parse(e,t,r),n}o.prototype.parse=function(e,t,r){if(!i.isString(e))throw new TypeError("Parameter 'url' must be a string, not "+typeof e);var o=e.indexOf("?"),s=-1!==o&&o<e.indexOf("#")?"?":"#",d=e.split(s);d[0]=d[0].replace(/\\/g,"/");var v=e=d.join(s);if(v=v.trim(),!r&&1===e.split("#").length){var w=p.exec(v);if(w)return this.path=v,this.href=v,this.pathname=w[1],w[2]?(this.search=w[2],this.query=t?b.parse(this.search.substr(1)):this.search.substr(1)):t&&(this.search="",this.query={}),this}var S=a.exec(v);if(S){var I=(S=S[0]).toLowerCase();this.protocol=I,v=v.substr(S.length)}if(r||S||v.match(/^\/\/[^@\/]+@[^@\/]+/)){var x="//"===v.substr(0,2);!x||S&&y[S]||(v=v.substr(2),this.slashes=!0)}if(!y[S]&&(x||S&&!g[S])){for(var k,T,R=-1,C=0;C<u.length;C++){-1!==($=v.indexOf(u[C]))&&(-1===R||$<R)&&(R=$)}-1!==(T=-1===R?v.lastIndexOf("@"):v.lastIndexOf("@",R))&&(k=v.slice(0,T),v=v.slice(T+1),this.auth=decodeURIComponent(k)),R=-1;for(C=0;C<l.length;C++){var $;-1!==($=v.indexOf(l[C]))&&(-1===R||$<R)&&(R=$)}-1===R&&(R=v.length),this.host=v.slice(0,R),v=v.slice(R),this.parseHost(),this.hostname=this.hostname||"";var A="["===this.hostname[0]&&"]"===this.hostname[this.hostname.length-1];if(!A)for(var O=this.hostname.split(/\./),P=(C=0,O.length);C<P;C++){var D=O[C];if(D&&!D.match(m)){for(var N="",j=0,E=D.length;j<E;j++)D.charCodeAt(j)>127?N+="x":N+=D[j];if(!N.match(m)){var M=O.slice(0,C),F=O.slice(C+1),q=D.match(h);q&&(M.push(q[1]),F.unshift(q[2])),F.length&&(v="/"+F.join(".")+v),this.hostname=M.join(".");break}}}this.hostname.length>255?this.hostname="":this.hostname=this.hostname.toLowerCase(),A||(this.hostname=n.toASCII(this.hostname));var L=this.port?":"+this.port:"",B=this.hostname||"";this.host=B+L,this.href+=this.host,A&&(this.hostname=this.hostname.substr(1,this.hostname.length-2),"/"!==v[0]&&(v="/"+v))}if(!f[I])for(C=0,P=c.length;C<P;C++){var U=c[C];if(-1!==v.indexOf(U)){var W=encodeURIComponent(U);W===U&&(W=escape(U)),v=v.split(U).join(W)}}var H=v.indexOf("#");-1!==H&&(this.hash=v.substr(H),v=v.slice(0,H));var _=v.indexOf("?");if(-1!==_?(this.search=v.substr(_),this.query=v.substr(_+1),t&&(this.query=b.parse(this.query)),v=v.slice(0,_)):t&&(this.search="",this.query={}),v&&(this.pathname=v),g[I]&&this.hostname&&!this.pathname&&(this.pathname="/"),this.pathname||this.search){L=this.pathname||"";var V=this.search||"";this.path=L+V}return this.href=this.format(),this},o.prototype.format=function(){var e=this.auth||"";e&&(e=(e=encodeURIComponent(e)).replace(/%3A/i,":"),e+="@");var t=this.protocol||"",r=this.pathname||"",n=this.hash||"",o=!1,a="";this.host?o=e+this.host:this.hostname&&(o=e+(-1===this.hostname.indexOf(":")?this.hostname:"["+this.hostname+"]"),this.port&&(o+=":"+this.port)),this.query&&i.isObject(this.query)&&Object.keys(this.query).length&&(a=b.stringify(this.query));var s=this.search||a&&"?"+a||"";return t&&":"!==t.substr(-1)&&(t+=":"),this.slashes||(!t||g[t])&&!1!==o?(o="//"+(o||""),r&&"/"!==r.charAt(0)&&(r="/"+r)):o||(o=""),n&&"#"!==n.charAt(0)&&(n="#"+n),s&&"?"!==s.charAt(0)&&(s="?"+s),t+o+(r=r.replace(/[?#]/g,(function(e){return encodeURIComponent(e)})))+(s=s.replace("#","%23"))+n},o.prototype.resolve=function(e){return this.resolveObject(v(e,!1,!0)).format()},o.prototype.resolveObject=function(e){if(i.isString(e)){var t=new o;t.parse(e,!1,!0),e=t}for(var r=new o,n=Object.keys(this),a=0;a<n.length;a++){var s=n[a];r[s]=this[s]}if(r.hash=e.hash,""===e.href)return r.href=r.format(),r;if(e.slashes&&!e.protocol){for(var p=Object.keys(e),d=0;d<p.length;d++){var c=p[d];"protocol"!==c&&(r[c]=e[c])}return g[r.protocol]&&r.hostname&&!r.pathname&&(r.path=r.pathname="/"),r.href=r.format(),r}if(e.protocol&&e.protocol!==r.protocol){if(!g[e.protocol]){for(var l=Object.keys(e),u=0;u<l.length;u++){var m=l[u];r[m]=e[m]}return r.href=r.format(),r}if(r.protocol=e.protocol,e.host||y[e.protocol])r.pathname=e.pathname;else{for(var h=(e.pathname||"").split("/");h.length&&!(e.host=h.shift()););e.host||(e.host=""),e.hostname||(e.hostname=""),""!==h[0]&&h.unshift(""),h.length<2&&h.unshift(""),r.pathname=h.join("/")}if(r.search=e.search,r.query=e.query,r.host=e.host||"",r.auth=e.auth,r.hostname=e.hostname||e.host,r.port=e.port,r.pathname||r.search){var f=r.pathname||"",b=r.search||"";r.path=f+b}return r.slashes=r.slashes||e.slashes,r.href=r.format(),r}var v=r.pathname&&"/"===r.pathname.charAt(0),w=e.host||e.pathname&&"/"===e.pathname.charAt(0),S=w||v||r.host&&e.pathname,I=S,x=r.pathname&&r.pathname.split("/")||[],k=(h=e.pathname&&e.pathname.split("/")||[],r.protocol&&!g[r.protocol]);if(k&&(r.hostname="",r.port=null,r.host&&(""===x[0]?x[0]=r.host:x.unshift(r.host)),r.host="",e.protocol&&(e.hostname=null,e.port=null,e.host&&(""===h[0]?h[0]=e.host:h.unshift(e.host)),e.host=null),S=S&&(""===h[0]||""===x[0])),w)r.host=e.host||""===e.host?e.host:r.host,r.hostname=e.hostname||""===e.hostname?e.hostname:r.hostname,r.search=e.search,r.query=e.query,x=h;else if(h.length)x||(x=[]),x.pop(),x=x.concat(h),r.search=e.search,r.query=e.query;else if(!i.isNullOrUndefined(e.search)){if(k)r.hostname=r.host=x.shift(),(A=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@"))&&(r.auth=A.shift(),r.host=r.hostname=A.shift());return r.search=e.search,r.query=e.query,i.isNull(r.pathname)&&i.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.href=r.format(),r}if(!x.length)return r.pathname=null,r.search?r.path="/"+r.search:r.path=null,r.href=r.format(),r;for(var T=x.slice(-1)[0],R=(r.host||e.host||x.length>1)&&("."===T||".."===T)||""===T,C=0,$=x.length;$>=0;$--)"."===(T=x[$])?x.splice($,1):".."===T?(x.splice($,1),C++):C&&(x.splice($,1),C--);if(!S&&!I)for(;C--;C)x.unshift("..");!S||""===x[0]||x[0]&&"/"===x[0].charAt(0)||x.unshift(""),R&&"/"!==x.join("/").substr(-1)&&x.push("");var A,O=""===x[0]||x[0]&&"/"===x[0].charAt(0);k&&(r.hostname=r.host=O?"":x.length?x.shift():"",(A=!!(r.host&&r.host.indexOf("@")>0)&&r.host.split("@"))&&(r.auth=A.shift(),r.host=r.hostname=A.shift()));return(S=S||r.host&&x.length)&&!O&&x.unshift(""),x.length?r.pathname=x.join("/"):(r.pathname=null,r.path=null),i.isNull(r.pathname)&&i.isNull(r.search)||(r.path=(r.pathname?r.pathname:"")+(r.search?r.search:"")),r.auth=e.auth||r.auth,r.slashes=r.slashes||e.slashes,r.href=r.format(),r},o.prototype.parseHost=function(){var e=this.host,t=s.exec(e);t&&(":"!==(t=t[0])&&(this.port=t.substr(1)),e=e.substr(0,e.length-t.length)),e&&(this.hostname=e)}},2502:e=>{"use strict";e.exports={isString:function(e){return"string"==typeof e},isObject:function(e){return"object"==typeof e&&null!==e},isNull:function(e){return null===e},isNullOrUndefined:function(e){return null==e}}},384:e=>{e.exports=function(e){return e&&"object"==typeof e&&"function"==typeof e.copy&&"function"==typeof e.fill&&"function"==typeof e.readUInt8}},5955:(e,t,r)=>{"use strict";var n=r(2584),i=r(8662),o=r(6430),a=r(5692);function s(e){return e.call.bind(e)}var p="undefined"!=typeof BigInt,d="undefined"!=typeof Symbol,c=s(Object.prototype.toString),l=s(Number.prototype.valueOf),u=s(String.prototype.valueOf),m=s(Boolean.prototype.valueOf);if(p)var h=s(BigInt.prototype.valueOf);if(d)var f=s(Symbol.prototype.valueOf);function y(e,t){if("object"!=typeof e)return!1;try{return t(e),!0}catch(e){return!1}}function g(e){return"[object Map]"===c(e)}function b(e){return"[object Set]"===c(e)}function v(e){return"[object WeakMap]"===c(e)}function w(e){return"[object WeakSet]"===c(e)}function S(e){return"[object ArrayBuffer]"===c(e)}function I(e){return"undefined"!=typeof ArrayBuffer&&(S.working?S(e):e instanceof ArrayBuffer)}function x(e){return"[object DataView]"===c(e)}function k(e){return"undefined"!=typeof DataView&&(x.working?x(e):e instanceof DataView)}t.isArgumentsObject=n,t.isGeneratorFunction=i,t.isTypedArray=a,t.isPromise=function(e){return"undefined"!=typeof Promise&&e instanceof Promise||null!==e&&"object"==typeof e&&"function"==typeof e.then&&"function"==typeof e.catch},t.isArrayBufferView=function(e){return"undefined"!=typeof ArrayBuffer&&ArrayBuffer.isView?ArrayBuffer.isView(e):a(e)||k(e)},t.isUint8Array=function(e){return"Uint8Array"===o(e)},t.isUint8ClampedArray=function(e){return"Uint8ClampedArray"===o(e)},t.isUint16Array=function(e){return"Uint16Array"===o(e)},t.isUint32Array=function(e){return"Uint32Array"===o(e)},t.isInt8Array=function(e){return"Int8Array"===o(e)},t.isInt16Array=function(e){return"Int16Array"===o(e)},t.isInt32Array=function(e){return"Int32Array"===o(e)},t.isFloat32Array=function(e){return"Float32Array"===o(e)},t.isFloat64Array=function(e){return"Float64Array"===o(e)},t.isBigInt64Array=function(e){return"BigInt64Array"===o(e)},t.isBigUint64Array=function(e){return"BigUint64Array"===o(e)},g.working="undefined"!=typeof Map&&g(new Map),t.isMap=function(e){return"undefined"!=typeof Map&&(g.working?g(e):e instanceof Map)},b.working="undefined"!=typeof Set&&b(new Set),t.isSet=function(e){return"undefined"!=typeof Set&&(b.working?b(e):e instanceof Set)},v.working="undefined"!=typeof WeakMap&&v(new WeakMap),t.isWeakMap=function(e){return"undefined"!=typeof WeakMap&&(v.working?v(e):e instanceof WeakMap)},w.working="undefined"!=typeof WeakSet&&w(new WeakSet),t.isWeakSet=function(e){return w(e)},S.working="undefined"!=typeof ArrayBuffer&&S(new ArrayBuffer),t.isArrayBuffer=I,x.working="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof DataView&&x(new DataView(new ArrayBuffer(1),0,1)),t.isDataView=k;var T="undefined"!=typeof SharedArrayBuffer?SharedArrayBuffer:void 0;function R(e){return"[object SharedArrayBuffer]"===c(e)}function C(e){return void 0!==T&&(void 0===R.working&&(R.working=R(new T)),R.working?R(e):e instanceof T)}function $(e){return y(e,l)}function A(e){return y(e,u)}function O(e){return y(e,m)}function P(e){return p&&y(e,h)}function D(e){return d&&y(e,f)}t.isSharedArrayBuffer=C,t.isAsyncFunction=function(e){return"[object AsyncFunction]"===c(e)},t.isMapIterator=function(e){return"[object Map Iterator]"===c(e)},t.isSetIterator=function(e){return"[object Set Iterator]"===c(e)},t.isGeneratorObject=function(e){return"[object Generator]"===c(e)},t.isWebAssemblyCompiledModule=function(e){return"[object WebAssembly.Module]"===c(e)},t.isNumberObject=$,t.isStringObject=A,t.isBooleanObject=O,t.isBigIntObject=P,t.isSymbolObject=D,t.isBoxedPrimitive=function(e){return $(e)||A(e)||O(e)||P(e)||D(e)},t.isAnyArrayBuffer=function(e){return"undefined"!=typeof Uint8Array&&(I(e)||C(e))},["isProxy","isExternal","isModuleNamespaceObject"].forEach((function(e){Object.defineProperty(t,e,{enumerable:!1,value:function(){throw new Error(e+" is not supported in userland")}})}))},1588:(e,t,r)=>{var n=r(4155),i=Object.getOwnPropertyDescriptors||function(e){for(var t=Object.keys(e),r={},n=0;n<t.length;n++)r[t[n]]=Object.getOwnPropertyDescriptor(e,t[n]);return r},o=/%[sdj%]/g;t.format=function(e){if(!v(e)){for(var t=[],r=0;r<arguments.length;r++)t.push(d(arguments[r]));return t.join(" ")}r=1;for(var n=arguments,i=n.length,a=String(e).replace(o,(function(e){if("%%"===e)return"%";if(r>=i)return e;switch(e){case"%s":return String(n[r++]);case"%d":return Number(n[r++]);case"%j":try{return JSON.stringify(n[r++])}catch(e){return"[Circular]"}default:return e}})),s=n[r];r<i;s=n[++r])g(s)||!I(s)?a+=" "+s:a+=" "+d(s);return a},t.deprecate=function(e,r){if(void 0!==n&&!0===n.noDeprecation)return e;if(void 0===n)return function(){return t.deprecate(e,r).apply(this,arguments)};var i=!1;return function(){if(!i){if(n.throwDeprecation)throw new Error(r);n.traceDeprecation?console.trace(r):console.error(r),i=!0}return e.apply(this,arguments)}};var a={},s=/^$/;if(n.env.NODE_DEBUG){var p=n.env.NODE_DEBUG;p=p.replace(/[|\\{}()[\]^$+?.]/g,"\\$&").replace(/\*/g,".*").replace(/,/g,"$|^").toUpperCase(),s=new RegExp("^"+p+"$","i")}function d(e,r){var n={seen:[],stylize:l};return arguments.length>=3&&(n.depth=arguments[2]),arguments.length>=4&&(n.colors=arguments[3]),y(r)?n.showHidden=r:r&&t._extend(n,r),w(n.showHidden)&&(n.showHidden=!1),w(n.depth)&&(n.depth=2),w(n.colors)&&(n.colors=!1),w(n.customInspect)&&(n.customInspect=!0),n.colors&&(n.stylize=c),u(n,e,n.depth)}function c(e,t){var r=d.styles[t];return r?"["+d.colors[r][0]+"m"+e+"["+d.colors[r][1]+"m":e}function l(e,t){return e}function u(e,r,n){if(e.customInspect&&r&&T(r.inspect)&&r.inspect!==t.inspect&&(!r.constructor||r.constructor.prototype!==r)){var i=r.inspect(n,e);return v(i)||(i=u(e,i,n)),i}var o=function(e,t){if(w(t))return e.stylize("undefined","undefined");if(v(t)){var r="'"+JSON.stringify(t).replace(/^"|"$/g,"").replace(/'/g,"\\'").replace(/\\"/g,'"')+"'";return e.stylize(r,"string")}if(b(t))return e.stylize(""+t,"number");if(y(t))return e.stylize(""+t,"boolean");if(g(t))return e.stylize("null","null")}(e,r);if(o)return o;var a=Object.keys(r),s=function(e){var t={};return e.forEach((function(e,r){t[e]=!0})),t}(a);if(e.showHidden&&(a=Object.getOwnPropertyNames(r)),k(r)&&(a.indexOf("message")>=0||a.indexOf("description")>=0))return m(r);if(0===a.length){if(T(r)){var p=r.name?": "+r.name:"";return e.stylize("[Function"+p+"]","special")}if(S(r))return e.stylize(RegExp.prototype.toString.call(r),"regexp");if(x(r))return e.stylize(Date.prototype.toString.call(r),"date");if(k(r))return m(r)}var d,c="",l=!1,I=["{","}"];(f(r)&&(l=!0,I=["[","]"]),T(r))&&(c=" [Function"+(r.name?": "+r.name:"")+"]");return S(r)&&(c=" "+RegExp.prototype.toString.call(r)),x(r)&&(c=" "+Date.prototype.toUTCString.call(r)),k(r)&&(c=" "+m(r)),0!==a.length||l&&0!=r.length?n<0?S(r)?e.stylize(RegExp.prototype.toString.call(r),"regexp"):e.stylize("[Object]","special"):(e.seen.push(r),d=l?function(e,t,r,n,i){for(var o=[],a=0,s=t.length;a<s;++a)O(t,String(a))?o.push(h(e,t,r,n,String(a),!0)):o.push("");return i.forEach((function(i){i.match(/^\d+$/)||o.push(h(e,t,r,n,i,!0))})),o}(e,r,n,s,a):a.map((function(t){return h(e,r,n,s,t,l)})),e.seen.pop(),function(e,t,r){if(e.reduce((function(e,t){return t.indexOf("\n")>=0&&0,e+t.replace(/\u001b\[\d\d?m/g,"").length+1}),0)>60)return r[0]+(""===t?"":t+"\n ")+" "+e.join(",\n ")+" "+r[1];return r[0]+t+" "+e.join(", ")+" "+r[1]}(d,c,I)):I[0]+c+I[1]}function m(e){return"["+Error.prototype.toString.call(e)+"]"}function h(e,t,r,n,i,o){var a,s,p;if((p=Object.getOwnPropertyDescriptor(t,i)||{value:t[i]}).get?s=p.set?e.stylize("[Getter/Setter]","special"):e.stylize("[Getter]","special"):p.set&&(s=e.stylize("[Setter]","special")),O(n,i)||(a="["+i+"]"),s||(e.seen.indexOf(p.value)<0?(s=g(r)?u(e,p.value,null):u(e,p.value,r-1)).indexOf("\n")>-1&&(s=o?s.split("\n").map((function(e){return" "+e})).join("\n").substr(2):"\n"+s.split("\n").map((function(e){return" "+e})).join("\n")):s=e.stylize("[Circular]","special")),w(a)){if(o&&i.match(/^\d+$/))return s;(a=JSON.stringify(""+i)).match(/^"([a-zA-Z_][a-zA-Z_0-9]*)"$/)?(a=a.substr(1,a.length-2),a=e.stylize(a,"name")):(a=a.replace(/'/g,"\\'").replace(/\\"/g,'"').replace(/(^"|"$)/g,"'"),a=e.stylize(a,"string"))}return a+": "+s}function f(e){return Array.isArray(e)}function y(e){return"boolean"==typeof e}function g(e){return null===e}function b(e){return"number"==typeof e}function v(e){return"string"==typeof e}function w(e){return void 0===e}function S(e){return I(e)&&"[object RegExp]"===R(e)}function I(e){return"object"==typeof e&&null!==e}function x(e){return I(e)&&"[object Date]"===R(e)}function k(e){return I(e)&&("[object Error]"===R(e)||e instanceof Error)}function T(e){return"function"==typeof e}function R(e){return Object.prototype.toString.call(e)}function C(e){return e<10?"0"+e.toString(10):e.toString(10)}t.debuglog=function(e){if(e=e.toUpperCase(),!a[e])if(s.test(e)){var r=n.pid;a[e]=function(){var n=t.format.apply(t,arguments);console.error("%s %d: %s",e,r,n)}}else a[e]=function(){};return a[e]},t.inspect=d,d.colors={bold:[1,22],italic:[3,23],underline:[4,24],inverse:[7,27],white:[37,39],grey:[90,39],black:[30,39],blue:[34,39],cyan:[36,39],green:[32,39],magenta:[35,39],red:[31,39],yellow:[33,39]},d.styles={special:"cyan",number:"yellow",boolean:"yellow",undefined:"grey",null:"bold",string:"green",date:"magenta",regexp:"red"},t.types=r(5955),t.isArray=f,t.isBoolean=y,t.isNull=g,t.isNullOrUndefined=function(e){return null==e},t.isNumber=b,t.isString=v,t.isSymbol=function(e){return"symbol"==typeof e},t.isUndefined=w,t.isRegExp=S,t.types.isRegExp=S,t.isObject=I,t.isDate=x,t.types.isDate=x,t.isError=k,t.types.isNativeError=k,t.isFunction=T,t.isPrimitive=function(e){return null===e||"boolean"==typeof e||"number"==typeof e||"string"==typeof e||"symbol"==typeof e||void 0===e},t.isBuffer=r(384);var $=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];function A(){var e=new Date,t=[C(e.getHours()),C(e.getMinutes()),C(e.getSeconds())].join(":");return[e.getDate(),$[e.getMonth()],t].join(" ")}function O(e,t){return Object.prototype.hasOwnProperty.call(e,t)}t.log=function(){console.log("%s - %s",A(),t.format.apply(t,arguments))},t.inherits=r(5717),t._extend=function(e,t){if(!t||!I(t))return e;for(var r=Object.keys(t),n=r.length;n--;)e[r[n]]=t[r[n]];return e};var P="undefined"!=typeof Symbol?Symbol("util.promisify.custom"):void 0;function D(e,t){if(!e){var r=new Error("Promise was rejected with a falsy value");r.reason=e,e=r}return t(e)}t.promisify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');if(P&&e[P]){var t;if("function"!=typeof(t=e[P]))throw new TypeError('The "util.promisify.custom" argument must be of type Function');return Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),t}function t(){for(var t,r,n=new Promise((function(e,n){t=e,r=n})),i=[],o=0;o<arguments.length;o++)i.push(arguments[o]);i.push((function(e,n){e?r(e):t(n)}));try{e.apply(this,i)}catch(e){r(e)}return n}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),P&&Object.defineProperty(t,P,{value:t,enumerable:!1,writable:!1,configurable:!0}),Object.defineProperties(t,i(e))},t.promisify.custom=P,t.callbackify=function(e){if("function"!=typeof e)throw new TypeError('The "original" argument must be of type Function');function t(){for(var t=[],r=0;r<arguments.length;r++)t.push(arguments[r]);var i=t.pop();if("function"!=typeof i)throw new TypeError("The last argument must be of type Function");var o=this,a=function(){return i.apply(o,arguments)};e.apply(this,t).then((function(e){n.nextTick(a.bind(null,null,e))}),(function(e){n.nextTick(D.bind(null,e,a))}))}return Object.setPrototypeOf(t,Object.getPrototypeOf(e)),Object.defineProperties(t,i(e)),t}},6430:(e,t,r)=>{"use strict";var n=r(4029),i=r(3083),o=r(1924),a=o("Object.prototype.toString"),s=r(6410)(),p="undefined"==typeof globalThis?r.g:globalThis,d=i(),c=o("String.prototype.slice"),l={},u=r(882),m=Object.getPrototypeOf;s&&u&&m&&n(d,(function(e){if("function"==typeof p[e]){var t=new p[e];if(Symbol.toStringTag in t){var r=m(t),n=u(r,Symbol.toStringTag);if(!n){var i=m(r);n=u(i,Symbol.toStringTag)}l[e]=n.get}}}));var h=r(5692);e.exports=function(e){return!!h(e)&&(s&&Symbol.toStringTag in e?function(e){var t=!1;return n(l,(function(r,n){if(!t)try{var i=r.call(e);i===n&&(t=i)}catch(e){}})),t}(e):c(a(e),8,-1))}},4162:e=>{"use strict";e.exports=function(e,t,r){window.criRequest(t,r)}},3423:()=>{},8532:()=>{},4782:()=>{},3083:(e,t,r)=>{"use strict";var n=["BigInt64Array","BigUint64Array","Float32Array","Float64Array","Int16Array","Int32Array","Int8Array","Uint16Array","Uint32Array","Uint8Array","Uint8ClampedArray"],i="undefined"==typeof globalThis?r.g:globalThis;e.exports=function(){for(var e=[],t=0;t<n.length;t++)"function"==typeof i[n[t]]&&(e[e.length]=n[t]);return e}},882:(e,t,r)=>{"use strict";var n=r(210)("%Object.getOwnPropertyDescriptor%",!0);if(n)try{n([],"length")}catch(e){n=null}e.exports=n},4203:e=>{"use strict";e.exports=JSON.parse('{"version":{"major":"1","minor":"3"},"domains":[{"domain":"Accessibility","experimental":true,"dependencies":["DOM"],"types":[{"id":"AXNodeId","description":"Unique accessibility node identifier.","type":"string"},{"id":"AXValueType","description":"Enum of possible property types.","type":"string","enum":["boolean","tristate","booleanOrUndefined","idref","idrefList","integer","node","nodeList","number","string","computedString","token","tokenList","domRelation","role","internalRole","valueUndefined"]},{"id":"AXValueSourceType","description":"Enum of possible property sources.","type":"string","enum":["attribute","implicit","style","contents","placeholder","relatedElement"]},{"id":"AXValueNativeSourceType","description":"Enum of possible native property sources (as a subtype of a particular AXValueSourceType).","type":"string","enum":["description","figcaption","label","labelfor","labelwrapped","legend","rubyannotation","tablecaption","title","other"]},{"id":"AXValueSource","description":"A single source for a computed AX property.","type":"object","properties":[{"name":"type","description":"What type of source this is.","$ref":"AXValueSourceType"},{"name":"value","description":"The value of this property source.","optional":true,"$ref":"AXValue"},{"name":"attribute","description":"The name of the relevant attribute, if any.","optional":true,"type":"string"},{"name":"attributeValue","description":"The value of the relevant attribute, if any.","optional":true,"$ref":"AXValue"},{"name":"superseded","description":"Whether this source is superseded by a higher priority source.","optional":true,"type":"boolean"},{"name":"nativeSource","description":"The native markup source for this value, e.g. a `<label>` element.","optional":true,"$ref":"AXValueNativeSourceType"},{"name":"nativeSourceValue","description":"The value, such as a node or node list, of the native source.","optional":true,"$ref":"AXValue"},{"name":"invalid","description":"Whether the value for this property is invalid.","optional":true,"type":"boolean"},{"name":"invalidReason","description":"Reason for the value being invalid, if it is.","optional":true,"type":"string"}]},{"id":"AXRelatedNode","type":"object","properties":[{"name":"backendDOMNodeId","description":"The BackendNodeId of the related DOM node.","$ref":"DOM.BackendNodeId"},{"name":"idref","description":"The IDRef value provided, if any.","optional":true,"type":"string"},{"name":"text","description":"The text alternative of this node in the current context.","optional":true,"type":"string"}]},{"id":"AXProperty","type":"object","properties":[{"name":"name","description":"The name of this property.","$ref":"AXPropertyName"},{"name":"value","description":"The value of this property.","$ref":"AXValue"}]},{"id":"AXValue","description":"A single computed AX property.","type":"object","properties":[{"name":"type","description":"The type of this value.","$ref":"AXValueType"},{"name":"value","description":"The computed value of this property.","optional":true,"type":"any"},{"name":"relatedNodes","description":"One or more related nodes, if applicable.","optional":true,"type":"array","items":{"$ref":"AXRelatedNode"}},{"name":"sources","description":"The sources which contributed to the computation of this property.","optional":true,"type":"array","items":{"$ref":"AXValueSource"}}]},{"id":"AXPropertyName","description":"Values of AXProperty name:\\n- from \'busy\' to \'roledescription\': states which apply to every AX node\\n- from \'live\' to \'root\': attributes which apply to nodes in live regions\\n- from \'autocomplete\' to \'valuetext\': attributes which apply to widgets\\n- from \'checked\' to \'selected\': states which apply to widgets\\n- from \'activedescendant\' to \'owns\' - relationships between elements other than parent/child/sibling.","type":"string","enum":["busy","disabled","editable","focusable","focused","hidden","hiddenRoot","invalid","keyshortcuts","settable","roledescription","live","atomic","relevant","root","autocomplete","hasPopup","level","multiselectable","orientation","multiline","readonly","required","valuemin","valuemax","valuetext","checked","expanded","modal","pressed","selected","activedescendant","controls","describedby","details","errormessage","flowto","labelledby","owns"]},{"id":"AXNode","description":"A node in the accessibility tree.","type":"object","properties":[{"name":"nodeId","description":"Unique identifier for this node.","$ref":"AXNodeId"},{"name":"ignored","description":"Whether this node is ignored for accessibility","type":"boolean"},{"name":"ignoredReasons","description":"Collection of reasons why this node is hidden.","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"role","description":"This `Node`\'s role, whether explicit or implicit.","optional":true,"$ref":"AXValue"},{"name":"chromeRole","description":"This `Node`\'s Chrome raw role.","optional":true,"$ref":"AXValue"},{"name":"name","description":"The accessible name for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"description","description":"The accessible description for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"value","description":"The value for this `Node`.","optional":true,"$ref":"AXValue"},{"name":"properties","description":"All other properties","optional":true,"type":"array","items":{"$ref":"AXProperty"}},{"name":"parentId","description":"ID for this node\'s parent.","optional":true,"$ref":"AXNodeId"},{"name":"childIds","description":"IDs for each of this node\'s child nodes.","optional":true,"type":"array","items":{"$ref":"AXNodeId"}},{"name":"backendDOMNodeId","description":"The backend ID for the associated DOM node, if any.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"frameId","description":"The frame ID for the frame associated with this nodes document.","optional":true,"$ref":"Page.FrameId"}]}],"commands":[{"name":"disable","description":"Disables the accessibility domain."},{"name":"enable","description":"Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."},{"name":"getPartialAXTree","description":"Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get the partial accessibility tree for.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get the partial accessibility tree for.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"fetchRelatives","description":"Whether to fetch this node\'s ancestors, siblings and children. Defaults to true.","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\\nchildren, if requested.","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getFullAXTree","description":"Fetches the entire accessibility tree for the root Document","experimental":true,"parameters":[{"name":"depth","description":"The maximum depth at which descendants of the root node should be retrieved.\\nIf omitted, the full tree is returned.","optional":true,"type":"integer"},{"name":"frameId","description":"The frame for whose document the AX tree should be retrieved.\\nIf omited, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getRootAXNode","description":"Fetches the root node.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"frameId","description":"The frame in whose document the node resides.\\nIf omitted, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"node","$ref":"AXNode"}]},{"name":"getAXNodeAndAncestors","description":"Fetches a node and all ancestors up to and including the root.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node to get.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to get.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper to get.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"getChildAXNodes","description":"Fetches a particular accessibility node by AXNodeId.\\nRequires `enable()` to have been called previously.","experimental":true,"parameters":[{"name":"id","$ref":"AXNodeId"},{"name":"frameId","description":"The frame in whose document the node resides.\\nIf omitted, the root frame is used.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"nodes","type":"array","items":{"$ref":"AXNode"}}]},{"name":"queryAXTree","description":"Query a DOM node\'s accessibility subtree for accessible name and role.\\nThis command computes the name and role for all nodes in the subtree, including those that are\\nignored for accessibility, and returns those that mactch the specified name and role. If no DOM\\nnode is specified, or the DOM node does not exist, the command returns an error. If neither\\n`accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node for the root to query.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node for the root to query.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper for the root to query.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"accessibleName","description":"Find nodes with this computed name.","optional":true,"type":"string"},{"name":"role","description":"Find nodes with this computed role.","optional":true,"type":"string"}],"returns":[{"name":"nodes","description":"A list of `Accessibility.AXNode` matching the specified attributes,\\nincluding nodes that are ignored for accessibility.","type":"array","items":{"$ref":"AXNode"}}]}],"events":[{"name":"loadComplete","description":"The loadComplete event mirrors the load complete event sent by the browser to assistive\\ntechnology when the web page has finished loading.","experimental":true,"parameters":[{"name":"root","description":"New document root node.","$ref":"AXNode"}]},{"name":"nodesUpdated","description":"The nodesUpdated event is sent every time a previously requested node has changed the in tree.","experimental":true,"parameters":[{"name":"nodes","description":"Updated node data.","type":"array","items":{"$ref":"AXNode"}}]}]},{"domain":"Animation","experimental":true,"dependencies":["Runtime","DOM"],"types":[{"id":"Animation","description":"Animation instance.","type":"object","properties":[{"name":"id","description":"`Animation`\'s id.","type":"string"},{"name":"name","description":"`Animation`\'s name.","type":"string"},{"name":"pausedState","description":"`Animation`\'s internal paused state.","type":"boolean"},{"name":"playState","description":"`Animation`\'s play state.","type":"string"},{"name":"playbackRate","description":"`Animation`\'s playback rate.","type":"number"},{"name":"startTime","description":"`Animation`\'s start time.","type":"number"},{"name":"currentTime","description":"`Animation`\'s current time.","type":"number"},{"name":"type","description":"Animation type of `Animation`.","type":"string","enum":["CSSTransition","CSSAnimation","WebAnimation"]},{"name":"source","description":"`Animation`\'s source animation node.","optional":true,"$ref":"AnimationEffect"},{"name":"cssId","description":"A unique ID for `Animation` representing the sources that triggered this CSS\\nanimation/transition.","optional":true,"type":"string"}]},{"id":"AnimationEffect","description":"AnimationEffect instance","type":"object","properties":[{"name":"delay","description":"`AnimationEffect`\'s delay.","type":"number"},{"name":"endDelay","description":"`AnimationEffect`\'s end delay.","type":"number"},{"name":"iterationStart","description":"`AnimationEffect`\'s iteration start.","type":"number"},{"name":"iterations","description":"`AnimationEffect`\'s iterations.","type":"number"},{"name":"duration","description":"`AnimationEffect`\'s iteration duration.","type":"number"},{"name":"direction","description":"`AnimationEffect`\'s playback direction.","type":"string"},{"name":"fill","description":"`AnimationEffect`\'s fill mode.","type":"string"},{"name":"backendNodeId","description":"`AnimationEffect`\'s target node.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"keyframesRule","description":"`AnimationEffect`\'s keyframes.","optional":true,"$ref":"KeyframesRule"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]},{"id":"KeyframesRule","description":"Keyframes Rule","type":"object","properties":[{"name":"name","description":"CSS keyframed animation\'s name.","optional":true,"type":"string"},{"name":"keyframes","description":"List of animation keyframes.","type":"array","items":{"$ref":"KeyframeStyle"}}]},{"id":"KeyframeStyle","description":"Keyframe Style","type":"object","properties":[{"name":"offset","description":"Keyframe\'s time offset.","type":"string"},{"name":"easing","description":"`AnimationEffect`\'s timing function.","type":"string"}]}],"commands":[{"name":"disable","description":"Disables animation domain notifications."},{"name":"enable","description":"Enables animation domain notifications."},{"name":"getCurrentTime","description":"Returns the current time of the an animation.","parameters":[{"name":"id","description":"Id of animation.","type":"string"}],"returns":[{"name":"currentTime","description":"Current time of the page.","type":"number"}]},{"name":"getPlaybackRate","description":"Gets the playback rate of the document timeline.","returns":[{"name":"playbackRate","description":"Playback rate for animations on page.","type":"number"}]},{"name":"releaseAnimations","description":"Releases a set of animations to no longer be manipulated.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}}]},{"name":"resolveAnimation","description":"Gets the remote object of the Animation.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"}],"returns":[{"name":"remoteObject","description":"Corresponding remote object.","$ref":"Runtime.RemoteObject"}]},{"name":"seekAnimations","description":"Seek a set of animations to a particular time within each animation.","parameters":[{"name":"animations","description":"List of animation ids to seek.","type":"array","items":{"type":"string"}},{"name":"currentTime","description":"Set the current time of each animation.","type":"number"}]},{"name":"setPaused","description":"Sets the paused state of a set of animations.","parameters":[{"name":"animations","description":"Animations to set the pause state of.","type":"array","items":{"type":"string"}},{"name":"paused","description":"Paused state to set to.","type":"boolean"}]},{"name":"setPlaybackRate","description":"Sets the playback rate of the document timeline.","parameters":[{"name":"playbackRate","description":"Playback rate for animations on page","type":"number"}]},{"name":"setTiming","description":"Sets the timing of an animation node.","parameters":[{"name":"animationId","description":"Animation id.","type":"string"},{"name":"duration","description":"Duration of the animation.","type":"number"},{"name":"delay","description":"Delay of the animation.","type":"number"}]}],"events":[{"name":"animationCanceled","description":"Event for when an animation has been cancelled.","parameters":[{"name":"id","description":"Id of the animation that was cancelled.","type":"string"}]},{"name":"animationCreated","description":"Event for each animation that has been created.","parameters":[{"name":"id","description":"Id of the animation that was created.","type":"string"}]},{"name":"animationStarted","description":"Event for animation that has been started.","parameters":[{"name":"animation","description":"Animation that was started.","$ref":"Animation"}]}]},{"domain":"Audits","description":"Audits domain allows investigation of page violations and possible improvements.","experimental":true,"dependencies":["Network"],"types":[{"id":"AffectedCookie","description":"Information about a cookie that is affected by an inspector issue.","type":"object","properties":[{"name":"name","description":"The following three properties uniquely identify a cookie","type":"string"},{"name":"path","type":"string"},{"name":"domain","type":"string"}]},{"id":"AffectedRequest","description":"Information about a request that is affected by an inspector issue.","type":"object","properties":[{"name":"requestId","description":"The unique request id.","$ref":"Network.RequestId"},{"name":"url","optional":true,"type":"string"}]},{"id":"AffectedFrame","description":"Information about the frame affected by an inspector issue.","type":"object","properties":[{"name":"frameId","$ref":"Page.FrameId"}]},{"id":"CookieExclusionReason","type":"string","enum":["ExcludeSameSiteUnspecifiedTreatedAsLax","ExcludeSameSiteNoneInsecure","ExcludeSameSiteLax","ExcludeSameSiteStrict","ExcludeInvalidSameParty","ExcludeSamePartyCrossPartyContext","ExcludeDomainNonASCII","ExcludeThirdPartyCookieBlockedInFirstPartySet"]},{"id":"CookieWarningReason","type":"string","enum":["WarnSameSiteUnspecifiedCrossSiteContext","WarnSameSiteNoneInsecure","WarnSameSiteUnspecifiedLaxAllowUnsafe","WarnSameSiteStrictLaxDowngradeStrict","WarnSameSiteStrictCrossDowngradeStrict","WarnSameSiteStrictCrossDowngradeLax","WarnSameSiteLaxCrossDowngradeStrict","WarnSameSiteLaxCrossDowngradeLax","WarnAttributeValueExceedsMaxSize","WarnDomainNonASCII","WarnThirdPartyPhaseout"]},{"id":"CookieOperation","type":"string","enum":["SetCookie","ReadCookie"]},{"id":"CookieIssueDetails","description":"This information is currently necessary, as the front-end has a difficult\\ntime finding a specific cookie. With this, we can convey specific error\\ninformation without the cookie.","type":"object","properties":[{"name":"cookie","description":"If AffectedCookie is not set then rawCookieLine contains the raw\\nSet-Cookie header string. This hints at a problem where the\\ncookie line is syntactically or semantically malformed in a way\\nthat no valid cookie could be created.","optional":true,"$ref":"AffectedCookie"},{"name":"rawCookieLine","optional":true,"type":"string"},{"name":"cookieWarningReasons","type":"array","items":{"$ref":"CookieWarningReason"}},{"name":"cookieExclusionReasons","type":"array","items":{"$ref":"CookieExclusionReason"}},{"name":"operation","description":"Optionally identifies the site-for-cookies and the cookie url, which\\nmay be used by the front-end as additional context.","$ref":"CookieOperation"},{"name":"siteForCookies","optional":true,"type":"string"},{"name":"cookieUrl","optional":true,"type":"string"},{"name":"request","optional":true,"$ref":"AffectedRequest"}]},{"id":"MixedContentResolutionStatus","type":"string","enum":["MixedContentBlocked","MixedContentAutomaticallyUpgraded","MixedContentWarning"]},{"id":"MixedContentResourceType","type":"string","enum":["AttributionSrc","Audio","Beacon","CSPReport","Download","EventSource","Favicon","Font","Form","Frame","Image","Import","Manifest","Ping","PluginData","PluginResource","Prefetch","Resource","Script","ServiceWorker","SharedWorker","Stylesheet","Track","Video","Worker","XMLHttpRequest","XSLT"]},{"id":"MixedContentIssueDetails","type":"object","properties":[{"name":"resourceType","description":"The type of resource causing the mixed content issue (css, js, iframe,\\nform,...). Marked as optional because it is mapped to from\\nblink::mojom::RequestContextType, which will be replaced\\nby network::mojom::RequestDestination","optional":true,"$ref":"MixedContentResourceType"},{"name":"resolutionStatus","description":"The way the mixed content issue is being resolved.","$ref":"MixedContentResolutionStatus"},{"name":"insecureURL","description":"The unsafe http url causing the mixed content issue.","type":"string"},{"name":"mainResourceURL","description":"The url responsible for the call to an unsafe url.","type":"string"},{"name":"request","description":"The mixed content request.\\nDoes not always exist (e.g. for unsafe form submission urls).","optional":true,"$ref":"AffectedRequest"},{"name":"frame","description":"Optional because not every mixed content issue is necessarily linked to a frame.","optional":true,"$ref":"AffectedFrame"}]},{"id":"BlockedByResponseReason","description":"Enum indicating the reason a response has been blocked. These reasons are\\nrefinements of the net error BLOCKED_BY_RESPONSE.","type":"string","enum":["CoepFrameResourceNeedsCoepHeader","CoopSandboxedIFrameCannotNavigateToCoopPage","CorpNotSameOrigin","CorpNotSameOriginAfterDefaultedToSameOriginByCoep","CorpNotSameSite"]},{"id":"BlockedByResponseIssueDetails","description":"Details for a request that has been blocked with the BLOCKED_BY_RESPONSE\\ncode. Currently only used for COEP/COOP, but may be extended to include\\nsome CSP errors in the future.","type":"object","properties":[{"name":"request","$ref":"AffectedRequest"},{"name":"parentFrame","optional":true,"$ref":"AffectedFrame"},{"name":"blockedFrame","optional":true,"$ref":"AffectedFrame"},{"name":"reason","$ref":"BlockedByResponseReason"}]},{"id":"HeavyAdResolutionStatus","type":"string","enum":["HeavyAdBlocked","HeavyAdWarning"]},{"id":"HeavyAdReason","type":"string","enum":["NetworkTotalLimit","CpuTotalLimit","CpuPeakLimit"]},{"id":"HeavyAdIssueDetails","type":"object","properties":[{"name":"resolution","description":"The resolution status, either blocking the content or warning.","$ref":"HeavyAdResolutionStatus"},{"name":"reason","description":"The reason the ad was blocked, total network or cpu or peak cpu.","$ref":"HeavyAdReason"},{"name":"frame","description":"The frame that was blocked.","$ref":"AffectedFrame"}]},{"id":"ContentSecurityPolicyViolationType","type":"string","enum":["kInlineViolation","kEvalViolation","kURLViolation","kTrustedTypesSinkViolation","kTrustedTypesPolicyViolation","kWasmEvalViolation"]},{"id":"SourceCodeLocation","type":"object","properties":[{"name":"scriptId","optional":true,"$ref":"Runtime.ScriptId"},{"name":"url","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"ContentSecurityPolicyIssueDetails","type":"object","properties":[{"name":"blockedURL","description":"The url not included in allowed sources.","optional":true,"type":"string"},{"name":"violatedDirective","description":"Specific directive that is violated, causing the CSP issue.","type":"string"},{"name":"isReportOnly","type":"boolean"},{"name":"contentSecurityPolicyViolationType","$ref":"ContentSecurityPolicyViolationType"},{"name":"frameAncestor","optional":true,"$ref":"AffectedFrame"},{"name":"sourceCodeLocation","optional":true,"$ref":"SourceCodeLocation"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"SharedArrayBufferIssueType","type":"string","enum":["TransferIssue","CreationIssue"]},{"id":"SharedArrayBufferIssueDetails","description":"Details for a issue arising from an SAB being instantiated in, or\\ntransferred to a context that is not cross-origin isolated.","type":"object","properties":[{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"isWarning","type":"boolean"},{"name":"type","$ref":"SharedArrayBufferIssueType"}]},{"id":"LowTextContrastIssueDetails","type":"object","properties":[{"name":"violatingNodeId","$ref":"DOM.BackendNodeId"},{"name":"violatingNodeSelector","type":"string"},{"name":"contrastRatio","type":"number"},{"name":"thresholdAA","type":"number"},{"name":"thresholdAAA","type":"number"},{"name":"fontSize","type":"string"},{"name":"fontWeight","type":"string"}]},{"id":"CorsIssueDetails","description":"Details for a CORS related issue, e.g. a warning or error related to\\nCORS RFC1918 enforcement.","type":"object","properties":[{"name":"corsErrorStatus","$ref":"Network.CorsErrorStatus"},{"name":"isWarning","type":"boolean"},{"name":"request","$ref":"AffectedRequest"},{"name":"location","optional":true,"$ref":"SourceCodeLocation"},{"name":"initiatorOrigin","optional":true,"type":"string"},{"name":"resourceIPAddressSpace","optional":true,"$ref":"Network.IPAddressSpace"},{"name":"clientSecurityState","optional":true,"$ref":"Network.ClientSecurityState"}]},{"id":"AttributionReportingIssueType","type":"string","enum":["PermissionPolicyDisabled","UntrustworthyReportingOrigin","InsecureContext","InvalidHeader","InvalidRegisterTriggerHeader","SourceAndTriggerHeaders","SourceIgnored","TriggerIgnored","OsSourceIgnored","OsTriggerIgnored","InvalidRegisterOsSourceHeader","InvalidRegisterOsTriggerHeader","WebAndOsHeaders","NoWebOrOsSupport","NavigationRegistrationWithoutTransientUserActivation"]},{"id":"AttributionReportingIssueDetails","description":"Details for issues around \\"Attribution Reporting API\\" usage.\\nExplainer: https://github.com/WICG/attribution-reporting-api","type":"object","properties":[{"name":"violationType","$ref":"AttributionReportingIssueType"},{"name":"request","optional":true,"$ref":"AffectedRequest"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"invalidParameter","optional":true,"type":"string"}]},{"id":"QuirksModeIssueDetails","description":"Details for issues about documents in Quirks Mode\\nor Limited Quirks Mode that affects page layouting.","type":"object","properties":[{"name":"isLimitedQuirksMode","description":"If false, it means the document\'s mode is \\"quirks\\"\\ninstead of \\"limited-quirks\\".","type":"boolean"},{"name":"documentNodeId","$ref":"DOM.BackendNodeId"},{"name":"url","type":"string"},{"name":"frameId","$ref":"Page.FrameId"},{"name":"loaderId","$ref":"Network.LoaderId"}]},{"id":"NavigatorUserAgentIssueDetails","deprecated":true,"type":"object","properties":[{"name":"url","type":"string"},{"name":"location","optional":true,"$ref":"SourceCodeLocation"}]},{"id":"GenericIssueErrorType","type":"string","enum":["CrossOriginPortalPostMessageError","FormLabelForNameError","FormDuplicateIdForInputError","FormInputWithNoLabelError","FormAutocompleteAttributeEmptyError","FormEmptyIdAndNameAttributesForInputError","FormAriaLabelledByToNonExistingId","FormInputAssignedAutocompleteValueToIdOrNameAttributeError","FormLabelHasNeitherForNorNestedInput","FormLabelForMatchesNonExistingIdError","FormInputHasWrongButWellIntendedAutocompleteValueError","ResponseWasBlockedByORB"]},{"id":"GenericIssueDetails","description":"Depending on the concrete errorType, different properties are set.","type":"object","properties":[{"name":"errorType","description":"Issues with the same errorType are aggregated in the frontend.","$ref":"GenericIssueErrorType"},{"name":"frameId","optional":true,"$ref":"Page.FrameId"},{"name":"violatingNodeId","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"violatingNodeAttribute","optional":true,"type":"string"},{"name":"request","optional":true,"$ref":"AffectedRequest"}]},{"id":"DeprecationIssueDetails","description":"This issue tracks information needed to print a deprecation message.\\nhttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/third_party/blink/renderer/core/frame/deprecation/README.md","type":"object","properties":[{"name":"affectedFrame","optional":true,"$ref":"AffectedFrame"},{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"type","description":"One of the deprecation names from third_party/blink/renderer/core/frame/deprecation/deprecation.json5","type":"string"}]},{"id":"BounceTrackingIssueDetails","description":"This issue warns about sites in the redirect chain of a finished navigation\\nthat may be flagged as trackers and have their state cleared if they don\'t\\nreceive a user interaction. Note that in this context \'site\' means eTLD+1.\\nFor example, if the URL `https://example.test:80/bounce` was in the\\nredirect chain, the site reported would be `example.test`.","type":"object","properties":[{"name":"trackingSites","type":"array","items":{"type":"string"}}]},{"id":"ClientHintIssueReason","type":"string","enum":["MetaTagAllowListInvalidOrigin","MetaTagModifiedHTML"]},{"id":"FederatedAuthRequestIssueDetails","type":"object","properties":[{"name":"federatedAuthRequestIssueReason","$ref":"FederatedAuthRequestIssueReason"}]},{"id":"FederatedAuthRequestIssueReason","description":"Represents the failure reason when a federated authentication reason fails.\\nShould be updated alongside RequestIdTokenStatus in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom to include\\nall cases except for success.","type":"string","enum":["ShouldEmbargo","TooManyRequests","WellKnownHttpNotFound","WellKnownNoResponse","WellKnownInvalidResponse","WellKnownListEmpty","WellKnownInvalidContentType","ConfigNotInWellKnown","WellKnownTooBig","ConfigHttpNotFound","ConfigNoResponse","ConfigInvalidResponse","ConfigInvalidContentType","ClientMetadataHttpNotFound","ClientMetadataNoResponse","ClientMetadataInvalidResponse","ClientMetadataInvalidContentType","DisabledInSettings","ErrorFetchingSignin","InvalidSigninResponse","AccountsHttpNotFound","AccountsNoResponse","AccountsInvalidResponse","AccountsListEmpty","AccountsInvalidContentType","IdTokenHttpNotFound","IdTokenNoResponse","IdTokenInvalidResponse","IdTokenInvalidRequest","IdTokenInvalidContentType","ErrorIdToken","Canceled","RpPageNotVisible","SilentMediationFailure","ThirdPartyCookiesBlocked"]},{"id":"FederatedAuthUserInfoRequestIssueDetails","type":"object","properties":[{"name":"federatedAuthUserInfoRequestIssueReason","$ref":"FederatedAuthUserInfoRequestIssueReason"}]},{"id":"FederatedAuthUserInfoRequestIssueReason","description":"Represents the failure reason when a getUserInfo() call fails.\\nShould be updated alongside FederatedAuthUserInfoRequestResult in\\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom.","type":"string","enum":["NotSameOrigin","NotIframe","NotPotentiallyTrustworthy","NoApiPermission","NotSignedInWithIdp","NoAccountSharingPermission","InvalidConfigOrWellKnown","InvalidAccountsResponse","NoReturningUserFromFetchedAccounts"]},{"id":"ClientHintIssueDetails","description":"This issue tracks client hints related issues. It\'s used to deprecate old\\nfeatures, encourage the use of new ones, and provide general guidance.","type":"object","properties":[{"name":"sourceCodeLocation","$ref":"SourceCodeLocation"},{"name":"clientHintIssueReason","$ref":"ClientHintIssueReason"}]},{"id":"FailedRequestInfo","type":"object","properties":[{"name":"url","description":"The URL that failed to load.","type":"string"},{"name":"failureMessage","description":"The failure message for the failed request.","type":"string"},{"name":"requestId","optional":true,"$ref":"Network.RequestId"}]},{"id":"StyleSheetLoadingIssueReason","type":"string","enum":["LateImportRule","RequestFailed"]},{"id":"StylesheetLoadingIssueDetails","description":"This issue warns when a referenced stylesheet couldn\'t be loaded.","type":"object","properties":[{"name":"sourceCodeLocation","description":"Source code position that referenced the failing stylesheet.","$ref":"SourceCodeLocation"},{"name":"styleSheetLoadingIssueReason","description":"Reason why the stylesheet couldn\'t be loaded.","$ref":"StyleSheetLoadingIssueReason"},{"name":"failedRequestInfo","description":"Contains additional info when the failure was due to a request.","optional":true,"$ref":"FailedRequestInfo"}]},{"id":"InspectorIssueCode","description":"A unique identifier for the type of issue. Each type may use one of the\\noptional fields in InspectorIssueDetails to convey more specific\\ninformation about the kind of issue.","type":"string","enum":["CookieIssue","MixedContentIssue","BlockedByResponseIssue","HeavyAdIssue","ContentSecurityPolicyIssue","SharedArrayBufferIssue","LowTextContrastIssue","CorsIssue","AttributionReportingIssue","QuirksModeIssue","NavigatorUserAgentIssue","GenericIssue","DeprecationIssue","ClientHintIssue","FederatedAuthRequestIssue","BounceTrackingIssue","StylesheetLoadingIssue","FederatedAuthUserInfoRequestIssue"]},{"id":"InspectorIssueDetails","description":"This struct holds a list of optional fields with additional information\\nspecific to the kind of issue. When adding a new issue code, please also\\nadd a new optional field to this type.","type":"object","properties":[{"name":"cookieIssueDetails","optional":true,"$ref":"CookieIssueDetails"},{"name":"mixedContentIssueDetails","optional":true,"$ref":"MixedContentIssueDetails"},{"name":"blockedByResponseIssueDetails","optional":true,"$ref":"BlockedByResponseIssueDetails"},{"name":"heavyAdIssueDetails","optional":true,"$ref":"HeavyAdIssueDetails"},{"name":"contentSecurityPolicyIssueDetails","optional":true,"$ref":"ContentSecurityPolicyIssueDetails"},{"name":"sharedArrayBufferIssueDetails","optional":true,"$ref":"SharedArrayBufferIssueDetails"},{"name":"lowTextContrastIssueDetails","optional":true,"$ref":"LowTextContrastIssueDetails"},{"name":"corsIssueDetails","optional":true,"$ref":"CorsIssueDetails"},{"name":"attributionReportingIssueDetails","optional":true,"$ref":"AttributionReportingIssueDetails"},{"name":"quirksModeIssueDetails","optional":true,"$ref":"QuirksModeIssueDetails"},{"name":"navigatorUserAgentIssueDetails","deprecated":true,"optional":true,"$ref":"NavigatorUserAgentIssueDetails"},{"name":"genericIssueDetails","optional":true,"$ref":"GenericIssueDetails"},{"name":"deprecationIssueDetails","optional":true,"$ref":"DeprecationIssueDetails"},{"name":"clientHintIssueDetails","optional":true,"$ref":"ClientHintIssueDetails"},{"name":"federatedAuthRequestIssueDetails","optional":true,"$ref":"FederatedAuthRequestIssueDetails"},{"name":"bounceTrackingIssueDetails","optional":true,"$ref":"BounceTrackingIssueDetails"},{"name":"stylesheetLoadingIssueDetails","optional":true,"$ref":"StylesheetLoadingIssueDetails"},{"name":"federatedAuthUserInfoRequestIssueDetails","optional":true,"$ref":"FederatedAuthUserInfoRequestIssueDetails"}]},{"id":"IssueId","description":"A unique id for a DevTools inspector issue. Allows other entities (e.g.\\nexceptions, CDP message, console messages, etc.) to reference an issue.","type":"string"},{"id":"InspectorIssue","description":"An inspector issue reported from the back-end.","type":"object","properties":[{"name":"code","$ref":"InspectorIssueCode"},{"name":"details","$ref":"InspectorIssueDetails"},{"name":"issueId","description":"A unique id for this issue. May be omitted if no other entity (e.g.\\nexception, CDP message, etc.) is referencing this issue.","optional":true,"$ref":"IssueId"}]}],"commands":[{"name":"getEncodedResponse","description":"Returns the response body and size if it were re-encoded with the specified settings. Only\\napplies to images.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"Network.RequestId"},{"name":"encoding","description":"The encoding to use.","type":"string","enum":["webp","jpeg","png"]},{"name":"quality","description":"The quality of the encoding (0-1). (defaults to 1)","optional":true,"type":"number"},{"name":"sizeOnly","description":"Whether to only return the size information (defaults to false).","optional":true,"type":"boolean"}],"returns":[{"name":"body","description":"The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"originalSize","description":"Size before re-encoding.","type":"integer"},{"name":"encodedSize","description":"Size after re-encoding.","type":"integer"}]},{"name":"disable","description":"Disables issues domain, prevents further issues from being reported to the client."},{"name":"enable","description":"Enables issues domain, sends the issues collected so far to the client by means of the\\n`issueAdded` event."},{"name":"checkContrast","description":"Runs the contrast check for the target page. Found issues are reported\\nusing Audits.issueAdded event.","parameters":[{"name":"reportAAA","description":"Whether to report WCAG AAA level issues. Default is false.","optional":true,"type":"boolean"}]},{"name":"checkFormsIssues","description":"Runs the form issues check for the target page. Found issues are reported\\nusing Audits.issueAdded event.","returns":[{"name":"formIssues","type":"array","items":{"$ref":"GenericIssueDetails"}}]}],"events":[{"name":"issueAdded","parameters":[{"name":"issue","$ref":"InspectorIssue"}]}]},{"domain":"Autofill","description":"Defines commands and events for Autofill.","experimental":true,"types":[{"id":"CreditCard","type":"object","properties":[{"name":"number","description":"16-digit credit card number.","type":"string"},{"name":"name","description":"Name of the credit card owner.","type":"string"},{"name":"expiryMonth","description":"2-digit expiry month.","type":"string"},{"name":"expiryYear","description":"4-digit expiry year.","type":"string"},{"name":"cvc","description":"3-digit card verification code.","type":"string"}]},{"id":"AddressField","type":"object","properties":[{"name":"name","description":"address field name, for example GIVEN_NAME.","type":"string"},{"name":"value","description":"address field name, for example Jon Doe.","type":"string"}]},{"id":"Address","type":"object","properties":[{"name":"fields","description":"fields and values defining a test address.","type":"array","items":{"$ref":"AddressField"}}]}],"commands":[{"name":"trigger","description":"Trigger autofill on a form identified by the fieldId.\\nIf the field and related form cannot be autofilled, returns an error.","parameters":[{"name":"fieldId","description":"Identifies a field that serves as an anchor for autofill.","$ref":"DOM.BackendNodeId"},{"name":"frameId","description":"Identifies the frame that field belongs to.","optional":true,"$ref":"Page.FrameId"},{"name":"card","description":"Credit card information to fill out the form. Credit card data is not saved.","$ref":"CreditCard"}]},{"name":"setAddresses","description":"Set addresses so that developers can verify their forms implementation.","parameters":[{"name":"addresses","type":"array","items":{"$ref":"Address"}}]}]},{"domain":"BackgroundService","description":"Defines events for background web platform features.","experimental":true,"types":[{"id":"ServiceName","description":"The Background Service that will be associated with the commands/events.\\nEvery Background Service operates independently, but they share the same\\nAPI.","type":"string","enum":["backgroundFetch","backgroundSync","pushMessaging","notifications","paymentHandler","periodicBackgroundSync"]},{"id":"EventMetadata","description":"A key-value pair for additional event information to pass along.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"BackgroundServiceEvent","type":"object","properties":[{"name":"timestamp","description":"Timestamp of the event (in seconds).","$ref":"Network.TimeSinceEpoch"},{"name":"origin","description":"The origin this event belongs to.","type":"string"},{"name":"serviceWorkerRegistrationId","description":"The Service Worker ID that initiated the event.","$ref":"ServiceWorker.RegistrationID"},{"name":"service","description":"The Background Service this event belongs to.","$ref":"ServiceName"},{"name":"eventName","description":"A description of the event.","type":"string"},{"name":"instanceId","description":"An identifier that groups related events together.","type":"string"},{"name":"eventMetadata","description":"A list of event-specific information.","type":"array","items":{"$ref":"EventMetadata"}},{"name":"storageKey","description":"Storage key this event belongs to.","type":"string"}]}],"commands":[{"name":"startObserving","description":"Enables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"stopObserving","description":"Disables event updates for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]},{"name":"setRecording","description":"Set the recording state for the service.","parameters":[{"name":"shouldRecord","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"clearEvents","description":"Clears all stored data for the service.","parameters":[{"name":"service","$ref":"ServiceName"}]}],"events":[{"name":"recordingStateChanged","description":"Called when the recording state for the service has been updated.","parameters":[{"name":"isRecording","type":"boolean"},{"name":"service","$ref":"ServiceName"}]},{"name":"backgroundServiceEventReceived","description":"Called with all existing backgroundServiceEvents when enabled, and all new\\nevents afterwards if enabled and recording.","parameters":[{"name":"backgroundServiceEvent","$ref":"BackgroundServiceEvent"}]}]},{"domain":"Browser","description":"The Browser domain defines methods and events for browser managing.","types":[{"id":"BrowserContextID","experimental":true,"type":"string"},{"id":"WindowID","experimental":true,"type":"integer"},{"id":"WindowState","description":"The state of the browser window.","experimental":true,"type":"string","enum":["normal","minimized","maximized","fullscreen"]},{"id":"Bounds","description":"Browser window bounds information","experimental":true,"type":"object","properties":[{"name":"left","description":"The offset from the left edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"top","description":"The offset from the top edge of the screen to the window in pixels.","optional":true,"type":"integer"},{"name":"width","description":"The window width in pixels.","optional":true,"type":"integer"},{"name":"height","description":"The window height in pixels.","optional":true,"type":"integer"},{"name":"windowState","description":"The window state. Default to normal.","optional":true,"$ref":"WindowState"}]},{"id":"PermissionType","experimental":true,"type":"string","enum":["accessibilityEvents","audioCapture","backgroundSync","backgroundFetch","clipboardReadWrite","clipboardSanitizedWrite","displayCapture","durableStorage","flash","geolocation","idleDetection","localFonts","midi","midiSysex","nfc","notifications","paymentHandler","periodicBackgroundSync","protectedMediaIdentifier","sensors","storageAccess","topLevelStorageAccess","videoCapture","videoCapturePanTiltZoom","wakeLockScreen","wakeLockSystem","windowManagement"]},{"id":"PermissionSetting","experimental":true,"type":"string","enum":["granted","denied","prompt"]},{"id":"PermissionDescriptor","description":"Definition of PermissionDescriptor defined in the Permissions API:\\nhttps://w3c.github.io/permissions/#dictdef-permissiondescriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name of permission.\\nSee https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names.","type":"string"},{"name":"sysex","description":"For \\"midi\\" permission, may also specify sysex control.","optional":true,"type":"boolean"},{"name":"userVisibleOnly","description":"For \\"push\\" permission, may specify userVisibleOnly.\\nNote that userVisibleOnly = true is the only currently supported type.","optional":true,"type":"boolean"},{"name":"allowWithoutSanitization","description":"For \\"clipboard\\" permission, may specify allowWithoutSanitization.","optional":true,"type":"boolean"},{"name":"panTiltZoom","description":"For \\"camera\\" permission, may specify panTiltZoom.","optional":true,"type":"boolean"}]},{"id":"BrowserCommandId","description":"Browser command ids used by executeBrowserCommand.","experimental":true,"type":"string","enum":["openTabSearch","closeTabSearch"]},{"id":"Bucket","description":"Chrome histogram bucket.","experimental":true,"type":"object","properties":[{"name":"low","description":"Minimum value (inclusive).","type":"integer"},{"name":"high","description":"Maximum value (exclusive).","type":"integer"},{"name":"count","description":"Number of samples.","type":"integer"}]},{"id":"Histogram","description":"Chrome histogram.","experimental":true,"type":"object","properties":[{"name":"name","description":"Name.","type":"string"},{"name":"sum","description":"Sum of sample values.","type":"integer"},{"name":"count","description":"Total number of samples.","type":"integer"},{"name":"buckets","description":"Buckets.","type":"array","items":{"$ref":"Bucket"}}]}],"commands":[{"name":"setPermission","description":"Set permission settings for given origin.","experimental":true,"parameters":[{"name":"permission","description":"Descriptor of permission to override.","$ref":"PermissionDescriptor"},{"name":"setting","description":"Setting of the permission.","$ref":"PermissionSetting"},{"name":"origin","description":"Origin the permission applies to, all origins if not specified.","optional":true,"type":"string"},{"name":"browserContextId","description":"Context to override. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"grantPermissions","description":"Grant specific permissions to the given origin and reject all others.","experimental":true,"parameters":[{"name":"permissions","type":"array","items":{"$ref":"PermissionType"}},{"name":"origin","description":"Origin the permission applies to, all origins if not specified.","optional":true,"type":"string"},{"name":"browserContextId","description":"BrowserContext to override permissions. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"resetPermissions","description":"Reset all permission management for all origins.","experimental":true,"parameters":[{"name":"browserContextId","description":"BrowserContext to reset permissions. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny). |allowAndName| allows download and names files according to\\ntheir dowmload guids.","type":"string","enum":["deny","allow","allowAndName","default"]},{"name":"browserContextId","description":"BrowserContext to set download behavior. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"},{"name":"downloadPath","description":"The default path to save downloaded files to. This is required if behavior is set to \'allow\'\\nor \'allowAndName\'.","optional":true,"type":"string"},{"name":"eventsEnabled","description":"Whether to emit download events (defaults to false).","optional":true,"type":"boolean"}]},{"name":"cancelDownload","description":"Cancel a download if in progress","experimental":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"browserContextId","description":"BrowserContext to perform the action in. When omitted, default browser context is used.","optional":true,"$ref":"BrowserContextID"}]},{"name":"close","description":"Close browser gracefully."},{"name":"crash","description":"Crashes browser on the main thread.","experimental":true},{"name":"crashGpuProcess","description":"Crashes GPU process.","experimental":true},{"name":"getVersion","description":"Returns version information.","returns":[{"name":"protocolVersion","description":"Protocol version.","type":"string"},{"name":"product","description":"Product name.","type":"string"},{"name":"revision","description":"Product revision.","type":"string"},{"name":"userAgent","description":"User-Agent.","type":"string"},{"name":"jsVersion","description":"V8 version.","type":"string"}]},{"name":"getBrowserCommandLine","description":"Returns the command line switches for the browser process if, and only if\\n--enable-automation is on the commandline.","experimental":true,"returns":[{"name":"arguments","description":"Commandline parameters","type":"array","items":{"type":"string"}}]},{"name":"getHistograms","description":"Get Chrome histograms.","experimental":true,"parameters":[{"name":"query","description":"Requested substring in name. Only histograms which have query as a\\nsubstring in their name are extracted. An empty or absent query returns\\nall histograms.","optional":true,"type":"string"},{"name":"delta","description":"If true, retrieve delta since last delta call.","optional":true,"type":"boolean"}],"returns":[{"name":"histograms","description":"Histograms.","type":"array","items":{"$ref":"Histogram"}}]},{"name":"getHistogram","description":"Get a Chrome histogram by name.","experimental":true,"parameters":[{"name":"name","description":"Requested histogram name.","type":"string"},{"name":"delta","description":"If true, retrieve delta since last delta call.","optional":true,"type":"boolean"}],"returns":[{"name":"histogram","description":"Histogram.","$ref":"Histogram"}]},{"name":"getWindowBounds","description":"Get position and size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"}],"returns":[{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"getWindowForTarget","description":"Get the browser window that contains the devtools target.","experimental":true,"parameters":[{"name":"targetId","description":"Devtools agent host id. If called as a part of the session, associated targetId is used.","optional":true,"$ref":"Target.TargetID"}],"returns":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"Bounds information of the window. When window state is \'minimized\', the restored window\\nposition and size are returned.","$ref":"Bounds"}]},{"name":"setWindowBounds","description":"Set position and/or size of the browser window.","experimental":true,"parameters":[{"name":"windowId","description":"Browser window id.","$ref":"WindowID"},{"name":"bounds","description":"New window bounds. The \'minimized\', \'maximized\' and \'fullscreen\' states cannot be combined\\nwith \'left\', \'top\', \'width\' or \'height\'. Leaves unspecified fields unchanged.","$ref":"Bounds"}]},{"name":"setDockTile","description":"Set dock tile details, platform-specific.","experimental":true,"parameters":[{"name":"badgeLabel","optional":true,"type":"string"},{"name":"image","description":"Png encoded image. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"executeBrowserCommand","description":"Invoke custom browser commands used by telemetry.","experimental":true,"parameters":[{"name":"commandId","$ref":"BrowserCommandId"}]},{"name":"addPrivacySandboxEnrollmentOverride","description":"Allows a site to use privacy sandbox features that require enrollment\\nwithout the site actually being enrolled. Only supported on page targets.","parameters":[{"name":"url","type":"string"}]}],"events":[{"name":"downloadWillBegin","description":"Fired when page is about to start a download.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused the download to begin.","$ref":"Page.FrameId"},{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"},{"name":"suggestedFilename","description":"Suggested file name of the resource (the actual name of the file saved on disk may differ).","type":"string"}]},{"name":"downloadProgress","description":"Fired when download makes progress. Last call has |done| == true.","experimental":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"totalBytes","description":"Total expected bytes to download.","type":"number"},{"name":"receivedBytes","description":"Total bytes received.","type":"number"},{"name":"state","description":"Download status.","type":"string","enum":["inProgress","completed","canceled"]}]}]},{"domain":"CSS","description":"This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\\nhave an associated `id` used in subsequent operations on the related object. Each object type has\\na specific `id` structure, and those are not interchangeable between objects of different kinds.\\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.","experimental":true,"dependencies":["DOM","Page"],"types":[{"id":"StyleSheetId","type":"string"},{"id":"StyleSheetOrigin","description":"Stylesheet type: \\"injected\\" for stylesheets injected via extension, \\"user-agent\\" for user-agent\\nstylesheets, \\"inspector\\" for stylesheets created by the inspector (i.e. those holding the \\"via\\ninspector\\" rules), \\"regular\\" for regular stylesheets.","type":"string","enum":["injected","user-agent","inspector","regular"]},{"id":"PseudoElementMatches","description":"CSS rule collection for a single pseudo style.","type":"object","properties":[{"name":"pseudoType","description":"Pseudo element type.","$ref":"DOM.PseudoType"},{"name":"pseudoIdentifier","description":"Pseudo element custom ident.","optional":true,"type":"string"},{"name":"matches","description":"Matches of CSS rules applicable to the pseudo style.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedStyleEntry","description":"Inherited CSS rule collection from ancestor node.","type":"object","properties":[{"name":"inlineStyle","description":"The ancestor node\'s inline style, if any, in the style inheritance chain.","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"Matches of CSS rules matching the ancestor node in the style inheritance chain.","type":"array","items":{"$ref":"RuleMatch"}}]},{"id":"InheritedPseudoElementMatches","description":"Inherited pseudo element matches from pseudos of an ancestor node.","type":"object","properties":[{"name":"pseudoElements","description":"Matches of pseudo styles from the pseudos of an ancestor node.","type":"array","items":{"$ref":"PseudoElementMatches"}}]},{"id":"RuleMatch","description":"Match data for a CSS rule.","type":"object","properties":[{"name":"rule","description":"CSS rule in the match.","$ref":"CSSRule"},{"name":"matchingSelectors","description":"Matching selector indices in the rule\'s selectorList selectors (0-based).","type":"array","items":{"type":"integer"}}]},{"id":"Value","description":"Data for a simple selector (these are delimited by commas in a selector list).","type":"object","properties":[{"name":"text","description":"Value text.","type":"string"},{"name":"range","description":"Value range in the underlying resource (if available).","optional":true,"$ref":"SourceRange"},{"name":"specificity","description":"Specificity of the selector.","experimental":true,"optional":true,"$ref":"Specificity"}]},{"id":"Specificity","description":"Specificity:\\nhttps://drafts.csswg.org/selectors/#specificity-rules","experimental":true,"type":"object","properties":[{"name":"a","description":"The a component, which represents the number of ID selectors.","type":"integer"},{"name":"b","description":"The b component, which represents the number of class selectors, attributes selectors, and\\npseudo-classes.","type":"integer"},{"name":"c","description":"The c component, which represents the number of type selectors and pseudo-elements.","type":"integer"}]},{"id":"SelectorList","description":"Selector list data.","type":"object","properties":[{"name":"selectors","description":"Selectors in the list.","type":"array","items":{"$ref":"Value"}},{"name":"text","description":"Rule selector text.","type":"string"}]},{"id":"CSSStyleSheetHeader","description":"CSS stylesheet metainformation.","type":"object","properties":[{"name":"styleSheetId","description":"The stylesheet identifier.","$ref":"StyleSheetId"},{"name":"frameId","description":"Owner frame identifier.","$ref":"Page.FrameId"},{"name":"sourceURL","description":"Stylesheet resource URL. Empty if this is a constructed stylesheet created using\\nnew CSSStyleSheet() (but non-empty if this is a constructed sylesheet imported\\nas a CSS module script).","type":"string"},{"name":"sourceMapURL","description":"URL of source map associated with the stylesheet (if any).","optional":true,"type":"string"},{"name":"origin","description":"Stylesheet origin.","$ref":"StyleSheetOrigin"},{"name":"title","description":"Stylesheet title.","type":"string"},{"name":"ownerNode","description":"The backend id for the owner node of the stylesheet.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"disabled","description":"Denotes whether the stylesheet is disabled.","type":"boolean"},{"name":"hasSourceURL","description":"Whether the sourceURL field value comes from the sourceURL comment.","optional":true,"type":"boolean"},{"name":"isInline","description":"Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\\ndocument.written STYLE tags.","type":"boolean"},{"name":"isMutable","description":"Whether this stylesheet is mutable. Inline stylesheets become mutable\\nafter they have been modified via CSSOM API.\\n`<link>` element\'s stylesheets become mutable only if DevTools modifies them.\\nConstructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.","type":"boolean"},{"name":"isConstructed","description":"True if this stylesheet is created through new CSSStyleSheet() or imported as a\\nCSS module script.","type":"boolean"},{"name":"startLine","description":"Line offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"startColumn","description":"Column offset of the stylesheet within the resource (zero based).","type":"number"},{"name":"length","description":"Size of the content (in characters).","type":"number"},{"name":"endLine","description":"Line offset of the end of the stylesheet within the resource (zero based).","type":"number"},{"name":"endColumn","description":"Column offset of the end of the stylesheet within the resource (zero based).","type":"number"},{"name":"loadingFailed","description":"If the style sheet was loaded from a network resource, this indicates when the resource failed to load","experimental":true,"optional":true,"type":"boolean"}]},{"id":"CSSRule","description":"CSS rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"selectorList","description":"Rule selector data.","$ref":"SelectorList"},{"name":"nestingSelectors","description":"Array of selectors from ancestor style rules, sorted by distance from the current rule.","experimental":true,"optional":true,"type":"array","items":{"type":"string"}},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"},{"name":"media","description":"Media list array (for rules involving media queries). The array enumerates media queries\\nstarting with the innermost one, going outwards.","optional":true,"type":"array","items":{"$ref":"CSSMedia"}},{"name":"containerQueries","description":"Container query list array (for rules involving container queries).\\nThe array enumerates container queries starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSContainerQuery"}},{"name":"supports","description":"@supports CSS at-rule array.\\nThe array enumerates @supports at-rules starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSSupports"}},{"name":"layers","description":"Cascade layer array. Contains the layer hierarchy that this rule belongs to starting\\nwith the innermost layer and going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSLayer"}},{"name":"scopes","description":"@scope CSS at-rule array.\\nThe array enumerates @scope at-rules starting with the innermost one, going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSScope"}},{"name":"ruleTypes","description":"The array keeps the types of ancestor CSSRules from the innermost going outwards.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSRuleType"}}]},{"id":"CSSRuleType","description":"Enum indicating the type of a CSS rule, used to represent the order of a style rule\'s ancestors.\\nThis list only contains rule types that are collected during the ancestor rule collection.","experimental":true,"type":"string","enum":["MediaRule","SupportsRule","ContainerRule","LayerRule","ScopeRule","StyleRule"]},{"id":"RuleUsage","description":"CSS coverage information.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","$ref":"StyleSheetId"},{"name":"startOffset","description":"Offset of the start of the rule (including selector) from the beginning of the stylesheet.","type":"number"},{"name":"endOffset","description":"Offset of the end of the rule body from the beginning of the stylesheet.","type":"number"},{"name":"used","description":"Indicates whether the rule was actually used by some element in the page.","type":"boolean"}]},{"id":"SourceRange","description":"Text range within a resource. All numbers are zero-based.","type":"object","properties":[{"name":"startLine","description":"Start line of range.","type":"integer"},{"name":"startColumn","description":"Start column of range (inclusive).","type":"integer"},{"name":"endLine","description":"End line of range","type":"integer"},{"name":"endColumn","description":"End column of range (exclusive).","type":"integer"}]},{"id":"ShorthandEntry","type":"object","properties":[{"name":"name","description":"Shorthand name.","type":"string"},{"name":"value","description":"Shorthand value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]},{"id":"CSSStyle","description":"CSS style representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"cssProperties","description":"CSS properties in the style.","type":"array","items":{"$ref":"CSSProperty"}},{"name":"shorthandEntries","description":"Computed values for all shorthands found in the style.","type":"array","items":{"$ref":"ShorthandEntry"}},{"name":"cssText","description":"Style declaration text (if available).","optional":true,"type":"string"},{"name":"range","description":"Style declaration range in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"}]},{"id":"CSSProperty","description":"CSS property declaration data.","type":"object","properties":[{"name":"name","description":"The property name.","type":"string"},{"name":"value","description":"The property value.","type":"string"},{"name":"important","description":"Whether the property has \\"!important\\" annotation (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"implicit","description":"Whether the property is implicit (implies `false` if absent).","optional":true,"type":"boolean"},{"name":"text","description":"The full property text as specified in the style.","optional":true,"type":"string"},{"name":"parsedOk","description":"Whether the property is understood by the browser (implies `true` if absent).","optional":true,"type":"boolean"},{"name":"disabled","description":"Whether the property is disabled by the user (present for source-based properties only).","optional":true,"type":"boolean"},{"name":"range","description":"The entire property range in the enclosing style declaration (if available).","optional":true,"$ref":"SourceRange"},{"name":"longhandProperties","description":"Parsed longhand components of this property if it is a shorthand.\\nThis field will be empty if the given property is not a shorthand.","experimental":true,"optional":true,"type":"array","items":{"$ref":"CSSProperty"}}]},{"id":"CSSMedia","description":"CSS media rule descriptor.","type":"object","properties":[{"name":"text","description":"Media query text.","type":"string"},{"name":"source","description":"Source of the media query: \\"mediaRule\\" if specified by a @media rule, \\"importRule\\" if\\nspecified by an @import rule, \\"linkedSheet\\" if specified by a \\"media\\" attribute in a linked\\nstylesheet\'s LINK tag, \\"inlineSheet\\" if specified by a \\"media\\" attribute in an inline\\nstylesheet\'s STYLE tag.","type":"string","enum":["mediaRule","importRule","linkedSheet","inlineSheet"]},{"name":"sourceURL","description":"URL of the document containing the media query description.","optional":true,"type":"string"},{"name":"range","description":"The associated rule (@media or @import) header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"mediaList","description":"Array of media queries.","optional":true,"type":"array","items":{"$ref":"MediaQuery"}}]},{"id":"MediaQuery","description":"Media query descriptor.","type":"object","properties":[{"name":"expressions","description":"Array of media query expressions.","type":"array","items":{"$ref":"MediaQueryExpression"}},{"name":"active","description":"Whether the media query condition is satisfied.","type":"boolean"}]},{"id":"MediaQueryExpression","description":"Media query expression descriptor.","type":"object","properties":[{"name":"value","description":"Media query expression value.","type":"number"},{"name":"unit","description":"Media query expression units.","type":"string"},{"name":"feature","description":"Media query expression feature.","type":"string"},{"name":"valueRange","description":"The associated range of the value text in the enclosing stylesheet (if available).","optional":true,"$ref":"SourceRange"},{"name":"computedLength","description":"Computed length of media query expression (if applicable).","optional":true,"type":"number"}]},{"id":"CSSContainerQuery","description":"CSS container query rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Container query text.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"},{"name":"name","description":"Optional name for the container.","optional":true,"type":"string"},{"name":"physicalAxes","description":"Optional physical axes queried for the container.","optional":true,"$ref":"DOM.PhysicalAxes"},{"name":"logicalAxes","description":"Optional logical axes queried for the container.","optional":true,"$ref":"DOM.LogicalAxes"}]},{"id":"CSSSupports","description":"CSS Supports at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Supports rule text.","type":"string"},{"name":"active","description":"Whether the supports condition is satisfied.","type":"boolean"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSScope","description":"CSS Scope at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Scope rule text.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSLayer","description":"CSS Layer at-rule descriptor.","experimental":true,"type":"object","properties":[{"name":"text","description":"Layer name.","type":"string"},{"name":"range","description":"The associated rule header range in the enclosing stylesheet (if\\navailable).","optional":true,"$ref":"SourceRange"},{"name":"styleSheetId","description":"Identifier of the stylesheet containing this object (if exists).","optional":true,"$ref":"StyleSheetId"}]},{"id":"CSSLayerData","description":"CSS Layer data.","experimental":true,"type":"object","properties":[{"name":"name","description":"Layer name.","type":"string"},{"name":"subLayers","description":"Direct sub-layers","optional":true,"type":"array","items":{"$ref":"CSSLayerData"}},{"name":"order","description":"Layer order. The order determines the order of the layer in the cascade order.\\nA higher number has higher priority in the cascade order.","type":"number"}]},{"id":"PlatformFontUsage","description":"Information about amount of glyphs that were rendered with given font.","type":"object","properties":[{"name":"familyName","description":"Font\'s family name reported by platform.","type":"string"},{"name":"isCustomFont","description":"Indicates if the font was downloaded or resolved locally.","type":"boolean"},{"name":"glyphCount","description":"Amount of glyphs that were rendered with this font.","type":"number"}]},{"id":"FontVariationAxis","description":"Information about font variation axes for variable fonts","type":"object","properties":[{"name":"tag","description":"The font-variation-setting tag (a.k.a. \\"axis tag\\").","type":"string"},{"name":"name","description":"Human-readable variation name in the default language (normally, \\"en\\").","type":"string"},{"name":"minValue","description":"The minimum value (inclusive) the font supports for this tag.","type":"number"},{"name":"maxValue","description":"The maximum value (inclusive) the font supports for this tag.","type":"number"},{"name":"defaultValue","description":"The default value.","type":"number"}]},{"id":"FontFace","description":"Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions\\nand additional information such as platformFontFamily and fontVariationAxes.","type":"object","properties":[{"name":"fontFamily","description":"The font-family.","type":"string"},{"name":"fontStyle","description":"The font-style.","type":"string"},{"name":"fontVariant","description":"The font-variant.","type":"string"},{"name":"fontWeight","description":"The font-weight.","type":"string"},{"name":"fontStretch","description":"The font-stretch.","type":"string"},{"name":"fontDisplay","description":"The font-display.","type":"string"},{"name":"unicodeRange","description":"The unicode-range.","type":"string"},{"name":"src","description":"The src.","type":"string"},{"name":"platformFontFamily","description":"The resolved platform font family","type":"string"},{"name":"fontVariationAxes","description":"Available variation settings (a.k.a. \\"axes\\").","optional":true,"type":"array","items":{"$ref":"FontVariationAxis"}}]},{"id":"CSSTryRule","description":"CSS try rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"CSSPositionFallbackRule","description":"CSS position-fallback rule representation.","type":"object","properties":[{"name":"name","$ref":"Value"},{"name":"tryRules","description":"List of keyframes.","type":"array","items":{"$ref":"CSSTryRule"}}]},{"id":"CSSKeyframesRule","description":"CSS keyframes rule representation.","type":"object","properties":[{"name":"animationName","description":"Animation name.","$ref":"Value"},{"name":"keyframes","description":"List of keyframes.","type":"array","items":{"$ref":"CSSKeyframeRule"}}]},{"id":"CSSKeyframeRule","description":"CSS keyframe rule representation.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier (absent for user agent stylesheet and user-specified\\nstylesheet rules) this rule came from.","optional":true,"$ref":"StyleSheetId"},{"name":"origin","description":"Parent stylesheet\'s origin.","$ref":"StyleSheetOrigin"},{"name":"keyText","description":"Associated key text.","$ref":"Value"},{"name":"style","description":"Associated style declaration.","$ref":"CSSStyle"}]},{"id":"StyleDeclarationEdit","description":"A descriptor of operation to mutate style declaration text.","type":"object","properties":[{"name":"styleSheetId","description":"The css style sheet identifier.","$ref":"StyleSheetId"},{"name":"range","description":"The range of the style text in the enclosing stylesheet.","$ref":"SourceRange"},{"name":"text","description":"New style text.","type":"string"}]}],"commands":[{"name":"addRule","description":"Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\\nposition specified by `location`.","parameters":[{"name":"styleSheetId","description":"The css style sheet identifier where a new rule should be inserted.","$ref":"StyleSheetId"},{"name":"ruleText","description":"The text of a new rule.","type":"string"},{"name":"location","description":"Text position of a new rule in the target style sheet.","$ref":"SourceRange"}],"returns":[{"name":"rule","description":"The newly created rule.","$ref":"CSSRule"}]},{"name":"collectClassNames","description":"Returns all class names from specified stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"createStyleSheet","description":"Creates a new special \\"via-inspector\\" stylesheet in the frame with given `frameId`.","parameters":[{"name":"frameId","description":"Identifier of the frame where \\"via-inspector\\" stylesheet should be created.","$ref":"Page.FrameId"}],"returns":[{"name":"styleSheetId","description":"Identifier of the created \\"via-inspector\\" stylesheet.","$ref":"StyleSheetId"}]},{"name":"disable","description":"Disables the CSS agent for the given page."},{"name":"enable","description":"Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\\nenabled until the result of this command is received."},{"name":"forcePseudoState","description":"Ensures that the given node will have specified pseudo-classes whenever its style is computed by\\nthe browser.","parameters":[{"name":"nodeId","description":"The element id for which to force the pseudo state.","$ref":"DOM.NodeId"},{"name":"forcedPseudoClasses","description":"Element pseudo classes to force when computing the element\'s style.","type":"array","items":{"type":"string"}}]},{"name":"getBackgroundColors","parameters":[{"name":"nodeId","description":"Id of the node to get background colors for.","$ref":"DOM.NodeId"}],"returns":[{"name":"backgroundColors","description":"The range of background colors behind this element, if it contains any visible text. If no\\nvisible text is present, this will be undefined. In the case of a flat background color,\\nthis will consist of simply that color. In the case of a gradient, this will consist of each\\nof the color stops. For anything more complicated, this will be an empty array. Images will\\nbe ignored (as if the image had failed to load).","optional":true,"type":"array","items":{"type":"string"}},{"name":"computedFontSize","description":"The computed font size for this node, as a CSS computed value string (e.g. \'12px\').","optional":true,"type":"string"},{"name":"computedFontWeight","description":"The computed font weight for this node, as a CSS computed value string (e.g. \'normal\' or\\n\'100\').","optional":true,"type":"string"}]},{"name":"getComputedStyleForNode","description":"Returns the computed style for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"computedStyle","description":"Computed style for the specified DOM node.","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"getInlineStylesForNode","description":"Returns the styles defined inline (explicitly in the \\"style\\" attribute and implicitly, using DOM\\nattributes) for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"}]},{"name":"getMatchedStylesForNode","description":"Returns requested styles for a DOM node identified by `nodeId`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"inlineStyle","description":"Inline style for the specified DOM node.","optional":true,"$ref":"CSSStyle"},{"name":"attributesStyle","description":"Attribute-defined element style (e.g. resulting from \\"width=20 height=100%\\").","optional":true,"$ref":"CSSStyle"},{"name":"matchedCSSRules","description":"CSS rules matching this node, from all applicable stylesheets.","optional":true,"type":"array","items":{"$ref":"RuleMatch"}},{"name":"pseudoElements","description":"Pseudo style matches for this node.","optional":true,"type":"array","items":{"$ref":"PseudoElementMatches"}},{"name":"inherited","description":"A chain of inherited styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedStyleEntry"}},{"name":"inheritedPseudoElements","description":"A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root).","optional":true,"type":"array","items":{"$ref":"InheritedPseudoElementMatches"}},{"name":"cssKeyframesRules","description":"A list of CSS keyframed animations matching this node.","optional":true,"type":"array","items":{"$ref":"CSSKeyframesRule"}},{"name":"cssPositionFallbackRules","description":"A list of CSS position fallbacks matching this node.","optional":true,"type":"array","items":{"$ref":"CSSPositionFallbackRule"}},{"name":"parentLayoutNodeId","description":"Id of the first parent element that does not have display: contents.","experimental":true,"optional":true,"$ref":"DOM.NodeId"}]},{"name":"getMediaQueries","description":"Returns all media queries parsed by the rendering engine.","returns":[{"name":"medias","type":"array","items":{"$ref":"CSSMedia"}}]},{"name":"getPlatformFontsForNode","description":"Requests information about platform fonts which we used to render child TextNodes in the given\\nnode.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"fonts","description":"Usage statistics for every employed platform font.","type":"array","items":{"$ref":"PlatformFontUsage"}}]},{"name":"getStyleSheetText","description":"Returns the current textual content for a stylesheet.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}],"returns":[{"name":"text","description":"The stylesheet text.","type":"string"}]},{"name":"getLayersForNode","description":"Returns all layers parsed by the rendering engine for the tree scope of a node.\\nGiven a DOM element identified by nodeId, getLayersForNode returns the root\\nlayer for the nearest ancestor document or shadow root. The layer root contains\\nthe full layer tree for the tree scope and their ordering.","experimental":true,"parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}],"returns":[{"name":"rootLayer","$ref":"CSSLayerData"}]},{"name":"trackComputedStyleUpdates","description":"Starts tracking the given computed styles for updates. The specified array of properties\\nreplaces the one previously specified. Pass empty array to disable tracking.\\nUse takeComputedStyleUpdates to retrieve the list of nodes that had properties modified.\\nThe changes to computed style properties are only tracked for nodes pushed to the front-end\\nby the DOM agent. If no changes to the tracked properties occur after the node has been pushed\\nto the front-end, no updates will be issued for the node.","experimental":true,"parameters":[{"name":"propertiesToTrack","type":"array","items":{"$ref":"CSSComputedStyleProperty"}}]},{"name":"takeComputedStyleUpdates","description":"Polls the next batch of computed style updates.","experimental":true,"returns":[{"name":"nodeIds","description":"The list of node Ids that have their tracked computed styles updated.","type":"array","items":{"$ref":"DOM.NodeId"}}]},{"name":"setEffectivePropertyValueForNode","description":"Find a rule with the given active property for the given node and set the new value for this\\nproperty","parameters":[{"name":"nodeId","description":"The element id for which to set property.","$ref":"DOM.NodeId"},{"name":"propertyName","type":"string"},{"name":"value","type":"string"}]},{"name":"setKeyframeKey","description":"Modifies the keyframe rule key text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"keyText","type":"string"}],"returns":[{"name":"keyText","description":"The resulting key text after modification.","$ref":"Value"}]},{"name":"setMediaText","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"media","description":"The resulting CSS media rule after modification.","$ref":"CSSMedia"}]},{"name":"setContainerQueryText","description":"Modifies the expression of a container query.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"containerQuery","description":"The resulting CSS container query rule after modification.","$ref":"CSSContainerQuery"}]},{"name":"setSupportsText","description":"Modifies the expression of a supports at-rule.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"supports","description":"The resulting CSS Supports rule after modification.","$ref":"CSSSupports"}]},{"name":"setScopeText","description":"Modifies the expression of a scope at-rule.","experimental":true,"parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"text","type":"string"}],"returns":[{"name":"scope","description":"The resulting CSS Scope rule after modification.","$ref":"CSSScope"}]},{"name":"setRuleSelector","description":"Modifies the rule selector.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"range","$ref":"SourceRange"},{"name":"selector","type":"string"}],"returns":[{"name":"selectorList","description":"The resulting selector list after modification.","$ref":"SelectorList"}]},{"name":"setStyleSheetText","description":"Sets the new stylesheet text.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"},{"name":"text","type":"string"}],"returns":[{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"}]},{"name":"setStyleTexts","description":"Applies specified style edits one after another in the given order.","parameters":[{"name":"edits","type":"array","items":{"$ref":"StyleDeclarationEdit"}}],"returns":[{"name":"styles","description":"The resulting styles after modification.","type":"array","items":{"$ref":"CSSStyle"}}]},{"name":"startRuleUsageTracking","description":"Enables the selector recording."},{"name":"stopRuleUsageTracking","description":"Stop tracking rule usage and return the list of rules that were used since last call to\\n`takeCoverageDelta` (or since start of coverage instrumentation).","returns":[{"name":"ruleUsage","type":"array","items":{"$ref":"RuleUsage"}}]},{"name":"takeCoverageDelta","description":"Obtain list of rules that became used since last call to this method (or since start of coverage\\ninstrumentation).","returns":[{"name":"coverage","type":"array","items":{"$ref":"RuleUsage"}},{"name":"timestamp","description":"Monotonically increasing time, in seconds.","type":"number"}]},{"name":"setLocalFontsEnabled","description":"Enables/disables rendering of local CSS fonts (enabled by default).","experimental":true,"parameters":[{"name":"enabled","description":"Whether rendering of local fonts is enabled.","type":"boolean"}]}],"events":[{"name":"fontsUpdated","description":"Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\\nweb font.","parameters":[{"name":"font","description":"The web font that has loaded.","optional":true,"$ref":"FontFace"}]},{"name":"mediaQueryResultChanged","description":"Fires whenever a MediaQuery result changes (for example, after a browser window has been\\nresized.) The current implementation considers only viewport-dependent media features."},{"name":"styleSheetAdded","description":"Fired whenever an active document stylesheet is added.","parameters":[{"name":"header","description":"Added stylesheet metainfo.","$ref":"CSSStyleSheetHeader"}]},{"name":"styleSheetChanged","description":"Fired whenever a stylesheet is changed as a result of the client operation.","parameters":[{"name":"styleSheetId","$ref":"StyleSheetId"}]},{"name":"styleSheetRemoved","description":"Fired whenever an active document stylesheet is removed.","parameters":[{"name":"styleSheetId","description":"Identifier of the removed stylesheet.","$ref":"StyleSheetId"}]}]},{"domain":"CacheStorage","experimental":true,"dependencies":["Storage"],"types":[{"id":"CacheId","description":"Unique identifier of the Cache object.","type":"string"},{"id":"CachedResponseType","description":"type of HTTP response cached","type":"string","enum":["basic","cors","default","error","opaqueResponse","opaqueRedirect"]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"requestURL","description":"Request URL.","type":"string"},{"name":"requestMethod","description":"Request method.","type":"string"},{"name":"requestHeaders","description":"Request headers","type":"array","items":{"$ref":"Header"}},{"name":"responseTime","description":"Number of seconds since epoch.","type":"number"},{"name":"responseStatus","description":"HTTP response status code.","type":"integer"},{"name":"responseStatusText","description":"HTTP response status text.","type":"string"},{"name":"responseType","description":"HTTP response type","$ref":"CachedResponseType"},{"name":"responseHeaders","description":"Response headers","type":"array","items":{"$ref":"Header"}}]},{"id":"Cache","description":"Cache identifier.","type":"object","properties":[{"name":"cacheId","description":"An opaque unique id of the cache.","$ref":"CacheId"},{"name":"securityOrigin","description":"Security origin of the cache.","type":"string"},{"name":"storageKey","description":"Storage key of the cache.","type":"string"},{"name":"storageBucket","description":"Storage bucket of the cache.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"cacheName","description":"The name of the cache.","type":"string"}]},{"id":"Header","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"CachedResponse","description":"Cached response","type":"object","properties":[{"name":"body","description":"Entry content, base64-encoded. (Encoded as a base64 string when passed over JSON)","type":"string"}]}],"commands":[{"name":"deleteCache","description":"Deletes a cache.","parameters":[{"name":"cacheId","description":"Id of cache for deletion.","$ref":"CacheId"}]},{"name":"deleteEntry","description":"Deletes a cache entry.","parameters":[{"name":"cacheId","description":"Id of cache where the entry will be deleted.","$ref":"CacheId"},{"name":"request","description":"URL spec of the request.","type":"string"}]},{"name":"requestCacheNames","description":"Requests cache names.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"}],"returns":[{"name":"caches","description":"Caches for the security origin.","type":"array","items":{"$ref":"Cache"}}]},{"name":"requestCachedResponse","description":"Fetches cache entry.","parameters":[{"name":"cacheId","description":"Id of cache that contains the entry.","$ref":"CacheId"},{"name":"requestURL","description":"URL spec of the request.","type":"string"},{"name":"requestHeaders","description":"headers of the request.","type":"array","items":{"$ref":"Header"}}],"returns":[{"name":"response","description":"Response read from the cache.","$ref":"CachedResponse"}]},{"name":"requestEntries","description":"Requests data from cache.","parameters":[{"name":"cacheId","description":"ID of cache to get entries from.","$ref":"CacheId"},{"name":"skipCount","description":"Number of records to skip.","optional":true,"type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","optional":true,"type":"integer"},{"name":"pathFilter","description":"If present, only return the entries containing this substring in the path","optional":true,"type":"string"}],"returns":[{"name":"cacheDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"returnCount","description":"Count of returned entries from this storage. If pathFilter is empty, it\\nis the count of all entries from this storage.","type":"number"}]}]},{"domain":"Cast","description":"A domain for interacting with Cast, Presentation API, and Remote Playback API\\nfunctionalities.","experimental":true,"types":[{"id":"Sink","type":"object","properties":[{"name":"name","type":"string"},{"name":"id","type":"string"},{"name":"session","description":"Text describing the current session. Present only if there is an active\\nsession on the sink.","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Starts observing for sinks that can be used for tab mirroring, and if set,\\nsinks compatible with |presentationUrl| as well. When sinks are found, a\\n|sinksUpdated| event is fired.\\nAlso starts observing for issue messages. When an issue is added or removed,\\nan |issueUpdated| event is fired.","parameters":[{"name":"presentationUrl","optional":true,"type":"string"}]},{"name":"disable","description":"Stops observing for sinks and issues."},{"name":"setSinkToUse","description":"Sets a sink to be used when the web page requests the browser to choose a\\nsink via Presentation API, Remote Playback API, or Cast SDK.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startDesktopMirroring","description":"Starts mirroring the desktop to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"startTabMirroring","description":"Starts mirroring the tab to the sink.","parameters":[{"name":"sinkName","type":"string"}]},{"name":"stopCasting","description":"Stops the active Cast session on the sink.","parameters":[{"name":"sinkName","type":"string"}]}],"events":[{"name":"sinksUpdated","description":"This is fired whenever the list of available sinks changes. A sink is a\\ndevice or a software surface that you can cast to.","parameters":[{"name":"sinks","type":"array","items":{"$ref":"Sink"}}]},{"name":"issueUpdated","description":"This is fired whenever the outstanding issue/error message changes.\\n|issueMessage| is empty if there is no issue.","parameters":[{"name":"issueMessage","type":"string"}]}]},{"domain":"DOM","description":"This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\\nand never sends the same node twice. It is client\'s responsibility to collect information about\\nthe nodes that were sent to the client. Note that `iframe` owner elements will return\\ncorresponding document elements as their child nodes.","dependencies":["Runtime"],"types":[{"id":"NodeId","description":"Unique DOM node identifier.","type":"integer"},{"id":"BackendNodeId","description":"Unique DOM node identifier used to reference a node that may not have been pushed to the\\nfront-end.","type":"integer"},{"id":"BackendNode","description":"Backend node with a friendly name.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"backendNodeId","$ref":"BackendNodeId"}]},{"id":"PseudoType","description":"Pseudo element type.","type":"string","enum":["first-line","first-letter","before","after","marker","backdrop","selection","target-text","spelling-error","grammar-error","highlight","first-line-inherited","scrollbar","scrollbar-thumb","scrollbar-button","scrollbar-track","scrollbar-track-piece","scrollbar-corner","resizer","input-list-button","view-transition","view-transition-group","view-transition-image-pair","view-transition-old","view-transition-new"]},{"id":"ShadowRootType","description":"Shadow root type.","type":"string","enum":["user-agent","open","closed"]},{"id":"CompatibilityMode","description":"Document compatibility mode.","type":"string","enum":["QuirksMode","LimitedQuirksMode","NoQuirksMode"]},{"id":"PhysicalAxes","description":"ContainerSelector physical axes","type":"string","enum":["Horizontal","Vertical","Both"]},{"id":"LogicalAxes","description":"ContainerSelector logical axes","type":"string","enum":["Inline","Block","Both"]},{"id":"Node","description":"DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\\nDOMNode is a base node mirror type.","type":"object","properties":[{"name":"nodeId","description":"Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\\nwill only push node with given `id` once. It is aware of all requested nodes and will only\\nfire DOM events for nodes known to the client.","$ref":"NodeId"},{"name":"parentId","description":"The id of the parent node if any.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"The BackendNodeId for this node.","$ref":"BackendNodeId"},{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"localName","description":"`Node`\'s localName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"childNodeCount","description":"Child count for `Container` nodes.","optional":true,"type":"integer"},{"name":"children","description":"Child nodes of this node when requested with children.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"attributes","description":"Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.","optional":true,"type":"array","items":{"type":"string"}},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType`\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType`\'s systemId.","optional":true,"type":"string"},{"name":"internalSubset","description":"`DocumentType`\'s internalSubset.","optional":true,"type":"string"},{"name":"xmlVersion","description":"`Document`\'s XML version in case of XML documents.","optional":true,"type":"string"},{"name":"name","description":"`Attr`\'s name.","optional":true,"type":"string"},{"name":"value","description":"`Attr`\'s value.","optional":true,"type":"string"},{"name":"pseudoType","description":"Pseudo element type for this node.","optional":true,"$ref":"PseudoType"},{"name":"pseudoIdentifier","description":"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.","optional":true,"type":"string"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"ShadowRootType"},{"name":"frameId","description":"Frame ID for frame owner elements.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocument","description":"Content document for frame owner elements.","optional":true,"$ref":"Node"},{"name":"shadowRoots","description":"Shadow root list for given element host.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"templateContent","description":"Content document fragment for template elements.","optional":true,"$ref":"Node"},{"name":"pseudoElements","description":"Pseudo elements associated with this node.","optional":true,"type":"array","items":{"$ref":"Node"}},{"name":"importedDocument","description":"Deprecated, as the HTML Imports API has been removed (crbug.com/937746).\\nThis property used to return the imported document for the HTMLImport links.\\nThe property is always undefined now.","deprecated":true,"optional":true,"$ref":"Node"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","optional":true,"type":"array","items":{"$ref":"BackendNode"}},{"name":"isSVG","description":"Whether the node is SVG.","optional":true,"type":"boolean"},{"name":"compatibilityMode","optional":true,"$ref":"CompatibilityMode"},{"name":"assignedSlot","optional":true,"$ref":"BackendNode"}]},{"id":"RGBA","description":"A structure holding an RGBA color.","type":"object","properties":[{"name":"r","description":"The red component, in the [0-255] range.","type":"integer"},{"name":"g","description":"The green component, in the [0-255] range.","type":"integer"},{"name":"b","description":"The blue component, in the [0-255] range.","type":"integer"},{"name":"a","description":"The alpha component, in the [0-1] range (default: 1).","optional":true,"type":"number"}]},{"id":"Quad","description":"An array of quad vertices, x immediately followed by y for each point, points clock-wise.","type":"array","items":{"type":"number"}},{"id":"BoxModel","description":"Box model.","type":"object","properties":[{"name":"content","description":"Content box","$ref":"Quad"},{"name":"padding","description":"Padding box","$ref":"Quad"},{"name":"border","description":"Border box","$ref":"Quad"},{"name":"margin","description":"Margin box","$ref":"Quad"},{"name":"width","description":"Node width","type":"integer"},{"name":"height","description":"Node height","type":"integer"},{"name":"shapeOutside","description":"Shape outside coordinates","optional":true,"$ref":"ShapeOutsideInfo"}]},{"id":"ShapeOutsideInfo","description":"CSS Shape Outside details.","type":"object","properties":[{"name":"bounds","description":"Shape bounds","$ref":"Quad"},{"name":"shape","description":"Shape coordinate details","type":"array","items":{"type":"any"}},{"name":"marginShape","description":"Margin shape bounds","type":"array","items":{"type":"any"}}]},{"id":"Rect","description":"Rectangle.","type":"object","properties":[{"name":"x","description":"X coordinate","type":"number"},{"name":"y","description":"Y coordinate","type":"number"},{"name":"width","description":"Rectangle width","type":"number"},{"name":"height","description":"Rectangle height","type":"number"}]},{"id":"CSSComputedStyleProperty","type":"object","properties":[{"name":"name","description":"Computed style property name.","type":"string"},{"name":"value","description":"Computed style property value.","type":"string"}]}],"commands":[{"name":"collectClassNamesFromSubtree","description":"Collects class names for the node with given id and all of it\'s child nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to collect class names.","$ref":"NodeId"}],"returns":[{"name":"classNames","description":"Class name list.","type":"array","items":{"type":"string"}}]},{"name":"copyTo","description":"Creates a deep copy of the specified node and places it into the target container before the\\ngiven anchor.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to copy.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the copy into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop the copy before this node (if absent, the copy becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Id of the node clone.","$ref":"NodeId"}]},{"name":"describeNode","description":"Describes node given its id, does not require domain to be enabled. Does not start tracking any\\nobjects, can be used for automation.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"node","description":"Node description.","$ref":"Node"}]},{"name":"scrollIntoViewIfNeeded","description":"Scrolls the specified rect of the given node into view if not already visible.\\nNote: exactly one between nodeId, backendNodeId and objectId should be passed\\nto identify the node.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"rect","description":"The rect to be scrolled into view, relative to the node\'s border box, in CSS pixels.\\nWhen omitted, center of the node will be used, similar to Element.scrollIntoView.","optional":true,"$ref":"Rect"}]},{"name":"disable","description":"Disables DOM agent for the given page."},{"name":"discardSearchResults","description":"Discards search results from the session with the given id. `getSearchResults` should no longer\\nbe called for that search.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"}]},{"name":"enable","description":"Enables DOM agent for the given page.","parameters":[{"name":"includeWhitespace","description":"Whether to include whitespaces in the children array of returned Nodes.","experimental":true,"optional":true,"type":"string","enum":["none","all"]}]},{"name":"focus","description":"Focuses the given element.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"getAttributes","description":"Returns attributes for the specified node.","parameters":[{"name":"nodeId","description":"Id of the node to retrieve attibutes for.","$ref":"NodeId"}],"returns":[{"name":"attributes","description":"An interleaved array of node attribute names and values.","type":"array","items":{"type":"string"}}]},{"name":"getBoxModel","description":"Returns boxes for the given node.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"model","description":"Box model for the node.","$ref":"BoxModel"}]},{"name":"getContentQuads","description":"Returns quads that describe node position on the page. This method\\nmight return multiple quads for inline nodes.","experimental":true,"parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"quads","description":"Quads that describe node layout relative to viewport.","type":"array","items":{"$ref":"Quad"}}]},{"name":"getDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.\\nImplicitly enables the DOM domain events for the current target.","parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"root","description":"Resulting node.","$ref":"Node"}]},{"name":"getFlattenedDocument","description":"Returns the root DOM node (and optionally the subtree) to the caller.\\nDeprecated, as it is not designed to work well with the rest of the DOM agent.\\nUse DOMSnapshot.captureSnapshot instead.","deprecated":true,"parameters":[{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodes","description":"Resulting node.","type":"array","items":{"$ref":"Node"}}]},{"name":"getNodesForSubtreeByStyle","description":"Finds nodes with a given computed style in a subtree.","experimental":true,"parameters":[{"name":"nodeId","description":"Node ID pointing to the root of a subtree.","$ref":"NodeId"},{"name":"computedStyles","description":"The style to filter nodes by (includes nodes if any of properties matches).","type":"array","items":{"$ref":"CSSComputedStyleProperty"}},{"name":"pierce","description":"Whether or not iframes and shadow roots in the same target should be traversed when returning the\\nresults (default is false).","optional":true,"type":"boolean"}],"returns":[{"name":"nodeIds","description":"Resulting nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"getNodeForLocation","description":"Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\\neither returned or not.","parameters":[{"name":"x","description":"X coordinate.","type":"integer"},{"name":"y","description":"Y coordinate.","type":"integer"},{"name":"includeUserAgentShadowDOM","description":"False to skip to the nearest non-UA shadow root ancestor (default: false).","optional":true,"type":"boolean"},{"name":"ignorePointerEventsNone","description":"Whether to ignore pointer-events: none on elements and hit test them.","optional":true,"type":"boolean"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"frameId","description":"Frame this node belongs to.","$ref":"Page.FrameId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getOuterHTML","description":"Returns node\'s HTML markup.","parameters":[{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"outerHTML","description":"Outer HTML markup.","type":"string"}]},{"name":"getRelayoutBoundary","description":"Returns the id of the nearest ancestor that is a relayout boundary.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node.","$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"Relayout boundary node id for the given node.","$ref":"NodeId"}]},{"name":"getSearchResults","description":"Returns search results from given `fromIndex` to given `toIndex` from the search with the given\\nidentifier.","experimental":true,"parameters":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"fromIndex","description":"Start index of the search result to be returned.","type":"integer"},{"name":"toIndex","description":"End index of the search result to be returned.","type":"integer"}],"returns":[{"name":"nodeIds","description":"Ids of the search result nodes.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"hideHighlight","description":"Hides any highlight.","redirect":"Overlay"},{"name":"highlightNode","description":"Highlights DOM node.","redirect":"Overlay"},{"name":"highlightRect","description":"Highlights given rectangle.","redirect":"Overlay"},{"name":"markUndoableState","description":"Marks last undoable state.","experimental":true},{"name":"moveTo","description":"Moves node into the new container, places it before the given anchor.","parameters":[{"name":"nodeId","description":"Id of the node to move.","$ref":"NodeId"},{"name":"targetNodeId","description":"Id of the element to drop the moved node into.","$ref":"NodeId"},{"name":"insertBeforeNodeId","description":"Drop node before this one (if absent, the moved node becomes the last child of\\n`targetNodeId`).","optional":true,"$ref":"NodeId"}],"returns":[{"name":"nodeId","description":"New id of the moved node.","$ref":"NodeId"}]},{"name":"performSearch","description":"Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\\n`cancelSearch` to end this search session.","experimental":true,"parameters":[{"name":"query","description":"Plain text or query selector or XPath search query.","type":"string"},{"name":"includeUserAgentShadowDOM","description":"True to search in user agent shadow DOM.","optional":true,"type":"boolean"}],"returns":[{"name":"searchId","description":"Unique search session identifier.","type":"string"},{"name":"resultCount","description":"Number of search results.","type":"integer"}]},{"name":"pushNodeByPathToFrontend","description":"Requests that the node is sent to the caller given its path. // FIXME, use XPath","experimental":true,"parameters":[{"name":"path","description":"Path to node in the proprietary format.","type":"string"}],"returns":[{"name":"nodeId","description":"Id of the node for given path.","$ref":"NodeId"}]},{"name":"pushNodesByBackendIdsToFrontend","description":"Requests that a batch of nodes is sent to the caller given their backend node ids.","experimental":true,"parameters":[{"name":"backendNodeIds","description":"The array of backend node ids.","type":"array","items":{"$ref":"BackendNodeId"}}],"returns":[{"name":"nodeIds","description":"The array of ids of pushed nodes that correspond to the backend ids specified in\\nbackendNodeIds.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"querySelector","description":"Executes `querySelector` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeId","description":"Query selector result.","$ref":"NodeId"}]},{"name":"querySelectorAll","description":"Executes `querySelectorAll` on a given node.","parameters":[{"name":"nodeId","description":"Id of the node to query upon.","$ref":"NodeId"},{"name":"selector","description":"Selector string.","type":"string"}],"returns":[{"name":"nodeIds","description":"Query selector result.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"getTopLayerElements","description":"Returns NodeIds of current top layer elements.\\nTop layer is rendered closest to the user within a viewport, therefore its elements always\\nappear on top of all other content.","experimental":true,"returns":[{"name":"nodeIds","description":"NodeIds of top layer elements","type":"array","items":{"$ref":"NodeId"}}]},{"name":"redo","description":"Re-does the last undone action.","experimental":true},{"name":"removeAttribute","description":"Removes attribute with given name from an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to remove attribute from.","$ref":"NodeId"},{"name":"name","description":"Name of the attribute to remove.","type":"string"}]},{"name":"removeNode","description":"Removes node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to remove.","$ref":"NodeId"}]},{"name":"requestChildNodes","description":"Requests that children of the node with given id are returned to the caller in form of\\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\\nthe specified depth.","parameters":[{"name":"nodeId","description":"Id of the node to get children for.","$ref":"NodeId"},{"name":"depth","description":"The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the sub-tree\\n(default is false).","optional":true,"type":"boolean"}]},{"name":"requestNode","description":"Requests that the node is sent to the caller given the JavaScript node object reference. All\\nnodes that form the path from the node to the root are also sent to the client as a series of\\n`setChildNodes` notifications.","parameters":[{"name":"objectId","description":"JavaScript object id to convert into node.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"nodeId","description":"Node id for given object.","$ref":"NodeId"}]},{"name":"resolveNode","description":"Resolves the JavaScript node object for a given NodeId or BackendNodeId.","parameters":[{"name":"nodeId","description":"Id of the node to resolve.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Backend identifier of the node to resolve.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"executionContextId","description":"Execution context in which to resolve the node.","optional":true,"$ref":"Runtime.ExecutionContextId"}],"returns":[{"name":"object","description":"JavaScript object wrapper for given node.","$ref":"Runtime.RemoteObject"}]},{"name":"setAttributeValue","description":"Sets attribute for an element with given id.","parameters":[{"name":"nodeId","description":"Id of the element to set attribute for.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"setAttributesAsText","description":"Sets attributes on element with given id. This method is useful when user edits some existing\\nattribute value and types in several attribute name/value pairs.","parameters":[{"name":"nodeId","description":"Id of the element to set attributes for.","$ref":"NodeId"},{"name":"text","description":"Text with a number of attributes. Will parse this text using HTML parser.","type":"string"},{"name":"name","description":"Attribute name to replace with new attributes derived from text in case text parsed\\nsuccessfully.","optional":true,"type":"string"}]},{"name":"setFileInputFiles","description":"Sets files for the given file input element.","parameters":[{"name":"files","description":"Array of file paths to set.","type":"array","items":{"type":"string"}},{"name":"nodeId","description":"Identifier of the node.","optional":true,"$ref":"NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node.","optional":true,"$ref":"BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node wrapper.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"setNodeStackTracesEnabled","description":"Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled.","experimental":true,"parameters":[{"name":"enable","description":"Enable or disable.","type":"boolean"}]},{"name":"getNodeStackTraces","description":"Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the node to get stack traces for.","$ref":"NodeId"}],"returns":[{"name":"creation","description":"Creation stack trace, if available.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"getFileInfo","description":"Returns file information for the given\\nFile wrapper.","experimental":true,"parameters":[{"name":"objectId","description":"JavaScript object id of the node wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"path","type":"string"}]},{"name":"setInspectedNode","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","experimental":true,"parameters":[{"name":"nodeId","description":"DOM node id to be accessible by means of $x command line API.","$ref":"NodeId"}]},{"name":"setNodeName","description":"Sets node name for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set name for.","$ref":"NodeId"},{"name":"name","description":"New node\'s name.","type":"string"}],"returns":[{"name":"nodeId","description":"New node\'s id.","$ref":"NodeId"}]},{"name":"setNodeValue","description":"Sets node value for a node with given id.","parameters":[{"name":"nodeId","description":"Id of the node to set value for.","$ref":"NodeId"},{"name":"value","description":"New node\'s value.","type":"string"}]},{"name":"setOuterHTML","description":"Sets node HTML markup, returns new node id.","parameters":[{"name":"nodeId","description":"Id of the node to set markup for.","$ref":"NodeId"},{"name":"outerHTML","description":"Outer HTML markup to set.","type":"string"}]},{"name":"undo","description":"Undoes the last performed action.","experimental":true},{"name":"getFrameOwner","description":"Returns iframe node that owns iframe with the given domain.","experimental":true,"parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"backendNodeId","description":"Resulting node.","$ref":"BackendNodeId"},{"name":"nodeId","description":"Id of the node at given coordinates, only when enabled and requested document.","optional":true,"$ref":"NodeId"}]},{"name":"getContainerForNode","description":"Returns the query container of the given node based on container query\\nconditions: containerName, physical, and logical axes. If no axes are\\nprovided, the style container is returned, which is the direct parent or the\\nclosest element with a matching container-name.","experimental":true,"parameters":[{"name":"nodeId","$ref":"NodeId"},{"name":"containerName","optional":true,"type":"string"},{"name":"physicalAxes","optional":true,"$ref":"PhysicalAxes"},{"name":"logicalAxes","optional":true,"$ref":"LogicalAxes"}],"returns":[{"name":"nodeId","description":"The container node for the given node, or null if not found.","optional":true,"$ref":"NodeId"}]},{"name":"getQueryingDescendantsForContainer","description":"Returns the descendants of a container query container that have\\ncontainer queries against this container.","experimental":true,"parameters":[{"name":"nodeId","description":"Id of the container node to find querying descendants from.","$ref":"NodeId"}],"returns":[{"name":"nodeIds","description":"Descendant nodes with container queries against the given container.","type":"array","items":{"$ref":"NodeId"}}]}],"events":[{"name":"attributeModified","description":"Fired when `Element`\'s attribute is modified.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"Attribute name.","type":"string"},{"name":"value","description":"Attribute value.","type":"string"}]},{"name":"attributeRemoved","description":"Fired when `Element`\'s attribute is removed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"name","description":"A ttribute name.","type":"string"}]},{"name":"characterDataModified","description":"Mirrors `DOMCharacterDataModified` event.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"characterData","description":"New text value.","type":"string"}]},{"name":"childNodeCountUpdated","description":"Fired when `Container`\'s child node count has changed.","parameters":[{"name":"nodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"childNodeCount","description":"New node count.","type":"integer"}]},{"name":"childNodeInserted","description":"Mirrors `DOMNodeInserted` event.","parameters":[{"name":"parentNodeId","description":"Id of the node that has changed.","$ref":"NodeId"},{"name":"previousNodeId","description":"Id of the previous sibling.","$ref":"NodeId"},{"name":"node","description":"Inserted node data.","$ref":"Node"}]},{"name":"childNodeRemoved","description":"Mirrors `DOMNodeRemoved` event.","parameters":[{"name":"parentNodeId","description":"Parent id.","$ref":"NodeId"},{"name":"nodeId","description":"Id of the node that has been removed.","$ref":"NodeId"}]},{"name":"distributedNodesUpdated","description":"Called when distribution is changed.","experimental":true,"parameters":[{"name":"insertionPointId","description":"Insertion point where distributed nodes were updated.","$ref":"NodeId"},{"name":"distributedNodes","description":"Distributed nodes for given insertion point.","type":"array","items":{"$ref":"BackendNode"}}]},{"name":"documentUpdated","description":"Fired when `Document` has been totally updated. Node ids are no longer valid."},{"name":"inlineStyleInvalidated","description":"Fired when `Element`\'s inline style is modified via a CSS property modification.","experimental":true,"parameters":[{"name":"nodeIds","description":"Ids of the nodes for which the inline styles have been invalidated.","type":"array","items":{"$ref":"NodeId"}}]},{"name":"pseudoElementAdded","description":"Called when a pseudo element is added to an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElement","description":"The added pseudo element.","$ref":"Node"}]},{"name":"topLayerElementsUpdated","description":"Called when top layer elements are changed.","experimental":true},{"name":"pseudoElementRemoved","description":"Called when a pseudo element is removed from an element.","experimental":true,"parameters":[{"name":"parentId","description":"Pseudo element\'s parent element id.","$ref":"NodeId"},{"name":"pseudoElementId","description":"The removed pseudo element id.","$ref":"NodeId"}]},{"name":"setChildNodes","description":"Fired when backend wants to provide client with the missing DOM structure. This happens upon\\nmost of the calls requesting node ids.","parameters":[{"name":"parentId","description":"Parent node id to populate with children.","$ref":"NodeId"},{"name":"nodes","description":"Child nodes array.","type":"array","items":{"$ref":"Node"}}]},{"name":"shadowRootPopped","description":"Called when shadow root is popped from the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"rootId","description":"Shadow root id.","$ref":"NodeId"}]},{"name":"shadowRootPushed","description":"Called when shadow root is pushed into the element.","experimental":true,"parameters":[{"name":"hostId","description":"Host element id.","$ref":"NodeId"},{"name":"root","description":"Shadow root.","$ref":"Node"}]}]},{"domain":"DOMDebugger","description":"DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\\nexecution will stop on these operations as if there was a regular breakpoint set.","dependencies":["DOM","Debugger","Runtime"],"types":[{"id":"DOMBreakpointType","description":"DOM breakpoint type.","type":"string","enum":["subtree-modified","attribute-modified","node-removed"]},{"id":"CSPViolationType","description":"CSP Violation type.","experimental":true,"type":"string","enum":["trustedtype-sink-violation","trustedtype-policy-violation"]},{"id":"EventListener","description":"Object event listener.","type":"object","properties":[{"name":"type","description":"`EventListener`\'s type.","type":"string"},{"name":"useCapture","description":"`EventListener`\'s useCapture.","type":"boolean"},{"name":"passive","description":"`EventListener`\'s passive flag.","type":"boolean"},{"name":"once","description":"`EventListener`\'s once flag.","type":"boolean"},{"name":"scriptId","description":"Script id of the handler code.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","type":"integer"},{"name":"handler","description":"Event handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"originalHandler","description":"Event original handler function value.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"backendNodeId","description":"Node the listener is added to (if any).","optional":true,"$ref":"DOM.BackendNodeId"}]}],"commands":[{"name":"getEventListeners","description":"Returns event listeners of the given object.","parameters":[{"name":"objectId","description":"Identifier of the object to return listeners for.","$ref":"Runtime.RemoteObjectId"},{"name":"depth","description":"The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\\nentire subtree or provide an integer larger than 0.","optional":true,"type":"integer"},{"name":"pierce","description":"Whether or not iframes and shadow roots should be traversed when returning the subtree\\n(default is false). Reports listeners for all contexts if pierce is enabled.","optional":true,"type":"boolean"}],"returns":[{"name":"listeners","description":"Array of relevant listeners.","type":"array","items":{"$ref":"EventListener"}}]},{"name":"removeDOMBreakpoint","description":"Removes DOM breakpoint that was set using `setDOMBreakpoint`.","parameters":[{"name":"nodeId","description":"Identifier of the node to remove breakpoint from.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the breakpoint to remove.","$ref":"DOMBreakpointType"}]},{"name":"removeEventListenerBreakpoint","description":"Removes breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"Event name.","type":"string"},{"name":"targetName","description":"EventTarget interface name.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeXHRBreakpoint","description":"Removes breakpoint from XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring.","type":"string"}]},{"name":"setBreakOnCSPViolation","description":"Sets breakpoint on particular CSP violations.","experimental":true,"parameters":[{"name":"violationTypes","description":"CSP Violations to stop upon.","type":"array","items":{"$ref":"CSPViolationType"}}]},{"name":"setDOMBreakpoint","description":"Sets breakpoint on particular operation with DOM.","parameters":[{"name":"nodeId","description":"Identifier of the node to set breakpoint on.","$ref":"DOM.NodeId"},{"name":"type","description":"Type of the operation to stop upon.","$ref":"DOMBreakpointType"}]},{"name":"setEventListenerBreakpoint","description":"Sets breakpoint on particular DOM event.","parameters":[{"name":"eventName","description":"DOM Event name to stop on (any DOM event will do).","type":"string"},{"name":"targetName","description":"EventTarget interface name to stop on. If equal to `\\"*\\"` or not provided, will stop on any\\nEventTarget.","experimental":true,"optional":true,"type":"string"}]},{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","experimental":true,"parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"setXHRBreakpoint","description":"Sets breakpoint on XMLHttpRequest.","parameters":[{"name":"url","description":"Resource URL substring. All XHRs having this substring in the URL will get stopped upon.","type":"string"}]}]},{"domain":"EventBreakpoints","description":"EventBreakpoints permits setting breakpoints on particular operations and\\nevents in targets that run JavaScript but do not have a DOM.\\nJavaScript execution will stop on these operations as if there was a regular\\nbreakpoint set.","experimental":true,"commands":[{"name":"setInstrumentationBreakpoint","description":"Sets breakpoint on particular native event.","parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]},{"name":"removeInstrumentationBreakpoint","description":"Removes breakpoint on particular native event.","parameters":[{"name":"eventName","description":"Instrumentation name to stop on.","type":"string"}]}]},{"domain":"DOMSnapshot","description":"This domain facilitates obtaining document snapshots with DOM, layout, and style information.","experimental":true,"dependencies":["CSS","DOM","DOMDebugger","Page"],"types":[{"id":"DOMNode","description":"A Node in the DOM tree.","type":"object","properties":[{"name":"nodeType","description":"`Node`\'s nodeType.","type":"integer"},{"name":"nodeName","description":"`Node`\'s nodeName.","type":"string"},{"name":"nodeValue","description":"`Node`\'s nodeValue.","type":"string"},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"type":"string"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"type":"string"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"type":"boolean"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"type":"boolean"},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","$ref":"DOM.BackendNodeId"},{"name":"childNodeIndexes","description":"The indexes of the node\'s child nodes in the `domNodes` array returned by `getSnapshot`, if\\nany.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"attributes","description":"Attributes of an `Element` node.","optional":true,"type":"array","items":{"$ref":"NameValue"}},{"name":"pseudoElementIndexes","description":"Indexes of pseudo elements associated with this node in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"layoutNodeIndex","description":"The index of the node\'s related layout tree node in the `layoutTreeNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","optional":true,"type":"string"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","optional":true,"type":"string"},{"name":"contentLanguage","description":"Only set for documents, contains the document\'s content language.","optional":true,"type":"string"},{"name":"documentEncoding","description":"Only set for documents, contains the document\'s character set encoding.","optional":true,"type":"string"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","optional":true,"type":"string"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","optional":true,"type":"string"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","optional":true,"$ref":"Page.FrameId"},{"name":"contentDocumentIndex","description":"The index of a frame owner element\'s content document in the `domNodes` array returned by\\n`getSnapshot`, if any.","optional":true,"type":"integer"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"DOM.PseudoType"},{"name":"shadowRootType","description":"Shadow root type.","optional":true,"$ref":"DOM.ShadowRootType"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"type":"boolean"},{"name":"eventListeners","description":"Details of the node\'s event listeners, if any.","optional":true,"type":"array","items":{"$ref":"DOMDebugger.EventListener"}},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"type":"string"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"type":"string"},{"name":"scrollOffsetX","description":"Scroll offsets, set when this node is a Document.","optional":true,"type":"number"},{"name":"scrollOffsetY","optional":true,"type":"number"}]},{"id":"InlineTextBox","description":"Details of post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"startCharacterIndex","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"integer"},{"name":"numCharacters","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"integer"}]},{"id":"LayoutTreeNode","description":"Details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"domNodeIndex","description":"The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.","type":"integer"},{"name":"boundingBox","description":"The bounding box in document coordinates. Note that scroll offset of the document is ignored.","$ref":"DOM.Rect"},{"name":"layoutText","description":"Contents of the LayoutText, if any.","optional":true,"type":"string"},{"name":"inlineTextNodes","description":"The post-layout inline text nodes, if any.","optional":true,"type":"array","items":{"$ref":"InlineTextBox"}},{"name":"styleIndex","description":"Index into the `computedStyles` array returned by `getSnapshot`.","optional":true,"type":"integer"},{"name":"paintOrder","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ngetSnapshot was true.","optional":true,"type":"integer"},{"name":"isStackingContext","description":"Set to true to indicate the element begins a new stacking context.","optional":true,"type":"boolean"}]},{"id":"ComputedStyle","description":"A subset of the full ComputedStyle as defined by the request whitelist.","type":"object","properties":[{"name":"properties","description":"Name/value pairs of computed style properties.","type":"array","items":{"$ref":"NameValue"}}]},{"id":"NameValue","description":"A name/value pair.","type":"object","properties":[{"name":"name","description":"Attribute/property name.","type":"string"},{"name":"value","description":"Attribute/property value.","type":"string"}]},{"id":"StringIndex","description":"Index of the string in the strings table.","type":"integer"},{"id":"ArrayOfStrings","description":"Index of the string in the strings table.","type":"array","items":{"$ref":"StringIndex"}},{"id":"RareStringData","description":"Data that is only present on rare nodes.","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"$ref":"StringIndex"}}]},{"id":"RareBooleanData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}}]},{"id":"RareIntegerData","type":"object","properties":[{"name":"index","type":"array","items":{"type":"integer"}},{"name":"value","type":"array","items":{"type":"integer"}}]},{"id":"Rectangle","type":"array","items":{"type":"number"}},{"id":"DocumentSnapshot","description":"Document snapshot.","type":"object","properties":[{"name":"documentURL","description":"Document URL that `Document` or `FrameOwner` node points to.","$ref":"StringIndex"},{"name":"title","description":"Document title.","$ref":"StringIndex"},{"name":"baseURL","description":"Base URL that `Document` or `FrameOwner` node uses for URL completion.","$ref":"StringIndex"},{"name":"contentLanguage","description":"Contains the document\'s content language.","$ref":"StringIndex"},{"name":"encodingName","description":"Contains the document\'s character set encoding.","$ref":"StringIndex"},{"name":"publicId","description":"`DocumentType` node\'s publicId.","$ref":"StringIndex"},{"name":"systemId","description":"`DocumentType` node\'s systemId.","$ref":"StringIndex"},{"name":"frameId","description":"Frame ID for frame owner elements and also for the document node.","$ref":"StringIndex"},{"name":"nodes","description":"A table with dom nodes.","$ref":"NodeTreeSnapshot"},{"name":"layout","description":"The nodes in the layout tree.","$ref":"LayoutTreeSnapshot"},{"name":"textBoxes","description":"The post-layout inline text nodes.","$ref":"TextBoxSnapshot"},{"name":"scrollOffsetX","description":"Horizontal scroll offset.","optional":true,"type":"number"},{"name":"scrollOffsetY","description":"Vertical scroll offset.","optional":true,"type":"number"},{"name":"contentWidth","description":"Document content width.","optional":true,"type":"number"},{"name":"contentHeight","description":"Document content height.","optional":true,"type":"number"}]},{"id":"NodeTreeSnapshot","description":"Table containing nodes.","type":"object","properties":[{"name":"parentIndex","description":"Parent node index.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"nodeType","description":"`Node`\'s nodeType.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"shadowRootType","description":"Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum.","optional":true,"$ref":"RareStringData"},{"name":"nodeName","description":"`Node`\'s nodeName.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"nodeValue","description":"`Node`\'s nodeValue.","optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"backendNodeId","description":"`Node`\'s id, corresponds to DOM.Node.backendNodeId.","optional":true,"type":"array","items":{"$ref":"DOM.BackendNodeId"}},{"name":"attributes","description":"Attributes of an `Element` node. Flatten name, value pairs.","optional":true,"type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"textValue","description":"Only set for textarea elements, contains the text value.","optional":true,"$ref":"RareStringData"},{"name":"inputValue","description":"Only set for input elements, contains the input\'s associated text value.","optional":true,"$ref":"RareStringData"},{"name":"inputChecked","description":"Only set for radio and checkbox input elements, indicates if the element has been checked","optional":true,"$ref":"RareBooleanData"},{"name":"optionSelected","description":"Only set for option elements, indicates if the element has been selected","optional":true,"$ref":"RareBooleanData"},{"name":"contentDocumentIndex","description":"The index of the document in the list of the snapshot documents.","optional":true,"$ref":"RareIntegerData"},{"name":"pseudoType","description":"Type of a pseudo element node.","optional":true,"$ref":"RareStringData"},{"name":"pseudoIdentifier","description":"Pseudo element identifier for this node. Only present if there is a\\nvalid pseudoType.","optional":true,"$ref":"RareStringData"},{"name":"isClickable","description":"Whether this DOM node responds to mouse clicks. This includes nodes that have had click\\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\\nclicked.","optional":true,"$ref":"RareBooleanData"},{"name":"currentSourceURL","description":"The selected url for nodes with a srcset attribute.","optional":true,"$ref":"RareStringData"},{"name":"originURL","description":"The url of the script (if any) that generates this node.","optional":true,"$ref":"RareStringData"}]},{"id":"LayoutTreeSnapshot","description":"Table of details of an element in the DOM tree with a LayoutObject.","type":"object","properties":[{"name":"nodeIndex","description":"Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.","type":"array","items":{"type":"integer"}},{"name":"styles","description":"Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.","type":"array","items":{"$ref":"ArrayOfStrings"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"text","description":"Contents of the LayoutText, if any.","type":"array","items":{"$ref":"StringIndex"}},{"name":"stackingContexts","description":"Stacking context information.","$ref":"RareBooleanData"},{"name":"paintOrders","description":"Global paint order index, which is determined by the stacking order of the nodes. Nodes\\nthat are painted together will have the same index. Only provided if includePaintOrder in\\ncaptureSnapshot was true.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"offsetRects","description":"The offset rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"scrollRects","description":"The scroll rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"clientRects","description":"The client rect of nodes. Only available when includeDOMRects is set to true","optional":true,"type":"array","items":{"$ref":"Rectangle"}},{"name":"blendedBackgroundColors","description":"The list of background colors that are blended with colors of overlapping elements.","experimental":true,"optional":true,"type":"array","items":{"$ref":"StringIndex"}},{"name":"textColorOpacities","description":"The list of computed text opacities.","experimental":true,"optional":true,"type":"array","items":{"type":"number"}}]},{"id":"TextBoxSnapshot","description":"Table of details of the post layout rendered text positions. The exact layout should not be regarded as\\nstable and may change between versions.","type":"object","properties":[{"name":"layoutIndex","description":"Index of the layout tree node that owns this box collection.","type":"array","items":{"type":"integer"}},{"name":"bounds","description":"The absolute position bounding box.","type":"array","items":{"$ref":"Rectangle"}},{"name":"start","description":"The starting index in characters, for this post layout textbox substring. Characters that\\nwould be represented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}},{"name":"length","description":"The number of characters in this post layout textbox substring. Characters that would be\\nrepresented as a surrogate pair in UTF-16 have length 2.","type":"array","items":{"type":"integer"}}]}],"commands":[{"name":"disable","description":"Disables DOM snapshot agent for the given page."},{"name":"enable","description":"Enables DOM snapshot agent for the given page."},{"name":"getSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","deprecated":true,"parameters":[{"name":"computedStyleWhitelist","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includeEventListeners","description":"Whether or not to retrieve details of DOM listeners (default false).","optional":true,"type":"boolean"},{"name":"includePaintOrder","description":"Whether to determine and include the paint order index of LayoutTreeNodes (default false).","optional":true,"type":"boolean"},{"name":"includeUserAgentShadowTree","description":"Whether to include UA shadow tree in the snapshot (default false).","optional":true,"type":"boolean"}],"returns":[{"name":"domNodes","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DOMNode"}},{"name":"layoutTreeNodes","description":"The nodes in the layout tree.","type":"array","items":{"$ref":"LayoutTreeNode"}},{"name":"computedStyles","description":"Whitelisted ComputedStyle properties for each node in the layout tree.","type":"array","items":{"$ref":"ComputedStyle"}}]},{"name":"captureSnapshot","description":"Returns a document snapshot, including the full DOM tree of the root node (including iframes,\\ntemplate contents, and imported documents) in a flattened array, as well as layout and\\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\\nflattened.","parameters":[{"name":"computedStyles","description":"Whitelist of computed styles to return.","type":"array","items":{"type":"string"}},{"name":"includePaintOrder","description":"Whether to include layout object paint orders into the snapshot.","optional":true,"type":"boolean"},{"name":"includeDOMRects","description":"Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot","optional":true,"type":"boolean"},{"name":"includeBlendedBackgroundColors","description":"Whether to include blended background colors in the snapshot (default: false).\\nBlended background color is achieved by blending background colors of all elements\\nthat overlap with the current element.","experimental":true,"optional":true,"type":"boolean"},{"name":"includeTextColorOpacities","description":"Whether to include text color opacity in the snapshot (default: false).\\nAn element might have the opacity property set that affects the text color of the element.\\nThe final text color opacity is computed based on the opacity of all overlapping elements.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"documents","description":"The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.","type":"array","items":{"$ref":"DocumentSnapshot"}},{"name":"strings","description":"Shared string table that all string properties refer to with indexes.","type":"array","items":{"type":"string"}}]}]},{"domain":"DOMStorage","description":"Query and modify DOM storage.","experimental":true,"types":[{"id":"SerializedStorageKey","type":"string"},{"id":"StorageId","description":"DOM Storage identifier.","type":"object","properties":[{"name":"securityOrigin","description":"Security origin for the storage.","optional":true,"type":"string"},{"name":"storageKey","description":"Represents a key by which DOM Storage keys its CachedStorageAreas","optional":true,"$ref":"SerializedStorageKey"},{"name":"isLocalStorage","description":"Whether the storage is local storage (not session storage).","type":"boolean"}]},{"id":"Item","description":"DOM Storage item.","type":"array","items":{"type":"string"}}],"commands":[{"name":"clear","parameters":[{"name":"storageId","$ref":"StorageId"}]},{"name":"disable","description":"Disables storage tracking, prevents storage events from being sent to the client."},{"name":"enable","description":"Enables storage tracking, storage events will now be delivered to the client."},{"name":"getDOMStorageItems","parameters":[{"name":"storageId","$ref":"StorageId"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"Item"}}]},{"name":"removeDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"setDOMStorageItem","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"value","type":"string"}]}],"events":[{"name":"domStorageItemAdded","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemRemoved","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"}]},{"name":"domStorageItemUpdated","parameters":[{"name":"storageId","$ref":"StorageId"},{"name":"key","type":"string"},{"name":"oldValue","type":"string"},{"name":"newValue","type":"string"}]},{"name":"domStorageItemsCleared","parameters":[{"name":"storageId","$ref":"StorageId"}]}]},{"domain":"Database","experimental":true,"types":[{"id":"DatabaseId","description":"Unique identifier of Database object.","type":"string"},{"id":"Database","description":"Database object.","type":"object","properties":[{"name":"id","description":"Database ID.","$ref":"DatabaseId"},{"name":"domain","description":"Database domain.","type":"string"},{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version.","type":"string"}]},{"id":"Error","description":"Database error.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"code","description":"Error code.","type":"integer"}]}],"commands":[{"name":"disable","description":"Disables database tracking, prevents database events from being sent to the client."},{"name":"enable","description":"Enables database tracking, database events will now be delivered to the client."},{"name":"executeSQL","parameters":[{"name":"databaseId","$ref":"DatabaseId"},{"name":"query","type":"string"}],"returns":[{"name":"columnNames","optional":true,"type":"array","items":{"type":"string"}},{"name":"values","optional":true,"type":"array","items":{"type":"any"}},{"name":"sqlError","optional":true,"$ref":"Error"}]},{"name":"getDatabaseTableNames","parameters":[{"name":"databaseId","$ref":"DatabaseId"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]}],"events":[{"name":"addDatabase","parameters":[{"name":"database","$ref":"Database"}]}]},{"domain":"DeviceOrientation","experimental":true,"commands":[{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation."},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]}]},{"domain":"Emulation","description":"This domain emulates different environments for the page.","dependencies":["DOM","Page","Runtime"],"types":[{"id":"ScreenOrientation","description":"Screen orientation.","type":"object","properties":[{"name":"type","description":"Orientation type.","type":"string","enum":["portraitPrimary","portraitSecondary","landscapePrimary","landscapeSecondary"]},{"name":"angle","description":"Orientation angle.","type":"integer"}]},{"id":"DisplayFeature","type":"object","properties":[{"name":"orientation","description":"Orientation of a display feature in relation to screen","type":"string","enum":["vertical","horizontal"]},{"name":"offset","description":"The offset from the screen origin in either the x (for vertical\\norientation) or y (for horizontal orientation) direction.","type":"integer"},{"name":"maskLength","description":"A display feature may mask content such that it is not physically\\ndisplayed - this length along with the offset describes this area.\\nA display feature that only splits content will have a 0 mask_length.","type":"integer"}]},{"id":"MediaFeature","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"VirtualTimePolicy","description":"advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\\nresource fetches.","experimental":true,"type":"string","enum":["advance","pause","pauseIfNetworkFetchesPending"]},{"id":"UserAgentBrandVersion","description":"Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints","experimental":true,"type":"object","properties":[{"name":"brand","type":"string"},{"name":"version","type":"string"}]},{"id":"UserAgentMetadata","description":"Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints\\nMissing optional values will be filled in by the target with what it would normally use.","experimental":true,"type":"object","properties":[{"name":"brands","description":"Brands appearing in Sec-CH-UA.","optional":true,"type":"array","items":{"$ref":"UserAgentBrandVersion"}},{"name":"fullVersionList","description":"Brands appearing in Sec-CH-UA-Full-Version-List.","optional":true,"type":"array","items":{"$ref":"UserAgentBrandVersion"}},{"name":"fullVersion","deprecated":true,"optional":true,"type":"string"},{"name":"platform","type":"string"},{"name":"platformVersion","type":"string"},{"name":"architecture","type":"string"},{"name":"model","type":"string"},{"name":"mobile","type":"boolean"},{"name":"bitness","optional":true,"type":"string"},{"name":"wow64","optional":true,"type":"boolean"}]},{"id":"DisabledImageType","description":"Enum of image types that can be disabled.","experimental":true,"type":"string","enum":["avif","webp"]}],"commands":[{"name":"canEmulate","description":"Tells whether emulation is supported.","returns":[{"name":"result","description":"True if emulation is supported.","type":"boolean"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overridden device metrics."},{"name":"clearGeolocationOverride","description":"Clears the overridden Geolocation Position and Error."},{"name":"resetPageScaleFactor","description":"Requests that page scale factor is reset to initial values.","experimental":true},{"name":"setFocusEmulationEnabled","description":"Enables or disables simulating a focused and active page.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable to disable focus emulation.","type":"boolean"}]},{"name":"setAutoDarkModeOverride","description":"Automatically render all web contents using a dark theme.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to enable or disable automatic dark mode.\\nIf not specified, any existing override will be cleared.","optional":true,"type":"boolean"}]},{"name":"setCPUThrottlingRate","description":"Enables CPU throttling to emulate slow CPUs.","experimental":true,"parameters":[{"name":"rate","description":"Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).","type":"number"}]},{"name":"setDefaultBackgroundColorOverride","description":"Sets or clears an override of the default background color of the frame. This override is used\\nif the content does not specify one.","parameters":[{"name":"color","description":"RGBA of the default background color. If not specified, any existing override will be\\ncleared.","optional":true,"$ref":"DOM.RGBA"}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","experimental":true,"optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","experimental":true,"optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","experimental":true,"optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"ScreenOrientation"},{"name":"viewport","description":"If set, the visible area of the page will be overridden to this viewport. This viewport\\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.","experimental":true,"optional":true,"$ref":"Page.Viewport"},{"name":"displayFeature","description":"If set, the display feature of a multi-segment screen. If not set, multi-segment support\\nis turned-off.","experimental":true,"optional":true,"$ref":"DisplayFeature"}]},{"name":"setScrollbarsHidden","experimental":true,"parameters":[{"name":"hidden","description":"Whether scrollbars should be always hidden.","type":"boolean"}]},{"name":"setDocumentCookieDisabled","experimental":true,"parameters":[{"name":"disabled","description":"Whether document.coookie API should be disabled.","type":"boolean"}]},{"name":"setEmitTouchEventsForMouse","experimental":true,"parameters":[{"name":"enabled","description":"Whether touch emulation based on mouse input should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"setEmulatedMedia","description":"Emulates the given media type or media feature for CSS media queries.","parameters":[{"name":"media","description":"Media type to emulate. Empty string disables the override.","optional":true,"type":"string"},{"name":"features","description":"Media features to emulate.","optional":true,"type":"array","items":{"$ref":"MediaFeature"}}]},{"name":"setEmulatedVisionDeficiency","description":"Emulates the given vision deficiency.","experimental":true,"parameters":[{"name":"type","description":"Vision deficiency to emulate. Order: best-effort emulations come first, followed by any\\nphysiologically accurate emulations for medically recognized color vision deficiencies.","type":"string","enum":["none","blurredVision","reducedContrast","achromatopsia","deuteranopia","protanopia","tritanopia"]}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setIdleOverride","description":"Overrides the Idle state.","experimental":true,"parameters":[{"name":"isUserActive","description":"Mock isUserActive","type":"boolean"},{"name":"isScreenUnlocked","description":"Mock isScreenUnlocked","type":"boolean"}]},{"name":"clearIdleOverride","description":"Clears Idle state overrides.","experimental":true},{"name":"setNavigatorOverrides","description":"Overrides value returned by the javascript navigator object.","experimental":true,"deprecated":true,"parameters":[{"name":"platform","description":"The platform navigator.platform should return.","type":"string"}]},{"name":"setPageScaleFactor","description":"Sets a specified page scale factor.","experimental":true,"parameters":[{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"}]},{"name":"setScriptExecutionDisabled","description":"Switches script execution in the page.","parameters":[{"name":"value","description":"Whether script execution should be disabled in the page.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Enables touch on platforms which do not support them.","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"maxTouchPoints","description":"Maximum touch points supported. Defaults to one.","optional":true,"type":"integer"}]},{"name":"setVirtualTimePolicy","description":"Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\\nthe current virtual time policy. Note this supersedes any previous time budget.","experimental":true,"parameters":[{"name":"policy","$ref":"VirtualTimePolicy"},{"name":"budget","description":"If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\\nvirtualTimeBudgetExpired event is sent.","optional":true,"type":"number"},{"name":"maxVirtualTimeTaskStarvationCount","description":"If set this specifies the maximum number of tasks that can be run before virtual is forced\\nforwards to prevent deadlock.","optional":true,"type":"integer"},{"name":"initialVirtualTime","description":"If set, base::Time::Now will be overridden to initially return this value.","optional":true,"$ref":"Network.TimeSinceEpoch"}],"returns":[{"name":"virtualTimeTicksBase","description":"Absolute timestamp at which virtual time was first enabled (up time in milliseconds).","type":"number"}]},{"name":"setLocaleOverride","description":"Overrides default host system locale with the specified one.","experimental":true,"parameters":[{"name":"locale","description":"ICU style C locale (e.g. \\"en_US\\"). If not specified or empty, disables the override and\\nrestores default host system locale.","optional":true,"type":"string"}]},{"name":"setTimezoneOverride","description":"Overrides default host system timezone with the specified one.","experimental":true,"parameters":[{"name":"timezoneId","description":"The timezone identifier. If empty, disables the override and\\nrestores default host system timezone.","type":"string"}]},{"name":"setVisibleSize","description":"Resizes the frame/viewport of the page. Note that this does not affect the frame\'s container\\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\\non Android.","experimental":true,"deprecated":true,"parameters":[{"name":"width","description":"Frame width (DIP).","type":"integer"},{"name":"height","description":"Frame height (DIP).","type":"integer"}]},{"name":"setDisabledImageTypes","experimental":true,"parameters":[{"name":"imageTypes","description":"Image types to disable.","type":"array","items":{"$ref":"DisabledImageType"}}]},{"name":"setHardwareConcurrencyOverride","experimental":true,"parameters":[{"name":"hardwareConcurrency","description":"Hardware concurrency to report","type":"integer"}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"},{"name":"userAgentMetadata","description":"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData","experimental":true,"optional":true,"$ref":"UserAgentMetadata"}]},{"name":"setAutomationOverride","description":"Allows overriding the automation flag.","experimental":true,"parameters":[{"name":"enabled","description":"Whether the override should be enabled.","type":"boolean"}]}],"events":[{"name":"virtualTimeBudgetExpired","description":"Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.","experimental":true}]},{"domain":"HeadlessExperimental","description":"This domain provides experimental commands only supported in headless mode.","experimental":true,"dependencies":["Page","Runtime"],"types":[{"id":"ScreenshotParams","description":"Encoding options for a screenshot.","type":"object","properties":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png","webp"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg and webp only).","optional":true,"type":"integer"},{"name":"optimizeForSpeed","description":"Optimize image encoding for speed, not for resulting size (defaults to false)","optional":true,"type":"boolean"}]}],"commands":[{"name":"beginFrame","description":"Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\\nscreenshot from the resulting frame. Requires that the target was created with enabled\\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\\nhttps://goo.gle/chrome-headless-rendering for more background.","parameters":[{"name":"frameTimeTicks","description":"Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\\nthe current time will be used.","optional":true,"type":"number"},{"name":"interval","description":"The interval between BeginFrames that is reported to the compositor, in milliseconds.\\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.","optional":true,"type":"number"},{"name":"noDisplayUpdates","description":"Whether updates should not be committed and drawn onto the display. False by default. If\\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\\nany visual updates may not be visible on the display or in screenshots.","optional":true,"type":"boolean"},{"name":"screenshot","description":"If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\\nduring renderer initialization. In such a case, no screenshot data will be returned.","optional":true,"$ref":"ScreenshotParams"}],"returns":[{"name":"hasDamage","description":"Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\\ndisplay. Reported for diagnostic uses, may be removed in the future.","type":"boolean"},{"name":"screenshotData","description":"Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"disable","description":"Disables headless events for the target.","deprecated":true},{"name":"enable","description":"Enables headless events for the target.","deprecated":true}]},{"domain":"IO","description":"Input/Output operations for streams produced by DevTools.","types":[{"id":"StreamHandle","description":"This is either obtained from another method or specified as `blob:<uuid>` where\\n`<uuid>` is an UUID of a Blob.","type":"string"}],"commands":[{"name":"close","description":"Close the stream, discard any temporary backing storage.","parameters":[{"name":"handle","description":"Handle of the stream to close.","$ref":"StreamHandle"}]},{"name":"read","description":"Read a chunk of the stream","parameters":[{"name":"handle","description":"Handle of the stream to read.","$ref":"StreamHandle"},{"name":"offset","description":"Seek to the specified offset before reading (if not specificed, proceed with offset\\nfollowing the last read). Some types of streams may only support sequential reads.","optional":true,"type":"integer"},{"name":"size","description":"Maximum number of bytes to read (left upon the agent discretion if not specified).","optional":true,"type":"integer"}],"returns":[{"name":"base64Encoded","description":"Set if the data is base64-encoded","optional":true,"type":"boolean"},{"name":"data","description":"Data that were read.","type":"string"},{"name":"eof","description":"Set if the end-of-file condition occurred while reading.","type":"boolean"}]},{"name":"resolveBlob","description":"Return UUID of Blob object specified by a remote object id.","parameters":[{"name":"objectId","description":"Object id of a Blob object wrapper.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"uuid","description":"UUID of the specified Blob.","type":"string"}]}]},{"domain":"IndexedDB","experimental":true,"dependencies":["Runtime","Storage"],"types":[{"id":"DatabaseWithObjectStores","description":"Database with an array of object stores.","type":"object","properties":[{"name":"name","description":"Database name.","type":"string"},{"name":"version","description":"Database version (type is not \'integer\', as the standard\\nrequires the version number to be \'unsigned long long\')","type":"number"},{"name":"objectStores","description":"Object stores in this database.","type":"array","items":{"$ref":"ObjectStore"}}]},{"id":"ObjectStore","description":"Object store.","type":"object","properties":[{"name":"name","description":"Object store name.","type":"string"},{"name":"keyPath","description":"Object store key path.","$ref":"KeyPath"},{"name":"autoIncrement","description":"If true, object store has auto increment flag set.","type":"boolean"},{"name":"indexes","description":"Indexes in this object store.","type":"array","items":{"$ref":"ObjectStoreIndex"}}]},{"id":"ObjectStoreIndex","description":"Object store index.","type":"object","properties":[{"name":"name","description":"Index name.","type":"string"},{"name":"keyPath","description":"Index key path.","$ref":"KeyPath"},{"name":"unique","description":"If true, index is unique.","type":"boolean"},{"name":"multiEntry","description":"If true, index allows multiple entries for a key.","type":"boolean"}]},{"id":"Key","description":"Key.","type":"object","properties":[{"name":"type","description":"Key type.","type":"string","enum":["number","string","date","array"]},{"name":"number","description":"Number value.","optional":true,"type":"number"},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"date","description":"Date value.","optional":true,"type":"number"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"$ref":"Key"}}]},{"id":"KeyRange","description":"Key range.","type":"object","properties":[{"name":"lower","description":"Lower bound.","optional":true,"$ref":"Key"},{"name":"upper","description":"Upper bound.","optional":true,"$ref":"Key"},{"name":"lowerOpen","description":"If true lower bound is open.","type":"boolean"},{"name":"upperOpen","description":"If true upper bound is open.","type":"boolean"}]},{"id":"DataEntry","description":"Data entry.","type":"object","properties":[{"name":"key","description":"Key object.","$ref":"Runtime.RemoteObject"},{"name":"primaryKey","description":"Primary key object.","$ref":"Runtime.RemoteObject"},{"name":"value","description":"Value object.","$ref":"Runtime.RemoteObject"}]},{"id":"KeyPath","description":"Key path.","type":"object","properties":[{"name":"type","description":"Key path type.","type":"string","enum":["null","string","array"]},{"name":"string","description":"String value.","optional":true,"type":"string"},{"name":"array","description":"Array value.","optional":true,"type":"array","items":{"type":"string"}}]}],"commands":[{"name":"clearObjectStore","description":"Clears all entries from an object store.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}]},{"name":"deleteDatabase","description":"Deletes a database.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"}]},{"name":"deleteObjectStoreEntries","description":"Delete a range of entries from an object store","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","type":"string"},{"name":"objectStoreName","type":"string"},{"name":"keyRange","description":"Range of entry keys to delete","$ref":"KeyRange"}]},{"name":"disable","description":"Disables events from backend."},{"name":"enable","description":"Enables events from backend."},{"name":"requestData","description":"Requests data from object store or index.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"},{"name":"indexName","description":"Index name, empty string for object store data requests.","type":"string"},{"name":"skipCount","description":"Number of records to skip.","type":"integer"},{"name":"pageSize","description":"Number of records to fetch.","type":"integer"},{"name":"keyRange","description":"Key range.","optional":true,"$ref":"KeyRange"}],"returns":[{"name":"objectStoreDataEntries","description":"Array of object store data entries.","type":"array","items":{"$ref":"DataEntry"}},{"name":"hasMore","description":"If true, there are more entries to fetch in the given range.","type":"boolean"}]},{"name":"getMetadata","description":"Gets metadata of an object store.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"},{"name":"objectStoreName","description":"Object store name.","type":"string"}],"returns":[{"name":"entriesCount","description":"the entries count","type":"number"},{"name":"keyGeneratorValue","description":"the current value of key generator, to become the next inserted\\nkey into the object store. Valid if objectStore.autoIncrement\\nis true.","type":"number"}]},{"name":"requestDatabase","description":"Requests database with given name in given frame.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"},{"name":"databaseName","description":"Database name.","type":"string"}],"returns":[{"name":"databaseWithObjectStores","description":"Database with an array of object stores.","$ref":"DatabaseWithObjectStores"}]},{"name":"requestDatabaseNames","description":"Requests database names for given security origin.","parameters":[{"name":"securityOrigin","description":"At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\\nSecurity origin.","optional":true,"type":"string"},{"name":"storageKey","description":"Storage key.","optional":true,"type":"string"},{"name":"storageBucket","description":"Storage bucket. If not specified, it uses the default bucket.","optional":true,"$ref":"Storage.StorageBucket"}],"returns":[{"name":"databaseNames","description":"Database names for origin.","type":"array","items":{"type":"string"}}]}]},{"domain":"Input","types":[{"id":"TouchPoint","type":"object","properties":[{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"radiusX","description":"X radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"radiusY","description":"Y radius of the touch area (default: 1.0).","optional":true,"type":"number"},{"name":"rotationAngle","description":"Rotation angle (default: 0.0).","optional":true,"type":"number"},{"name":"force","description":"Force (default: 1.0).","optional":true,"type":"number"},{"name":"tangentialPressure","description":"The normalized tangential pressure, which has a range of [-1,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tiltX","description":"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0)","experimental":true,"optional":true,"type":"integer"},{"name":"tiltY","description":"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"twist","description":"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"id","description":"Identifier used to track touch sources between events, must be unique within an event.","optional":true,"type":"number"}]},{"id":"GestureSourceType","experimental":true,"type":"string","enum":["default","touch","mouse"]},{"id":"MouseButton","type":"string","enum":["none","left","middle","right","back","forward"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"DragDataItem","experimental":true,"type":"object","properties":[{"name":"mimeType","description":"Mime type of the dragged data.","type":"string"},{"name":"data","description":"Depending of the value of `mimeType`, it contains the dragged link,\\ntext, HTML markup or any other data.","type":"string"},{"name":"title","description":"Title associated with a link. Only valid when `mimeType` == \\"text/uri-list\\".","optional":true,"type":"string"},{"name":"baseURL","description":"Stores the base URL for the contained markup. Only valid when `mimeType`\\n== \\"text/html\\".","optional":true,"type":"string"}]},{"id":"DragData","experimental":true,"type":"object","properties":[{"name":"items","type":"array","items":{"$ref":"DragDataItem"}},{"name":"files","description":"List of filenames that should be included when dropping","optional":true,"type":"array","items":{"type":"string"}},{"name":"dragOperationsMask","description":"Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16","type":"integer"}]}],"commands":[{"name":"dispatchDragEvent","description":"Dispatches a drag event into the page.","experimental":true,"parameters":[{"name":"type","description":"Type of the drag event.","type":"string","enum":["dragEnter","dragOver","drop","dragCancel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"data","$ref":"DragData"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"}]},{"name":"dispatchKeyEvent","description":"Dispatches a key event to the page.","parameters":[{"name":"type","description":"Type of the key event.","type":"string","enum":["keyDown","keyUp","rawKeyDown","char"]},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"text","description":"Text as generated by processing a virtual key code with a keyboard layout. Not needed for\\nfor `keyUp` and `rawKeyDown` events (default: \\"\\")","optional":true,"type":"string"},{"name":"unmodifiedText","description":"Text that would have been generated by the keyboard if no modifiers were pressed (except for\\nshift). Useful for shortcut (accelerator) key handling (default: \\"\\").","optional":true,"type":"string"},{"name":"keyIdentifier","description":"Unique key identifier (e.g., \'U+0041\') (default: \\"\\").","optional":true,"type":"string"},{"name":"code","description":"Unique DOM defined string value for each physical key (e.g., \'KeyA\') (default: \\"\\").","optional":true,"type":"string"},{"name":"key","description":"Unique DOM defined string value describing the meaning of the key in the context of active\\nmodifiers, keyboard layout, etc (e.g., \'AltGr\') (default: \\"\\").","optional":true,"type":"string"},{"name":"windowsVirtualKeyCode","description":"Windows virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"nativeVirtualKeyCode","description":"Native virtual key code (default: 0).","optional":true,"type":"integer"},{"name":"autoRepeat","description":"Whether the event was generated from auto repeat (default: false).","optional":true,"type":"boolean"},{"name":"isKeypad","description":"Whether the event was generated from the keypad (default: false).","optional":true,"type":"boolean"},{"name":"isSystemKey","description":"Whether the event was a system key event (default: false).","optional":true,"type":"boolean"},{"name":"location","description":"Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\\n0).","optional":true,"type":"integer"},{"name":"commands","description":"Editing commands to send with the key event (e.g., \'selectAll\') (default: []).\\nThese are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding.\\nSee https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names.","experimental":true,"optional":true,"type":"array","items":{"type":"string"}}]},{"name":"insertText","description":"This method emulates inserting text that doesn\'t come from a key press,\\nfor example an emoji keyboard or an IME.","experimental":true,"parameters":[{"name":"text","description":"The text to insert.","type":"string"}]},{"name":"imeSetComposition","description":"This method sets the current candidate text for ime.\\nUse imeCommitComposition to commit the final text.\\nUse imeSetComposition with empty string as text to cancel composition.","experimental":true,"parameters":[{"name":"text","description":"The text to insert","type":"string"},{"name":"selectionStart","description":"selection start","type":"integer"},{"name":"selectionEnd","description":"selection end","type":"integer"},{"name":"replacementStart","description":"replacement start","optional":true,"type":"integer"},{"name":"replacementEnd","description":"replacement end","optional":true,"type":"integer"}]},{"name":"dispatchMouseEvent","description":"Dispatches a mouse event to the page.","parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the event relative to the main frame\'s viewport in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the event relative to the main frame\'s viewport in CSS pixels. 0 refers to\\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.","type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"button","description":"Mouse button (default: \\"none\\").","optional":true,"$ref":"MouseButton"},{"name":"buttons","description":"A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"},{"name":"force","description":"The normalized pressure, which has a range of [0,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tangentialPressure","description":"The normalized tangential pressure, which has a range of [-1,1] (default: 0).","experimental":true,"optional":true,"type":"number"},{"name":"tiltX","description":"The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"tiltY","description":"The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"twist","description":"The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).","experimental":true,"optional":true,"type":"integer"},{"name":"deltaX","description":"X delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in CSS pixels for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"pointerType","description":"Pointer type (default: \\"mouse\\").","optional":true,"type":"string","enum":["mouse","pen"]}]},{"name":"dispatchTouchEvent","description":"Dispatches a touch event to the page.","parameters":[{"name":"type","description":"Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\\nTouchStart and TouchMove must contains at least one.","type":"string","enum":["touchStart","touchEnd","touchMove","touchCancel"]},{"name":"touchPoints","description":"Active touch points on the touch device. One event per any changed point (compared to\\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\\none by one.","type":"array","items":{"$ref":"TouchPoint"}},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"timestamp","description":"Time at which the event occurred.","optional":true,"$ref":"TimeSinceEpoch"}]},{"name":"cancelDragging","description":"Cancels any active dragging in the page."},{"name":"emulateTouchFromMouseEvent","description":"Emulates touch event from the mouse event parameters.","experimental":true,"parameters":[{"name":"type","description":"Type of the mouse event.","type":"string","enum":["mousePressed","mouseReleased","mouseMoved","mouseWheel"]},{"name":"x","description":"X coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"y","description":"Y coordinate of the mouse pointer in DIP.","type":"integer"},{"name":"button","description":"Mouse button. Only \\"none\\", \\"left\\", \\"right\\" are supported.","$ref":"MouseButton"},{"name":"timestamp","description":"Time at which the event occurred (default: current time).","optional":true,"$ref":"TimeSinceEpoch"},{"name":"deltaX","description":"X delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"deltaY","description":"Y delta in DIP for mouse wheel event (default: 0).","optional":true,"type":"number"},{"name":"modifiers","description":"Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\\n(default: 0).","optional":true,"type":"integer"},{"name":"clickCount","description":"Number of times the mouse button was clicked (default: 0).","optional":true,"type":"integer"}]},{"name":"setIgnoreInputEvents","description":"Ignores input events (useful while auditing page).","parameters":[{"name":"ignore","description":"Ignores input events processing when set to true.","type":"boolean"}]},{"name":"setInterceptDrags","description":"Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events.\\nDrag and drop behavior can be directly controlled via `Input.dispatchDragEvent`.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"synthesizePinchGesture","description":"Synthesizes a pinch gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"scaleFactor","description":"Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).","type":"number"},{"name":"relativeSpeed","description":"Relative pointer speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]},{"name":"synthesizeScrollGesture","description":"Synthesizes a scroll gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"xDistance","description":"The distance to scroll along the X axis (positive to scroll left).","optional":true,"type":"number"},{"name":"yDistance","description":"The distance to scroll along the Y axis (positive to scroll up).","optional":true,"type":"number"},{"name":"xOverscroll","description":"The number of additional pixels to scroll back along the X axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"yOverscroll","description":"The number of additional pixels to scroll back along the Y axis, in addition to the given\\ndistance.","optional":true,"type":"number"},{"name":"preventFling","description":"Prevent fling (default: true).","optional":true,"type":"boolean"},{"name":"speed","description":"Swipe speed in pixels per second (default: 800).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"},{"name":"repeatCount","description":"The number of times to repeat the gesture (default: 0).","optional":true,"type":"integer"},{"name":"repeatDelayMs","description":"The number of milliseconds delay between each repeat. (default: 250).","optional":true,"type":"integer"},{"name":"interactionMarkerName","description":"The name of the interaction markers to generate, if not empty (default: \\"\\").","optional":true,"type":"string"}]},{"name":"synthesizeTapGesture","description":"Synthesizes a tap gesture over a time period by issuing appropriate touch events.","experimental":true,"parameters":[{"name":"x","description":"X coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"y","description":"Y coordinate of the start of the gesture in CSS pixels.","type":"number"},{"name":"duration","description":"Duration between touchdown and touchup events in ms (default: 50).","optional":true,"type":"integer"},{"name":"tapCount","description":"Number of times to perform the tap (e.g. 2 for double tap, default: 1).","optional":true,"type":"integer"},{"name":"gestureSourceType","description":"Which type of input events to be generated (default: \'default\', which queries the platform\\nfor the preferred input type).","optional":true,"$ref":"GestureSourceType"}]}],"events":[{"name":"dragIntercepted","description":"Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to\\nrestore normal drag and drop behavior.","experimental":true,"parameters":[{"name":"data","$ref":"DragData"}]}]},{"domain":"Inspector","experimental":true,"commands":[{"name":"disable","description":"Disables inspector domain notifications."},{"name":"enable","description":"Enables inspector domain notifications."}],"events":[{"name":"detached","description":"Fired when remote debugging connection is about to be terminated. Contains detach reason.","parameters":[{"name":"reason","description":"The reason why connection has been terminated.","type":"string"}]},{"name":"targetCrashed","description":"Fired when debugging target has crashed"},{"name":"targetReloadedAfterCrash","description":"Fired when debugging target has reloaded after crash"}]},{"domain":"LayerTree","experimental":true,"dependencies":["DOM"],"types":[{"id":"LayerId","description":"Unique Layer identifier.","type":"string"},{"id":"SnapshotId","description":"Unique snapshot identifier.","type":"string"},{"id":"ScrollRect","description":"Rectangle where scrolling happens on the main thread.","type":"object","properties":[{"name":"rect","description":"Rectangle itself.","$ref":"DOM.Rect"},{"name":"type","description":"Reason for rectangle to force scrolling on the main thread","type":"string","enum":["RepaintsOnScroll","TouchEventHandler","WheelEventHandler"]}]},{"id":"StickyPositionConstraint","description":"Sticky position constraints.","type":"object","properties":[{"name":"stickyBoxRect","description":"Layout rectangle of the sticky element before being shifted","$ref":"DOM.Rect"},{"name":"containingBlockRect","description":"Layout rectangle of the containing block of the sticky element","$ref":"DOM.Rect"},{"name":"nearestLayerShiftingStickyBox","description":"The nearest sticky layer that shifts the sticky box","optional":true,"$ref":"LayerId"},{"name":"nearestLayerShiftingContainingBlock","description":"The nearest sticky layer that shifts the containing block","optional":true,"$ref":"LayerId"}]},{"id":"PictureTile","description":"Serialized fragment of layer picture along with its offset within the layer.","type":"object","properties":[{"name":"x","description":"Offset from owning layer left boundary","type":"number"},{"name":"y","description":"Offset from owning layer top boundary","type":"number"},{"name":"picture","description":"Base64-encoded snapshot data. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"id":"Layer","description":"Information about a compositing layer.","type":"object","properties":[{"name":"layerId","description":"The unique id for this layer.","$ref":"LayerId"},{"name":"parentLayerId","description":"The id of parent (not present for root).","optional":true,"$ref":"LayerId"},{"name":"backendNodeId","description":"The backend id for the node associated with this layer.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"offsetX","description":"Offset from parent layer, X coordinate.","type":"number"},{"name":"offsetY","description":"Offset from parent layer, Y coordinate.","type":"number"},{"name":"width","description":"Layer width.","type":"number"},{"name":"height","description":"Layer height.","type":"number"},{"name":"transform","description":"Transformation matrix for layer, default is identity matrix","optional":true,"type":"array","items":{"type":"number"}},{"name":"anchorX","description":"Transform anchor point X, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorY","description":"Transform anchor point Y, absent if no transform specified","optional":true,"type":"number"},{"name":"anchorZ","description":"Transform anchor point Z, absent if no transform specified","optional":true,"type":"number"},{"name":"paintCount","description":"Indicates how many time this layer has painted.","type":"integer"},{"name":"drawsContent","description":"Indicates whether this layer hosts any content, rather than being used for\\ntransform/scrolling purposes only.","type":"boolean"},{"name":"invisible","description":"Set if layer is not visible.","optional":true,"type":"boolean"},{"name":"scrollRects","description":"Rectangles scrolling on main thread only.","optional":true,"type":"array","items":{"$ref":"ScrollRect"}},{"name":"stickyPositionConstraint","description":"Sticky position constraint information","optional":true,"$ref":"StickyPositionConstraint"}]},{"id":"PaintProfile","description":"Array of timings, one per paint step.","type":"array","items":{"type":"number"}}],"commands":[{"name":"compositingReasons","description":"Provides the reasons why the given layer was composited.","parameters":[{"name":"layerId","description":"The id of the layer for which we want to get the reasons it was composited.","$ref":"LayerId"}],"returns":[{"name":"compositingReasons","description":"A list of strings specifying reasons for the given layer to become composited.","type":"array","items":{"type":"string"}},{"name":"compositingReasonIds","description":"A list of strings specifying reason IDs for the given layer to become composited.","type":"array","items":{"type":"string"}}]},{"name":"disable","description":"Disables compositing tree inspection."},{"name":"enable","description":"Enables compositing tree inspection."},{"name":"loadSnapshot","description":"Returns the snapshot identifier.","parameters":[{"name":"tiles","description":"An array of tiles composing the snapshot.","type":"array","items":{"$ref":"PictureTile"}}],"returns":[{"name":"snapshotId","description":"The id of the snapshot.","$ref":"SnapshotId"}]},{"name":"makeSnapshot","description":"Returns the layer snapshot identifier.","parameters":[{"name":"layerId","description":"The id of the layer.","$ref":"LayerId"}],"returns":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"profileSnapshot","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"minRepeatCount","description":"The maximum number of times to replay the snapshot (1, if not specified).","optional":true,"type":"integer"},{"name":"minDuration","description":"The minimum duration (in seconds) to replay the snapshot.","optional":true,"type":"number"},{"name":"clipRect","description":"The clip rectangle to apply when replaying the snapshot.","optional":true,"$ref":"DOM.Rect"}],"returns":[{"name":"timings","description":"The array of paint profiles, one per run.","type":"array","items":{"$ref":"PaintProfile"}}]},{"name":"releaseSnapshot","description":"Releases layer snapshot captured by the back-end.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}]},{"name":"replaySnapshot","description":"Replays the layer snapshot and returns the resulting bitmap.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"},{"name":"fromStep","description":"The first step to replay from (replay from the very start if not specified).","optional":true,"type":"integer"},{"name":"toStep","description":"The last step to replay to (replay till the end if not specified).","optional":true,"type":"integer"},{"name":"scale","description":"The scale to apply while replaying (defaults to 1).","optional":true,"type":"number"}],"returns":[{"name":"dataURL","description":"A data: URL for resulting image.","type":"string"}]},{"name":"snapshotCommandLog","description":"Replays the layer snapshot and returns canvas log.","parameters":[{"name":"snapshotId","description":"The id of the layer snapshot.","$ref":"SnapshotId"}],"returns":[{"name":"commandLog","description":"The array of canvas function calls.","type":"array","items":{"type":"object"}}]}],"events":[{"name":"layerPainted","parameters":[{"name":"layerId","description":"The id of the painted layer.","$ref":"LayerId"},{"name":"clip","description":"Clip rectangle.","$ref":"DOM.Rect"}]},{"name":"layerTreeDidChange","parameters":[{"name":"layers","description":"Layer tree, absent if not in the comspositing mode.","optional":true,"type":"array","items":{"$ref":"Layer"}}]}]},{"domain":"Log","description":"Provides access to log entries.","dependencies":["Runtime","Network"],"types":[{"id":"LogEntry","description":"Log entry.","type":"object","properties":[{"name":"source","description":"Log entry source.","type":"string","enum":["xml","javascript","network","storage","appcache","rendering","security","deprecation","worker","violation","intervention","recommendation","other"]},{"name":"level","description":"Log entry severity.","type":"string","enum":["verbose","info","warning","error"]},{"name":"text","description":"Logged text.","type":"string"},{"name":"category","optional":true,"type":"string","enum":["cors"]},{"name":"timestamp","description":"Timestamp when this entry was added.","$ref":"Runtime.Timestamp"},{"name":"url","description":"URL of the resource if known.","optional":true,"type":"string"},{"name":"lineNumber","description":"Line number in the resource.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript stack trace.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"networkRequestId","description":"Identifier of the network request associated with this entry.","optional":true,"$ref":"Network.RequestId"},{"name":"workerId","description":"Identifier of the worker associated with this entry.","optional":true,"type":"string"},{"name":"args","description":"Call arguments.","optional":true,"type":"array","items":{"$ref":"Runtime.RemoteObject"}}]},{"id":"ViolationSetting","description":"Violation configuration setting.","type":"object","properties":[{"name":"name","description":"Violation type.","type":"string","enum":["longTask","longLayout","blockedEvent","blockedParser","discouragedAPIUse","handler","recurringHandler"]},{"name":"threshold","description":"Time threshold to trigger upon.","type":"number"}]}],"commands":[{"name":"clear","description":"Clears the log."},{"name":"disable","description":"Disables log domain, prevents further log entries from being reported to the client."},{"name":"enable","description":"Enables log domain, sends the entries collected so far to the client by means of the\\n`entryAdded` notification."},{"name":"startViolationsReport","description":"start violation reporting.","parameters":[{"name":"config","description":"Configuration for violations.","type":"array","items":{"$ref":"ViolationSetting"}}]},{"name":"stopViolationsReport","description":"Stop violation reporting."}],"events":[{"name":"entryAdded","description":"Issued when new message was logged.","parameters":[{"name":"entry","description":"The entry.","$ref":"LogEntry"}]}]},{"domain":"Memory","experimental":true,"types":[{"id":"PressureLevel","description":"Memory pressure level.","type":"string","enum":["moderate","critical"]},{"id":"SamplingProfileNode","description":"Heap profile sample.","type":"object","properties":[{"name":"size","description":"Size of the sampled allocation.","type":"number"},{"name":"total","description":"Total bytes attributed to this sample.","type":"number"},{"name":"stack","description":"Execution stack at the point of allocation.","type":"array","items":{"type":"string"}}]},{"id":"SamplingProfile","description":"Array of heap profile samples.","type":"object","properties":[{"name":"samples","type":"array","items":{"$ref":"SamplingProfileNode"}},{"name":"modules","type":"array","items":{"$ref":"Module"}}]},{"id":"Module","description":"Executable module information","type":"object","properties":[{"name":"name","description":"Name of the module.","type":"string"},{"name":"uuid","description":"UUID of the module.","type":"string"},{"name":"baseAddress","description":"Base address where the module is loaded into memory. Encoded as a decimal\\nor hexadecimal (0x prefixed) string.","type":"string"},{"name":"size","description":"Size of the module in bytes.","type":"number"}]}],"commands":[{"name":"getDOMCounters","returns":[{"name":"documents","type":"integer"},{"name":"nodes","type":"integer"},{"name":"jsEventListeners","type":"integer"}]},{"name":"prepareForLeakDetection"},{"name":"forciblyPurgeJavaScriptMemory","description":"Simulate OomIntervention by purging V8 memory."},{"name":"setPressureNotificationsSuppressed","description":"Enable/disable suppressing memory pressure notifications in all processes.","parameters":[{"name":"suppressed","description":"If true, memory pressure notifications will be suppressed.","type":"boolean"}]},{"name":"simulatePressureNotification","description":"Simulate a memory pressure notification in all processes.","parameters":[{"name":"level","description":"Memory pressure level of the notification.","$ref":"PressureLevel"}]},{"name":"startSampling","description":"Start collecting native memory profile.","parameters":[{"name":"samplingInterval","description":"Average number of bytes between samples.","optional":true,"type":"integer"},{"name":"suppressRandomness","description":"Do not randomize intervals between samples.","optional":true,"type":"boolean"}]},{"name":"stopSampling","description":"Stop collecting native memory profile."},{"name":"getAllTimeSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since renderer process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getBrowserSamplingProfile","description":"Retrieve native memory allocations profile\\ncollected since browser process startup.","returns":[{"name":"profile","$ref":"SamplingProfile"}]},{"name":"getSamplingProfile","description":"Retrieve native memory allocations profile collected since last\\n`startSampling` call.","returns":[{"name":"profile","$ref":"SamplingProfile"}]}]},{"domain":"Network","description":"Network domain allows tracking network activities of the page. It exposes information about http,\\nfile, data and other requests and responses, their headers, bodies, timing, etc.","dependencies":["Debugger","Runtime","Security"],"types":[{"id":"ResourceType","description":"Resource type as it was perceived by the rendering engine.","type":"string","enum":["Document","Stylesheet","Image","Media","Font","Script","TextTrack","XHR","Fetch","Prefetch","EventSource","WebSocket","Manifest","SignedExchange","Ping","CSPViolationReport","Preflight","Other"]},{"id":"LoaderId","description":"Unique loader identifier.","type":"string"},{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"InterceptionId","description":"Unique intercepted request identifier.","type":"string"},{"id":"ErrorReason","description":"Network level fetch failure reason.","type":"string","enum":["Failed","Aborted","TimedOut","AccessDenied","ConnectionClosed","ConnectionReset","ConnectionRefused","ConnectionAborted","ConnectionFailed","NameNotResolved","InternetDisconnected","AddressUnreachable","BlockedByClient","BlockedByResponse"]},{"id":"TimeSinceEpoch","description":"UTC time in seconds, counted from January 1, 1970.","type":"number"},{"id":"MonotonicTime","description":"Monotonically increasing time in seconds since an arbitrary point in the past.","type":"number"},{"id":"Headers","description":"Request / response headers as keys / values of JSON object.","type":"object"},{"id":"ConnectionType","description":"The underlying connection technology that the browser is supposedly using.","type":"string","enum":["none","cellular2g","cellular3g","cellular4g","bluetooth","ethernet","wifi","wimax","other"]},{"id":"CookieSameSite","description":"Represents the cookie\'s \'SameSite\' status:\\nhttps://tools.ietf.org/html/draft-west-first-party-cookies","type":"string","enum":["Strict","Lax","None"]},{"id":"CookiePriority","description":"Represents the cookie\'s \'Priority\' status:\\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00","experimental":true,"type":"string","enum":["Low","Medium","High"]},{"id":"CookieSourceScheme","description":"Represents the source scheme of the origin that originally set the cookie.\\nA value of \\"Unset\\" allows protocol clients to emulate legacy cookie scope for the scheme.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"type":"string","enum":["Unset","NonSecure","Secure"]},{"id":"ResourceTiming","description":"Timing information for the request.","type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime.","type":"number"},{"name":"proxyStart","description":"Started resolving proxy.","type":"number"},{"name":"proxyEnd","description":"Finished resolving proxy.","type":"number"},{"name":"dnsStart","description":"Started DNS address resolve.","type":"number"},{"name":"dnsEnd","description":"Finished DNS address resolve.","type":"number"},{"name":"connectStart","description":"Started connecting to the remote host.","type":"number"},{"name":"connectEnd","description":"Connected to the remote host.","type":"number"},{"name":"sslStart","description":"Started SSL handshake.","type":"number"},{"name":"sslEnd","description":"Finished SSL handshake.","type":"number"},{"name":"workerStart","description":"Started running ServiceWorker.","experimental":true,"type":"number"},{"name":"workerReady","description":"Finished Starting ServiceWorker.","experimental":true,"type":"number"},{"name":"workerFetchStart","description":"Started fetch event.","experimental":true,"type":"number"},{"name":"workerRespondWithSettled","description":"Settled fetch event respondWith promise.","experimental":true,"type":"number"},{"name":"sendStart","description":"Started sending request.","type":"number"},{"name":"sendEnd","description":"Finished sending request.","type":"number"},{"name":"pushStart","description":"Time the server started pushing request.","experimental":true,"type":"number"},{"name":"pushEnd","description":"Time the server finished pushing request.","experimental":true,"type":"number"},{"name":"receiveHeadersStart","description":"Started receiving response headers.","experimental":true,"type":"number"},{"name":"receiveHeadersEnd","description":"Finished receiving response headers.","type":"number"}]},{"id":"ResourcePriority","description":"Loading priority of a resource request.","type":"string","enum":["VeryLow","Low","Medium","High","VeryHigh"]},{"id":"PostDataEntry","description":"Post data entry for HTTP request","type":"object","properties":[{"name":"bytes","optional":true,"type":"string"}]},{"id":"Request","description":"HTTP request data.","type":"object","properties":[{"name":"url","description":"Request URL (without fragment).","type":"string"},{"name":"urlFragment","description":"Fragment of the requested URL starting with hash, if present.","optional":true,"type":"string"},{"name":"method","description":"HTTP request method.","type":"string"},{"name":"headers","description":"HTTP request headers.","$ref":"Headers"},{"name":"postData","description":"HTTP POST request data.","optional":true,"type":"string"},{"name":"hasPostData","description":"True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.","optional":true,"type":"boolean"},{"name":"postDataEntries","description":"Request body elements. This will be converted from base64 to binary","experimental":true,"optional":true,"type":"array","items":{"$ref":"PostDataEntry"}},{"name":"mixedContentType","description":"The mixed content type of the request.","optional":true,"$ref":"Security.MixedContentType"},{"name":"initialPriority","description":"Priority of the resource request at the time request is sent.","$ref":"ResourcePriority"},{"name":"referrerPolicy","description":"The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/","type":"string","enum":["unsafe-url","no-referrer-when-downgrade","no-referrer","origin","origin-when-cross-origin","same-origin","strict-origin","strict-origin-when-cross-origin"]},{"name":"isLinkPreload","description":"Whether is loaded via link preload.","optional":true,"type":"boolean"},{"name":"trustTokenParams","description":"Set for requests when the TrustToken API is used. Contains the parameters\\npassed by the developer (e.g. via \\"fetch\\") as understood by the backend.","experimental":true,"optional":true,"$ref":"TrustTokenParams"},{"name":"isSameSite","description":"True if this resource request is considered to be the \'same site\' as the\\nrequest correspondinfg to the main frame.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"SignedCertificateTimestamp","description":"Details of a signed certificate timestamp (SCT).","type":"object","properties":[{"name":"status","description":"Validation status.","type":"string"},{"name":"origin","description":"Origin.","type":"string"},{"name":"logDescription","description":"Log name / description.","type":"string"},{"name":"logId","description":"Log ID.","type":"string"},{"name":"timestamp","description":"Issuance date. Unlike TimeSinceEpoch, this contains the number of\\nmilliseconds since January 1, 1970, UTC, not the number of seconds.","type":"number"},{"name":"hashAlgorithm","description":"Hash algorithm.","type":"string"},{"name":"signatureAlgorithm","description":"Signature algorithm.","type":"string"},{"name":"signatureData","description":"Signature data.","type":"string"}]},{"id":"SecurityDetails","description":"Security details about a request.","type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificateId","description":"Certificate ID value.","$ref":"Security.CertificateId"},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"sanList","description":"Subject Alternative Name (SAN) DNS names and IP addresses.","type":"array","items":{"type":"string"}},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"TimeSinceEpoch"},{"name":"signedCertificateTimestampList","description":"List of signed certificate timestamps (SCTs).","type":"array","items":{"$ref":"SignedCertificateTimestamp"}},{"name":"certificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy","$ref":"CertificateTransparencyCompliance"},{"name":"serverSignatureAlgorithm","description":"The signature algorithm used by the server in the TLS server signature,\\nrepresented as a TLS SignatureScheme code point. Omitted if not\\napplicable or not known.","optional":true,"type":"integer"},{"name":"encryptedClientHello","description":"Whether the connection used Encrypted ClientHello","type":"boolean"}]},{"id":"CertificateTransparencyCompliance","description":"Whether the request complied with Certificate Transparency policy.","type":"string","enum":["unknown","not-compliant","compliant"]},{"id":"BlockedReason","description":"The reason why request was blocked.","type":"string","enum":["other","csp","mixed-content","origin","inspector","subresource-filter","content-type","coep-frame-resource-needs-coep-header","coop-sandboxed-iframe-cannot-navigate-to-coop-page","corp-not-same-origin","corp-not-same-origin-after-defaulted-to-same-origin-by-coep","corp-not-same-site"]},{"id":"CorsError","description":"The reason why request was blocked.","type":"string","enum":["DisallowedByMode","InvalidResponse","WildcardOriginNotAllowed","MissingAllowOriginHeader","MultipleAllowOriginValues","InvalidAllowOriginValue","AllowOriginMismatch","InvalidAllowCredentials","CorsDisabledScheme","PreflightInvalidStatus","PreflightDisallowedRedirect","PreflightWildcardOriginNotAllowed","PreflightMissingAllowOriginHeader","PreflightMultipleAllowOriginValues","PreflightInvalidAllowOriginValue","PreflightAllowOriginMismatch","PreflightInvalidAllowCredentials","PreflightMissingAllowExternal","PreflightInvalidAllowExternal","PreflightMissingAllowPrivateNetwork","PreflightInvalidAllowPrivateNetwork","InvalidAllowMethodsPreflightResponse","InvalidAllowHeadersPreflightResponse","MethodDisallowedByPreflightResponse","HeaderDisallowedByPreflightResponse","RedirectContainsCredentials","InsecurePrivateNetwork","InvalidPrivateNetworkAccess","UnexpectedPrivateNetworkAccess","NoCorsRedirectModeNotFollow","PreflightMissingPrivateNetworkAccessId","PreflightMissingPrivateNetworkAccessName","PrivateNetworkAccessPermissionUnavailable","PrivateNetworkAccessPermissionDenied"]},{"id":"CorsErrorStatus","type":"object","properties":[{"name":"corsError","$ref":"CorsError"},{"name":"failedParameter","type":"string"}]},{"id":"ServiceWorkerResponseSource","description":"Source of serviceworker response.","type":"string","enum":["cache-storage","http-cache","fallback-code","network"]},{"id":"TrustTokenParams","description":"Determines what type of Trust Token operation is executed and\\ndepending on the type, some additional parameters. The values\\nare specified in third_party/blink/renderer/core/fetch/trust_token.idl.","experimental":true,"type":"object","properties":[{"name":"operation","$ref":"TrustTokenOperationType"},{"name":"refreshPolicy","description":"Only set for \\"token-redemption\\" operation and determine whether\\nto request a fresh SRR or use a still valid cached SRR.","type":"string","enum":["UseCached","Refresh"]},{"name":"issuers","description":"Origins of issuers from whom to request tokens or redemption\\nrecords.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"TrustTokenOperationType","experimental":true,"type":"string","enum":["Issuance","Redemption","Signing"]},{"id":"AlternateProtocolUsage","description":"The reason why Chrome uses a specific transport protocol for HTTP semantics.","experimental":true,"type":"string","enum":["alternativeJobWonWithoutRace","alternativeJobWonRace","mainJobWonRace","mappingMissing","broken","dnsAlpnH3JobWonWithoutRace","dnsAlpnH3JobWonRace","unspecifiedReason"]},{"id":"Response","description":"HTTP response data.","type":"object","properties":[{"name":"url","description":"Response URL. This URL can be different from CachedResource.url in case of redirect.","type":"string"},{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo.","deprecated":true,"optional":true,"type":"string"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"requestHeaders","description":"Refined HTTP request headers that were actually transmitted over the network.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo.","deprecated":true,"optional":true,"type":"string"},{"name":"connectionReused","description":"Specifies whether physical connection was actually reused for this request.","type":"boolean"},{"name":"connectionId","description":"Physical connection id that was actually used for this request.","type":"number"},{"name":"remoteIPAddress","description":"Remote IP address.","optional":true,"type":"string"},{"name":"remotePort","description":"Remote port.","optional":true,"type":"integer"},{"name":"fromDiskCache","description":"Specifies that the request was served from the disk cache.","optional":true,"type":"boolean"},{"name":"fromServiceWorker","description":"Specifies that the request was served from the ServiceWorker.","optional":true,"type":"boolean"},{"name":"fromPrefetchCache","description":"Specifies that the request was served from the prefetch cache.","optional":true,"type":"boolean"},{"name":"encodedDataLength","description":"Total number of bytes received for this request so far.","type":"number"},{"name":"timing","description":"Timing information for the given request.","optional":true,"$ref":"ResourceTiming"},{"name":"serviceWorkerResponseSource","description":"Response source of response from ServiceWorker.","optional":true,"$ref":"ServiceWorkerResponseSource"},{"name":"responseTime","description":"The time at which the returned response was generated.","optional":true,"$ref":"TimeSinceEpoch"},{"name":"cacheStorageCacheName","description":"Cache Storage Cache Name.","optional":true,"type":"string"},{"name":"protocol","description":"Protocol used to fetch this request.","optional":true,"type":"string"},{"name":"alternateProtocolUsage","description":"The reason why Chrome uses a specific transport protocol for HTTP semantics.","experimental":true,"optional":true,"$ref":"AlternateProtocolUsage"},{"name":"securityState","description":"Security state of the request resource.","$ref":"Security.SecurityState"},{"name":"securityDetails","description":"Security details for the request.","optional":true,"$ref":"SecurityDetails"}]},{"id":"WebSocketRequest","description":"WebSocket request data.","type":"object","properties":[{"name":"headers","description":"HTTP request headers.","$ref":"Headers"}]},{"id":"WebSocketResponse","description":"WebSocket response data.","type":"object","properties":[{"name":"status","description":"HTTP response status code.","type":"integer"},{"name":"statusText","description":"HTTP response status text.","type":"string"},{"name":"headers","description":"HTTP response headers.","$ref":"Headers"},{"name":"headersText","description":"HTTP response headers text.","optional":true,"type":"string"},{"name":"requestHeaders","description":"HTTP request headers.","optional":true,"$ref":"Headers"},{"name":"requestHeadersText","description":"HTTP request headers text.","optional":true,"type":"string"}]},{"id":"WebSocketFrame","description":"WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.","type":"object","properties":[{"name":"opcode","description":"WebSocket message opcode.","type":"number"},{"name":"mask","description":"WebSocket message mask.","type":"boolean"},{"name":"payloadData","description":"WebSocket message payload data.\\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\\nIf the opcode isn\'t 1, then payloadData is a base64 encoded string representing binary data.","type":"string"}]},{"id":"CachedResource","description":"Information about the cached resource.","type":"object","properties":[{"name":"url","description":"Resource URL. This is the url of the original network request.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"ResourceType"},{"name":"response","description":"Cached response data.","optional":true,"$ref":"Response"},{"name":"bodySize","description":"Cached response body size.","type":"number"}]},{"id":"Initiator","description":"Information about the request initiator.","type":"object","properties":[{"name":"type","description":"Type of this initiator.","type":"string","enum":["parser","script","preload","SignedExchange","preflight","other"]},{"name":"stack","description":"Initiator JavaScript stack trace, set for Script only.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"url","description":"Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.","optional":true,"type":"string"},{"name":"lineNumber","description":"Initiator line number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"},{"name":"columnNumber","description":"Initiator column number, set for Parser type or for Script type (when script is importing\\nmodule) (0-based).","optional":true,"type":"number"},{"name":"requestId","description":"Set if another request triggered this request (e.g. preflight).","optional":true,"$ref":"RequestId"}]},{"id":"Cookie","description":"Cookie object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"domain","description":"Cookie domain.","type":"string"},{"name":"path","description":"Cookie path.","type":"string"},{"name":"expires","description":"Cookie expiration date as the number of seconds since the UNIX epoch.","type":"number"},{"name":"size","description":"Cookie size.","type":"integer"},{"name":"httpOnly","description":"True if cookie is http-only.","type":"boolean"},{"name":"secure","description":"True if cookie is secure.","type":"boolean"},{"name":"session","description":"True in case of session cookie.","type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"priority","description":"Cookie Priority","experimental":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.","experimental":true,"optional":true,"type":"string"},{"name":"partitionKeyOpaque","description":"True if cookie partition key is opaque.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"SetCookieBlockedReason","description":"Types of reasons why a cookie may not be stored from a response.","experimental":true,"type":"string","enum":["SecureOnly","SameSiteStrict","SameSiteLax","SameSiteUnspecifiedTreatedAsLax","SameSiteNoneInsecure","UserPreferences","ThirdPartyBlockedInFirstPartySet","SyntaxError","SchemeNotSupported","OverwriteSecure","InvalidDomain","InvalidPrefix","UnknownError","SchemefulSameSiteStrict","SchemefulSameSiteLax","SchemefulSameSiteUnspecifiedTreatedAsLax","SamePartyFromCrossPartyContext","SamePartyConflictsWithOtherAttributes","NameValuePairExceedsMaxSize"]},{"id":"CookieBlockedReason","description":"Types of reasons why a cookie may not be sent with a request.","experimental":true,"type":"string","enum":["SecureOnly","NotOnPath","DomainMismatch","SameSiteStrict","SameSiteLax","SameSiteUnspecifiedTreatedAsLax","SameSiteNoneInsecure","UserPreferences","ThirdPartyBlockedInFirstPartySet","UnknownError","SchemefulSameSiteStrict","SchemefulSameSiteLax","SchemefulSameSiteUnspecifiedTreatedAsLax","SamePartyFromCrossPartyContext","NameValuePairExceedsMaxSize"]},{"id":"BlockedSetCookieWithReason","description":"A cookie which was not stored from a response with the corresponding reason.","experimental":true,"type":"object","properties":[{"name":"blockedReasons","description":"The reason(s) this cookie was blocked.","type":"array","items":{"$ref":"SetCookieBlockedReason"}},{"name":"cookieLine","description":"The string representing this individual cookie as it would appear in the header.\\nThis is not the entire \\"cookie\\" or \\"set-cookie\\" header which could have multiple cookies.","type":"string"},{"name":"cookie","description":"The cookie object which represents the cookie which was not stored. It is optional because\\nsometimes complete cookie information is not available, such as in the case of parsing\\nerrors.","optional":true,"$ref":"Cookie"}]},{"id":"BlockedCookieWithReason","description":"A cookie with was not sent with a request with the corresponding reason.","experimental":true,"type":"object","properties":[{"name":"blockedReasons","description":"The reason(s) the cookie was blocked.","type":"array","items":{"$ref":"CookieBlockedReason"}},{"name":"cookie","description":"The cookie object representing the cookie which was not sent.","$ref":"Cookie"}]},{"id":"CookieParam","description":"Cookie parameter object","type":"object","properties":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"},{"name":"priority","description":"Cookie Priority.","experimental":true,"optional":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"optional":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"optional":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.\\nIf not set, the cookie will be set as not partitioned.","experimental":true,"optional":true,"type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","experimental":true,"type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","experimental":true,"type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]},{"id":"InterceptionStage","description":"Stages of the interception to begin intercepting. Request will intercept before the request is\\nsent. Response will intercept after the response is received.","experimental":true,"type":"string","enum":["Request","HeadersReceived"]},{"id":"RequestPattern","description":"Request pattern for interception.","experimental":true,"type":"object","properties":[{"name":"urlPattern","description":"Wildcards (`\'*\'` -> zero or more, `\'?\'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\"*\\"`.","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"ResourceType"},{"name":"interceptionStage","description":"Stage at which to begin intercepting requests. Default is Request.","optional":true,"$ref":"InterceptionStage"}]},{"id":"SignedExchangeSignature","description":"Information about a signed exchange signature.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1","experimental":true,"type":"object","properties":[{"name":"label","description":"Signed exchange signature label.","type":"string"},{"name":"signature","description":"The hex string of signed exchange signature.","type":"string"},{"name":"integrity","description":"Signed exchange signature integrity.","type":"string"},{"name":"certUrl","description":"Signed exchange signature cert Url.","optional":true,"type":"string"},{"name":"certSha256","description":"The hex string of signed exchange signature cert sha256.","optional":true,"type":"string"},{"name":"validityUrl","description":"Signed exchange signature validity Url.","type":"string"},{"name":"date","description":"Signed exchange signature date.","type":"integer"},{"name":"expires","description":"Signed exchange signature expires.","type":"integer"},{"name":"certificates","description":"The encoded certificates.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"SignedExchangeHeader","description":"Information about a signed exchange header.\\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation","experimental":true,"type":"object","properties":[{"name":"requestUrl","description":"Signed exchange request URL.","type":"string"},{"name":"responseCode","description":"Signed exchange response code.","type":"integer"},{"name":"responseHeaders","description":"Signed exchange response headers.","$ref":"Headers"},{"name":"signatures","description":"Signed exchange response signature.","type":"array","items":{"$ref":"SignedExchangeSignature"}},{"name":"headerIntegrity","description":"Signed exchange header integrity hash in the form of `sha256-<base64-hash-value>`.","type":"string"}]},{"id":"SignedExchangeErrorField","description":"Field type for a signed exchange related error.","experimental":true,"type":"string","enum":["signatureSig","signatureIntegrity","signatureCertUrl","signatureCertSha256","signatureValidityUrl","signatureTimestamps"]},{"id":"SignedExchangeError","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"signatureIndex","description":"The index of the signature which caused the error.","optional":true,"type":"integer"},{"name":"errorField","description":"The field which caused the error.","optional":true,"$ref":"SignedExchangeErrorField"}]},{"id":"SignedExchangeInfo","description":"Information about a signed exchange response.","experimental":true,"type":"object","properties":[{"name":"outerResponse","description":"The outer response of signed HTTP exchange which was received from network.","$ref":"Response"},{"name":"header","description":"Information about the signed exchange header.","optional":true,"$ref":"SignedExchangeHeader"},{"name":"securityDetails","description":"Security details for the signed exchange header.","optional":true,"$ref":"SecurityDetails"},{"name":"errors","description":"Errors occurred while handling the signed exchagne.","optional":true,"type":"array","items":{"$ref":"SignedExchangeError"}}]},{"id":"ContentEncoding","description":"List of content encodings supported by the backend.","experimental":true,"type":"string","enum":["deflate","gzip","br","zstd"]},{"id":"PrivateNetworkRequestPolicy","experimental":true,"type":"string","enum":["Allow","BlockFromInsecureToMorePrivate","WarnFromInsecureToMorePrivate","PreflightBlock","PreflightWarn"]},{"id":"IPAddressSpace","experimental":true,"type":"string","enum":["Local","Private","Public","Unknown"]},{"id":"ConnectTiming","experimental":true,"type":"object","properties":[{"name":"requestTime","description":"Timing\'s requestTime is a baseline in seconds, while the other numbers are ticks in\\nmilliseconds relatively to this requestTime. Matches ResourceTiming\'s requestTime for\\nthe same request (but not for redirected requests).","type":"number"}]},{"id":"ClientSecurityState","experimental":true,"type":"object","properties":[{"name":"initiatorIsSecureContext","type":"boolean"},{"name":"initiatorIPAddressSpace","$ref":"IPAddressSpace"},{"name":"privateNetworkRequestPolicy","$ref":"PrivateNetworkRequestPolicy"}]},{"id":"CrossOriginOpenerPolicyValue","experimental":true,"type":"string","enum":["SameOrigin","SameOriginAllowPopups","RestrictProperties","UnsafeNone","SameOriginPlusCoep","RestrictPropertiesPlusCoep"]},{"id":"CrossOriginOpenerPolicyStatus","experimental":true,"type":"object","properties":[{"name":"value","$ref":"CrossOriginOpenerPolicyValue"},{"name":"reportOnlyValue","$ref":"CrossOriginOpenerPolicyValue"},{"name":"reportingEndpoint","optional":true,"type":"string"},{"name":"reportOnlyReportingEndpoint","optional":true,"type":"string"}]},{"id":"CrossOriginEmbedderPolicyValue","experimental":true,"type":"string","enum":["None","Credentialless","RequireCorp"]},{"id":"CrossOriginEmbedderPolicyStatus","experimental":true,"type":"object","properties":[{"name":"value","$ref":"CrossOriginEmbedderPolicyValue"},{"name":"reportOnlyValue","$ref":"CrossOriginEmbedderPolicyValue"},{"name":"reportingEndpoint","optional":true,"type":"string"},{"name":"reportOnlyReportingEndpoint","optional":true,"type":"string"}]},{"id":"ContentSecurityPolicySource","experimental":true,"type":"string","enum":["HTTP","Meta"]},{"id":"ContentSecurityPolicyStatus","experimental":true,"type":"object","properties":[{"name":"effectiveDirectives","type":"string"},{"name":"isEnforced","type":"boolean"},{"name":"source","$ref":"ContentSecurityPolicySource"}]},{"id":"SecurityIsolationStatus","experimental":true,"type":"object","properties":[{"name":"coop","optional":true,"$ref":"CrossOriginOpenerPolicyStatus"},{"name":"coep","optional":true,"$ref":"CrossOriginEmbedderPolicyStatus"},{"name":"csp","optional":true,"type":"array","items":{"$ref":"ContentSecurityPolicyStatus"}}]},{"id":"ReportStatus","description":"The status of a Reporting API report.","experimental":true,"type":"string","enum":["Queued","Pending","MarkedForRemoval","Success"]},{"id":"ReportId","experimental":true,"type":"string"},{"id":"ReportingApiReport","description":"An object representing a report generated by the Reporting API.","experimental":true,"type":"object","properties":[{"name":"id","$ref":"ReportId"},{"name":"initiatorUrl","description":"The URL of the document that triggered the report.","type":"string"},{"name":"destination","description":"The name of the endpoint group that should be used to deliver the report.","type":"string"},{"name":"type","description":"The type of the report (specifies the set of data that is contained in the report body).","type":"string"},{"name":"timestamp","description":"When the report was generated.","$ref":"Network.TimeSinceEpoch"},{"name":"depth","description":"How many uploads deep the related request was.","type":"integer"},{"name":"completedAttempts","description":"The number of delivery attempts made so far, not including an active attempt.","type":"integer"},{"name":"body","type":"object"},{"name":"status","$ref":"ReportStatus"}]},{"id":"ReportingApiEndpoint","experimental":true,"type":"object","properties":[{"name":"url","description":"The URL of the endpoint to which reports may be delivered.","type":"string"},{"name":"groupName","description":"Name of the endpoint group.","type":"string"}]},{"id":"LoadNetworkResourcePageResult","description":"An object providing the result of a network resource load.","experimental":true,"type":"object","properties":[{"name":"success","type":"boolean"},{"name":"netError","description":"Optional values used for error reporting.","optional":true,"type":"number"},{"name":"netErrorName","optional":true,"type":"string"},{"name":"httpStatusCode","optional":true,"type":"number"},{"name":"stream","description":"If successful, one of the following two fields holds the result.","optional":true,"$ref":"IO.StreamHandle"},{"name":"headers","description":"Response headers.","optional":true,"$ref":"Network.Headers"}]},{"id":"LoadNetworkResourceOptions","description":"An options object that may be extended later to better support CORS,\\nCORB and streaming.","experimental":true,"type":"object","properties":[{"name":"disableCache","type":"boolean"},{"name":"includeCredentials","type":"boolean"}]}],"commands":[{"name":"setAcceptedEncodings","description":"Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted.","experimental":true,"parameters":[{"name":"encodings","description":"List of accepted content encodings.","type":"array","items":{"$ref":"ContentEncoding"}}]},{"name":"clearAcceptedEncodingsOverride","description":"Clears accepted encodings set by setAcceptedEncodings","experimental":true},{"name":"canClearBrowserCache","description":"Tells whether clearing browser cache is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cache can be cleared.","type":"boolean"}]},{"name":"canClearBrowserCookies","description":"Tells whether clearing browser cookies is supported.","deprecated":true,"returns":[{"name":"result","description":"True if browser cookies can be cleared.","type":"boolean"}]},{"name":"canEmulateNetworkConditions","description":"Tells whether emulation of network conditions is supported.","deprecated":true,"returns":[{"name":"result","description":"True if emulation of network conditions is supported.","type":"boolean"}]},{"name":"clearBrowserCache","description":"Clears browser cache."},{"name":"clearBrowserCookies","description":"Clears browser cookies."},{"name":"continueInterceptedRequest","description":"Response to Network.requestIntercepted which either modifies the request to continue with any\\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\\nevent will be sent with the same InterceptionId.\\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"},{"name":"errorReason","description":"If set this causes the request to fail with the given reason. Passing `Aborted` for requests\\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\\nto an authChallenge.","optional":true,"$ref":"ErrorReason"},{"name":"rawResponse","description":"If set the requests completes using with the provided base64 encoded raw response, including\\nHTTP status line and headers etc... Must not be set in response to an authChallenge. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"url","description":"If set the request url will be modified in a way that\'s not observable by page. Must not be\\nset in response to an authChallenge.","optional":true,"type":"string"},{"name":"method","description":"If set this allows the request method to be overridden. Must not be set in response to an\\nauthChallenge.","optional":true,"type":"string"},{"name":"postData","description":"If set this allows postData to be set. Must not be set in response to an authChallenge.","optional":true,"type":"string"},{"name":"headers","description":"If set this allows the request headers to be changed. Must not be set in response to an\\nauthChallenge.","optional":true,"$ref":"Headers"},{"name":"authChallengeResponse","description":"Response to a requestIntercepted with an authChallenge. Must not be set otherwise.","optional":true,"$ref":"AuthChallengeResponse"}]},{"name":"deleteCookies","description":"Deletes browser cookies with matching name and url or domain/path pair.","parameters":[{"name":"name","description":"Name of the cookies to remove.","type":"string"},{"name":"url","description":"If specified, deletes all the cookies with the given name where domain and path match\\nprovided URL.","optional":true,"type":"string"},{"name":"domain","description":"If specified, deletes only cookies with the exact domain.","optional":true,"type":"string"},{"name":"path","description":"If specified, deletes only cookies with the exact path.","optional":true,"type":"string"}]},{"name":"disable","description":"Disables network tracking, prevents network events from being sent to the client."},{"name":"emulateNetworkConditions","description":"Activates emulation of network conditions.","parameters":[{"name":"offline","description":"True to emulate internet disconnection.","type":"boolean"},{"name":"latency","description":"Minimum latency from request sent to response headers received (ms).","type":"number"},{"name":"downloadThroughput","description":"Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.","type":"number"},{"name":"uploadThroughput","description":"Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.","type":"number"},{"name":"connectionType","description":"Connection type if known.","optional":true,"$ref":"ConnectionType"}]},{"name":"enable","description":"Enables network tracking, network events will now be delivered to the client.","parameters":[{"name":"maxTotalBufferSize","description":"Buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxResourceBufferSize","description":"Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).","experimental":true,"optional":true,"type":"integer"},{"name":"maxPostDataSize","description":"Longest post body size (in bytes) that would be included in requestWillBeSent notification","optional":true,"type":"integer"}]},{"name":"getAllCookies","description":"Returns all browser cookies. Depending on the backend support, will return detailed cookie\\ninformation in the `cookies` field.\\nDeprecated. Use Storage.getCookies instead.","deprecated":true,"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getCertificate","description":"Returns the DER-encoded certificate.","experimental":true,"parameters":[{"name":"origin","description":"Origin to get certificate for.","type":"string"}],"returns":[{"name":"tableNames","type":"array","items":{"type":"string"}}]},{"name":"getCookies","description":"Returns all browser cookies for the current URL. Depending on the backend support, will return\\ndetailed cookie information in the `cookies` field.","parameters":[{"name":"urls","description":"The list of URLs for which applicable cookies will be fetched.\\nIf not specified, it\'s assumed to be set to the list containing\\nthe URLs of the page and all of its subframes.","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Cookie"}}]},{"name":"getResponseBody","description":"Returns content served for the given request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"getRequestPostData","description":"Returns post data sent with the request. Returns an error when no data was sent with the request.","parameters":[{"name":"requestId","description":"Identifier of the network request to get content for.","$ref":"RequestId"}],"returns":[{"name":"postData","description":"Request body string, omitting files from multipart requests","type":"string"}]},{"name":"getResponseBodyForInterception","description":"Returns content served for the given currently intercepted request.","experimental":true,"parameters":[{"name":"interceptionId","description":"Identifier for the intercepted request to get body for.","$ref":"InterceptionId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyForInterceptionAsStream","description":"Returns a handle to the stream representing the response body. Note that after this command,\\nthe intercepted request can\'t be continued as is -- you either need to cancel it or to provide\\nthe response body. The stream only supports sequential read, IO.read will fail if the position\\nis specified.","experimental":true,"parameters":[{"name":"interceptionId","$ref":"InterceptionId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]},{"name":"replayXHR","description":"This method sends a new XMLHttpRequest which is identical to the original one. The following\\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\\nattribute, user, password.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of XHR to replay.","$ref":"RequestId"}]},{"name":"searchInResponseBody","description":"Searches for given string in response content.","experimental":true,"parameters":[{"name":"requestId","description":"Identifier of the network response to search.","$ref":"RequestId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setBlockedURLs","description":"Blocks URLs from loading.","experimental":true,"parameters":[{"name":"urls","description":"URL patterns to block. Wildcards (\'*\') are allowed.","type":"array","items":{"type":"string"}}]},{"name":"setBypassServiceWorker","description":"Toggles ignoring of service worker for each request.","experimental":true,"parameters":[{"name":"bypass","description":"Bypass service worker and load from network.","type":"boolean"}]},{"name":"setCacheDisabled","description":"Toggles ignoring cache for each request. If `true`, cache will not be used.","parameters":[{"name":"cacheDisabled","description":"Cache disabled state.","type":"boolean"}]},{"name":"setCookie","description":"Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.","parameters":[{"name":"name","description":"Cookie name.","type":"string"},{"name":"value","description":"Cookie value.","type":"string"},{"name":"url","description":"The request-URI to associate with the setting of the cookie. This value can affect the\\ndefault domain, path, source port, and source scheme values of the created cookie.","optional":true,"type":"string"},{"name":"domain","description":"Cookie domain.","optional":true,"type":"string"},{"name":"path","description":"Cookie path.","optional":true,"type":"string"},{"name":"secure","description":"True if cookie is secure.","optional":true,"type":"boolean"},{"name":"httpOnly","description":"True if cookie is http-only.","optional":true,"type":"boolean"},{"name":"sameSite","description":"Cookie SameSite type.","optional":true,"$ref":"CookieSameSite"},{"name":"expires","description":"Cookie expiration date, session cookie if not set","optional":true,"$ref":"TimeSinceEpoch"},{"name":"priority","description":"Cookie Priority type.","experimental":true,"optional":true,"$ref":"CookiePriority"},{"name":"sameParty","description":"True if cookie is SameParty.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceScheme","description":"Cookie source scheme type.","experimental":true,"optional":true,"$ref":"CookieSourceScheme"},{"name":"sourcePort","description":"Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\\nThis is a temporary ability and it will be removed in the future.","experimental":true,"optional":true,"type":"integer"},{"name":"partitionKey","description":"Cookie partition key. The site of the top-level URL the browser was visiting at the start\\nof the request to the endpoint that set the cookie.\\nIf not set, the cookie will be set as not partitioned.","experimental":true,"optional":true,"type":"string"}],"returns":[{"name":"success","description":"Always set to true. If an error occurs, the response indicates protocol error.","deprecated":true,"type":"boolean"}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"CookieParam"}}]},{"name":"setExtraHTTPHeaders","description":"Specifies whether to always send extra HTTP headers with the requests from this page.","parameters":[{"name":"headers","description":"Map with extra HTTP headers.","$ref":"Headers"}]},{"name":"setAttachDebugStack","description":"Specifies whether to attach a page script stack id in requests","experimental":true,"parameters":[{"name":"enabled","description":"Whether to attach a page script stack for debugging purpose.","type":"boolean"}]},{"name":"setRequestInterception","description":"Sets the requests to intercept that match the provided patterns and optionally resource types.\\nDeprecated, please use Fetch.enable instead.","experimental":true,"deprecated":true,"parameters":[{"name":"patterns","description":"Requests matching any of these patterns will be forwarded and wait for the corresponding\\ncontinueInterceptedRequest call.","type":"array","items":{"$ref":"RequestPattern"}}]},{"name":"setUserAgentOverride","description":"Allows overriding user agent with the given string.","redirect":"Emulation","parameters":[{"name":"userAgent","description":"User agent to use.","type":"string"},{"name":"acceptLanguage","description":"Browser langugage to emulate.","optional":true,"type":"string"},{"name":"platform","description":"The platform navigator.platform should return.","optional":true,"type":"string"},{"name":"userAgentMetadata","description":"To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData","experimental":true,"optional":true,"$ref":"Emulation.UserAgentMetadata"}]},{"name":"getSecurityIsolationStatus","description":"Returns information about the COEP/COOP isolation status.","experimental":true,"parameters":[{"name":"frameId","description":"If no frameId is provided, the status of the target is provided.","optional":true,"$ref":"Page.FrameId"}],"returns":[{"name":"status","$ref":"SecurityIsolationStatus"}]},{"name":"enableReportingApi","description":"Enables tracking for the Reporting API, events generated by the Reporting API will now be delivered to the client.\\nEnabling triggers \'reportingApiReportAdded\' for all existing reports.","experimental":true,"parameters":[{"name":"enable","description":"Whether to enable or disable events for the Reporting API","type":"boolean"}]},{"name":"loadNetworkResource","description":"Fetches the resource and returns the content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get the resource for. Mandatory for frame targets, and\\nshould be omitted for worker targets.","optional":true,"$ref":"Page.FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"},{"name":"options","description":"Options for the request.","$ref":"LoadNetworkResourceOptions"}],"returns":[{"name":"resource","$ref":"LoadNetworkResourcePageResult"}]}],"events":[{"name":"dataReceived","description":"Fired when data chunk was received over the network.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"dataLength","description":"Data chunk length.","type":"integer"},{"name":"encodedDataLength","description":"Actual bytes received (might be less than dataLength for compressed encodings).","type":"integer"}]},{"name":"eventSourceMessageReceived","description":"Fired when EventSource message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"eventName","description":"Message type.","type":"string"},{"name":"eventId","description":"Message identifier.","type":"string"},{"name":"data","description":"Message content.","type":"string"}]},{"name":"loadingFailed","description":"Fired when HTTP request has failed to load.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"errorText","description":"User friendly error message.","type":"string"},{"name":"canceled","description":"True if loading was canceled.","optional":true,"type":"boolean"},{"name":"blockedReason","description":"The reason why loading was blocked, if any.","optional":true,"$ref":"BlockedReason"},{"name":"corsErrorStatus","description":"The reason why loading was blocked by CORS, if any.","optional":true,"$ref":"CorsErrorStatus"}]},{"name":"loadingFinished","description":"Fired when HTTP request has finished loading.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"encodedDataLength","description":"Total number of bytes received for this request.","type":"number"}]},{"name":"requestIntercepted","description":"Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\\nmocked.\\nDeprecated, use Fetch.requestPaused instead.","experimental":true,"deprecated":true,"parameters":[{"name":"interceptionId","description":"Each request the page makes will have a unique id, however if any redirects are encountered\\nwhile processing that fetch, they will be reported with the same id as the original fetch.\\nLikewise if HTTP authentication is needed then the same fetch id will be used.","$ref":"InterceptionId"},{"name":"request","$ref":"Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"ResourceType"},{"name":"isNavigationRequest","description":"Whether this is a navigation request, which can abort the navigation completely.","type":"boolean"},{"name":"isDownload","description":"Set if the request is a navigation that will result in a download.\\nOnly present after response is received from the server (i.e. HeadersReceived stage).","optional":true,"type":"boolean"},{"name":"redirectUrl","description":"Redirect location, only sent if a redirect was intercepted.","optional":true,"type":"string"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered. If this is set then\\ncontinueInterceptedRequest must contain an authChallengeResponse.","optional":true,"$ref":"AuthChallenge"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage or if redirect occurred while intercepting\\nrequest.","optional":true,"$ref":"ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage or if redirect occurred while intercepting\\nrequest or auth retry occurred.","optional":true,"type":"integer"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage or if redirect occurred while\\nintercepting request or auth retry occurred.","optional":true,"$ref":"Headers"},{"name":"requestId","description":"If the intercepted request had a corresponding requestWillBeSent event fired for it, then\\nthis requestId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"RequestId"}]},{"name":"requestServedFromCache","description":"Fired if request ended up loading from cache.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"}]},{"name":"requestWillBeSent","description":"Fired when page is about to send HTTP request.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"documentURL","description":"URL of the document this request is loaded for.","type":"string"},{"name":"request","description":"Request data.","$ref":"Request"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"Timestamp.","$ref":"TimeSinceEpoch"},{"name":"initiator","description":"Request initiator.","$ref":"Initiator"},{"name":"redirectHasExtraInfo","description":"In the case that redirectResponse is populated, this flag indicates whether\\nrequestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted\\nfor the request which was just redirected.","experimental":true,"type":"boolean"},{"name":"redirectResponse","description":"Redirect response data.","optional":true,"$ref":"Response"},{"name":"type","description":"Type of this resource.","optional":true,"$ref":"ResourceType"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"},{"name":"hasUserGesture","description":"Whether the request is initiated by a user gesture. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"resourceChangedPriority","description":"Fired when resource loading priority is changed","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"newPriority","description":"New priority","$ref":"ResourcePriority"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"signedExchangeReceived","description":"Fired when a signed exchange was received over the network","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"info","description":"Information about the signed exchange response.","$ref":"SignedExchangeInfo"}]},{"name":"responseReceived","description":"Fired when HTTP response is available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"LoaderId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"type","description":"Resource type.","$ref":"ResourceType"},{"name":"response","description":"Response data.","$ref":"Response"},{"name":"hasExtraInfo","description":"Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be\\nor were emitted for this request.","experimental":true,"type":"boolean"},{"name":"frameId","description":"Frame identifier.","optional":true,"$ref":"Page.FrameId"}]},{"name":"webSocketClosed","description":"Fired when WebSocket is closed.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webSocketCreated","description":"Fired upon WebSocket creation.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"url","description":"WebSocket request URL.","type":"string"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webSocketFrameError","description":"Fired when WebSocket message error occurs.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"errorMessage","description":"WebSocket error message.","type":"string"}]},{"name":"webSocketFrameReceived","description":"Fired when WebSocket message is received.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketFrameSent","description":"Fired when WebSocket message is sent.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketFrame"}]},{"name":"webSocketHandshakeResponseReceived","description":"Fired when WebSocket handshake response becomes available.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"response","description":"WebSocket response data.","$ref":"WebSocketResponse"}]},{"name":"webSocketWillSendHandshakeRequest","description":"Fired when WebSocket is about to initiate handshake.","parameters":[{"name":"requestId","description":"Request identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"wallTime","description":"UTC Timestamp.","$ref":"TimeSinceEpoch"},{"name":"request","description":"WebSocket request data.","$ref":"WebSocketRequest"}]},{"name":"webTransportCreated","description":"Fired upon WebTransport creation.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"url","description":"WebTransport request URL.","type":"string"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"},{"name":"initiator","description":"Request initiator.","optional":true,"$ref":"Initiator"}]},{"name":"webTransportConnectionEstablished","description":"Fired when WebTransport handshake is finished.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"webTransportClosed","description":"Fired when WebTransport is disposed.","parameters":[{"name":"transportId","description":"WebTransport identifier.","$ref":"RequestId"},{"name":"timestamp","description":"Timestamp.","$ref":"MonotonicTime"}]},{"name":"requestWillBeSentExtraInfo","description":"Fired when additional information about a requestWillBeSent event is available from the\\nnetwork stack. Not every requestWillBeSent event will have an additional\\nrequestWillBeSentExtraInfo fired for it, and there is no guarantee whether requestWillBeSent\\nor requestWillBeSentExtraInfo will be fired first for the same request.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to an existing requestWillBeSent event.","$ref":"RequestId"},{"name":"associatedCookies","description":"A list of cookies potentially associated to the requested URL. This includes both cookies sent with\\nthe request and the ones not sent; the latter are distinguished by having blockedReason field set.","type":"array","items":{"$ref":"BlockedCookieWithReason"}},{"name":"headers","description":"Raw request headers as they will be sent over the wire.","$ref":"Headers"},{"name":"connectTiming","description":"Connection timing information for the request.","experimental":true,"$ref":"ConnectTiming"},{"name":"clientSecurityState","description":"The client security state set for the request.","optional":true,"$ref":"ClientSecurityState"},{"name":"siteHasCookieInOtherPartition","description":"Whether the site has partitioned cookies stored in a partition different than the current one.","optional":true,"type":"boolean"}]},{"name":"responseReceivedExtraInfo","description":"Fired when additional information about a responseReceived event is available from the network\\nstack. Not every responseReceived event will have an additional responseReceivedExtraInfo for\\nit, and responseReceivedExtraInfo may be fired before or after responseReceived.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another responseReceived event.","$ref":"RequestId"},{"name":"blockedCookies","description":"A list of cookies which were not stored from the response along with the corresponding\\nreasons for blocking. The cookies here may not be valid due to syntax errors, which\\nare represented by the invalid cookie line string instead of a proper cookie.","type":"array","items":{"$ref":"BlockedSetCookieWithReason"}},{"name":"headers","description":"Raw response headers as they were received over the wire.","$ref":"Headers"},{"name":"resourceIPAddressSpace","description":"The IP address space of the resource. The address space can only be determined once the transport\\nestablished the connection, so we can\'t send it in `requestWillBeSentExtraInfo`.","$ref":"IPAddressSpace"},{"name":"statusCode","description":"The status code of the response. This is useful in cases the request failed and no responseReceived\\nevent is triggered, which is the case for, e.g., CORS errors. This is also the correct status code\\nfor cached requests, where the status in responseReceived is a 200 and this will be 304.","type":"integer"},{"name":"headersText","description":"Raw response header text as it was received over the wire. The raw text may not always be\\navailable, such as in the case of HTTP/2 or QUIC.","optional":true,"type":"string"},{"name":"cookiePartitionKey","description":"The cookie partition key that will be used to store partitioned cookies set in this response.\\nOnly sent when partitioned cookies are enabled.","optional":true,"type":"string"},{"name":"cookiePartitionKeyOpaque","description":"True if partitioned cookies are enabled, but the partition key is not serializeable to string.","optional":true,"type":"boolean"}]},{"name":"trustTokenOperationDone","description":"Fired exactly once for each Trust Token operation. Depending on\\nthe type of the operation and whether the operation succeeded or\\nfailed, the event is fired before the corresponding request was sent\\nor after the response was received.","experimental":true,"parameters":[{"name":"status","description":"Detailed success or error status of the operation.\\n\'AlreadyExists\' also signifies a successful operation, as the result\\nof the operation already exists und thus, the operation was abort\\npreemptively (e.g. a cache hit).","type":"string","enum":["Ok","InvalidArgument","MissingIssuerKeys","FailedPrecondition","ResourceExhausted","AlreadyExists","Unavailable","Unauthorized","BadResponse","InternalError","UnknownError","FulfilledLocally"]},{"name":"type","$ref":"TrustTokenOperationType"},{"name":"requestId","$ref":"RequestId"},{"name":"topLevelOrigin","description":"Top level origin. The context in which the operation was attempted.","optional":true,"type":"string"},{"name":"issuerOrigin","description":"Origin of the issuer in case of a \\"Issuance\\" or \\"Redemption\\" operation.","optional":true,"type":"string"},{"name":"issuedTokenCount","description":"The number of obtained Trust Tokens on a successful \\"Issuance\\" operation.","optional":true,"type":"integer"}]},{"name":"subresourceWebBundleMetadataReceived","description":"Fired once when parsing the .wbn file has succeeded.\\nThe event contains the information about the web bundle contents.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another event.","$ref":"RequestId"},{"name":"urls","description":"A list of URLs of resources in the subresource Web Bundle.","type":"array","items":{"type":"string"}}]},{"name":"subresourceWebBundleMetadataError","description":"Fired once when parsing the .wbn file has failed.","experimental":true,"parameters":[{"name":"requestId","description":"Request identifier. Used to match this information to another event.","$ref":"RequestId"},{"name":"errorMessage","description":"Error message","type":"string"}]},{"name":"subresourceWebBundleInnerResponseParsed","description":"Fired when handling requests for resources within a .wbn file.\\nNote: this will only be fired for resources that are requested by the webpage.","experimental":true,"parameters":[{"name":"innerRequestId","description":"Request identifier of the subresource request","$ref":"RequestId"},{"name":"innerRequestURL","description":"URL of the subresource resource.","type":"string"},{"name":"bundleRequestId","description":"Bundle request identifier. Used to match this information to another event.\\nThis made be absent in case when the instrumentation was enabled only\\nafter webbundle was parsed.","optional":true,"$ref":"RequestId"}]},{"name":"subresourceWebBundleInnerResponseError","description":"Fired when request for resources within a .wbn file failed.","experimental":true,"parameters":[{"name":"innerRequestId","description":"Request identifier of the subresource request","$ref":"RequestId"},{"name":"innerRequestURL","description":"URL of the subresource resource.","type":"string"},{"name":"errorMessage","description":"Error message","type":"string"},{"name":"bundleRequestId","description":"Bundle request identifier. Used to match this information to another event.\\nThis made be absent in case when the instrumentation was enabled only\\nafter webbundle was parsed.","optional":true,"$ref":"RequestId"}]},{"name":"reportingApiReportAdded","description":"Is sent whenever a new report is added.\\nAnd after \'enableReportingApi\' for all existing reports.","experimental":true,"parameters":[{"name":"report","$ref":"ReportingApiReport"}]},{"name":"reportingApiReportUpdated","experimental":true,"parameters":[{"name":"report","$ref":"ReportingApiReport"}]},{"name":"reportingApiEndpointsChangedForOrigin","experimental":true,"parameters":[{"name":"origin","description":"Origin of the document(s) which configured the endpoints.","type":"string"},{"name":"endpoints","type":"array","items":{"$ref":"ReportingApiEndpoint"}}]}]},{"domain":"Overlay","description":"This domain provides various functionality related to drawing atop the inspected page.","experimental":true,"dependencies":["DOM","Page","Runtime"],"types":[{"id":"SourceOrderConfig","description":"Configuration data for drawing the source order of an elements children.","type":"object","properties":[{"name":"parentOutlineColor","description":"the color to outline the givent element in.","$ref":"DOM.RGBA"},{"name":"childOutlineColor","description":"the color to outline the child elements in.","$ref":"DOM.RGBA"}]},{"id":"GridHighlightConfig","description":"Configuration data for the highlighting of Grid elements.","type":"object","properties":[{"name":"showGridExtensionLines","description":"Whether the extension lines from grid cells to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showPositiveLineNumbers","description":"Show Positive line number labels (default: false).","optional":true,"type":"boolean"},{"name":"showNegativeLineNumbers","description":"Show Negative line number labels (default: false).","optional":true,"type":"boolean"},{"name":"showAreaNames","description":"Show area name labels (default: false).","optional":true,"type":"boolean"},{"name":"showLineNames","description":"Show line name labels (default: false).","optional":true,"type":"boolean"},{"name":"showTrackSizes","description":"Show track size labels (default: false).","optional":true,"type":"boolean"},{"name":"gridBorderColor","description":"The grid container border highlight color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cellBorderColor","description":"The cell border color (default: transparent). Deprecated, please use rowLineColor and columnLineColor instead.","deprecated":true,"optional":true,"$ref":"DOM.RGBA"},{"name":"rowLineColor","description":"The row line color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnLineColor","description":"The column line color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"gridBorderDash","description":"Whether the grid border is dashed (default: false).","optional":true,"type":"boolean"},{"name":"cellBorderDash","description":"Whether the cell border is dashed (default: false). Deprecated, please us rowLineDash and columnLineDash instead.","deprecated":true,"optional":true,"type":"boolean"},{"name":"rowLineDash","description":"Whether row lines are dashed (default: false).","optional":true,"type":"boolean"},{"name":"columnLineDash","description":"Whether column lines are dashed (default: false).","optional":true,"type":"boolean"},{"name":"rowGapColor","description":"The row gap highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"rowHatchColor","description":"The row gap hatching fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnGapColor","description":"The column gap highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"columnHatchColor","description":"The column gap hatching fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"areaBorderColor","description":"The named grid areas border color (Default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"gridBackgroundColor","description":"The grid container background color (Default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"FlexContainerHighlightConfig","description":"Configuration data for the highlighting of Flex container elements.","type":"object","properties":[{"name":"containerBorder","description":"The style of the container border","optional":true,"$ref":"LineStyle"},{"name":"lineSeparator","description":"The style of the separator between lines","optional":true,"$ref":"LineStyle"},{"name":"itemSeparator","description":"The style of the separator between items","optional":true,"$ref":"LineStyle"},{"name":"mainDistributedSpace","description":"Style of content-distribution space on the main axis (justify-content).","optional":true,"$ref":"BoxStyle"},{"name":"crossDistributedSpace","description":"Style of content-distribution space on the cross axis (align-content).","optional":true,"$ref":"BoxStyle"},{"name":"rowGapSpace","description":"Style of empty space caused by row gaps (gap/row-gap).","optional":true,"$ref":"BoxStyle"},{"name":"columnGapSpace","description":"Style of empty space caused by columns gaps (gap/column-gap).","optional":true,"$ref":"BoxStyle"},{"name":"crossAlignment","description":"Style of the self-alignment line (align-items).","optional":true,"$ref":"LineStyle"}]},{"id":"FlexItemHighlightConfig","description":"Configuration data for the highlighting of Flex item elements.","type":"object","properties":[{"name":"baseSizeBox","description":"Style of the box representing the item\'s base size","optional":true,"$ref":"BoxStyle"},{"name":"baseSizeBorder","description":"Style of the border around the box representing the item\'s base size","optional":true,"$ref":"LineStyle"},{"name":"flexibilityArrow","description":"Style of the arrow representing if the item grew or shrank","optional":true,"$ref":"LineStyle"}]},{"id":"LineStyle","description":"Style information for drawing a line.","type":"object","properties":[{"name":"color","description":"The color of the line (default: transparent)","optional":true,"$ref":"DOM.RGBA"},{"name":"pattern","description":"The line pattern (default: solid)","optional":true,"type":"string","enum":["dashed","dotted"]}]},{"id":"BoxStyle","description":"Style information for drawing a box.","type":"object","properties":[{"name":"fillColor","description":"The background color for the box (default: transparent)","optional":true,"$ref":"DOM.RGBA"},{"name":"hatchColor","description":"The hatching color for the box (default: transparent)","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ContrastAlgorithm","type":"string","enum":["aa","aaa","apca"]},{"id":"HighlightConfig","description":"Configuration data for the highlighting of page elements.","type":"object","properties":[{"name":"showInfo","description":"Whether the node info tooltip should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showStyles","description":"Whether the node styles in the tooltip (default: false).","optional":true,"type":"boolean"},{"name":"showRulers","description":"Whether the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"showAccessibilityInfo","description":"Whether the a11y info should be shown (default: true).","optional":true,"type":"boolean"},{"name":"showExtensionLines","description":"Whether the extension lines from node to the rulers should be shown (default: false).","optional":true,"type":"boolean"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"paddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"borderColor","description":"The border highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"marginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"eventTargetColor","description":"The event target element highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeColor","description":"The shape outside fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"shapeMarginColor","description":"The shape margin fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"cssGridColor","description":"The grid layout color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"colorFormat","description":"The color format used to format color styles (default: hex).","optional":true,"$ref":"ColorFormat"},{"name":"gridHighlightConfig","description":"The grid layout highlight configuration (default: all transparent).","optional":true,"$ref":"GridHighlightConfig"},{"name":"flexContainerHighlightConfig","description":"The flex container highlight configuration (default: all transparent).","optional":true,"$ref":"FlexContainerHighlightConfig"},{"name":"flexItemHighlightConfig","description":"The flex item highlight configuration (default: all transparent).","optional":true,"$ref":"FlexItemHighlightConfig"},{"name":"contrastAlgorithm","description":"The contrast algorithm to use for the contrast ratio (default: aa).","optional":true,"$ref":"ContrastAlgorithm"},{"name":"containerQueryContainerHighlightConfig","description":"The container query container highlight configuration (default: all transparent).","optional":true,"$ref":"ContainerQueryContainerHighlightConfig"}]},{"id":"ColorFormat","type":"string","enum":["rgb","hsl","hwb","hex"]},{"id":"GridNodeHighlightConfig","description":"Configurations for Persistent Grid Highlight","type":"object","properties":[{"name":"gridHighlightConfig","description":"A descriptor for the highlight appearance.","$ref":"GridHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"FlexNodeHighlightConfig","type":"object","properties":[{"name":"flexContainerHighlightConfig","description":"A descriptor for the highlight appearance of flex containers.","$ref":"FlexContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"ScrollSnapContainerHighlightConfig","type":"object","properties":[{"name":"snapportBorder","description":"The style of the snapport border (default: transparent)","optional":true,"$ref":"LineStyle"},{"name":"snapAreaBorder","description":"The style of the snap area border (default: transparent)","optional":true,"$ref":"LineStyle"},{"name":"scrollMarginColor","description":"The margin highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"scrollPaddingColor","description":"The padding highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ScrollSnapHighlightConfig","type":"object","properties":[{"name":"scrollSnapContainerHighlightConfig","description":"A descriptor for the highlight appearance of scroll snap containers.","$ref":"ScrollSnapContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","$ref":"DOM.NodeId"}]},{"id":"HingeConfig","description":"Configuration for dual screen hinge","type":"object","properties":[{"name":"rect","description":"A rectangle represent hinge","$ref":"DOM.Rect"},{"name":"contentColor","description":"The content box highlight fill color (default: a dark color).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"ContainerQueryHighlightConfig","type":"object","properties":[{"name":"containerQueryContainerHighlightConfig","description":"A descriptor for the highlight appearance of container query containers.","$ref":"ContainerQueryContainerHighlightConfig"},{"name":"nodeId","description":"Identifier of the container node to highlight.","$ref":"DOM.NodeId"}]},{"id":"ContainerQueryContainerHighlightConfig","type":"object","properties":[{"name":"containerBorder","description":"The style of the container border.","optional":true,"$ref":"LineStyle"},{"name":"descendantBorder","description":"The style of the descendants\' borders.","optional":true,"$ref":"LineStyle"}]},{"id":"IsolatedElementHighlightConfig","type":"object","properties":[{"name":"isolationModeHighlightConfig","description":"A descriptor for the highlight appearance of an element in isolation mode.","$ref":"IsolationModeHighlightConfig"},{"name":"nodeId","description":"Identifier of the isolated element to highlight.","$ref":"DOM.NodeId"}]},{"id":"IsolationModeHighlightConfig","type":"object","properties":[{"name":"resizerColor","description":"The fill color of the resizers (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"resizerHandleColor","description":"The fill color for resizer handles (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"maskColor","description":"The fill color for the mask covering non-isolated elements (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"id":"InspectMode","type":"string","enum":["searchForNode","searchForUAShadowDOM","captureAreaScreenshot","showDistances","none"]}],"commands":[{"name":"disable","description":"Disables domain notifications."},{"name":"enable","description":"Enables domain notifications."},{"name":"getHighlightObjectForTest","description":"For testing.","parameters":[{"name":"nodeId","description":"Id of the node to get highlight object for.","$ref":"DOM.NodeId"},{"name":"includeDistance","description":"Whether to include distance info.","optional":true,"type":"boolean"},{"name":"includeStyle","description":"Whether to include style info.","optional":true,"type":"boolean"},{"name":"colorFormat","description":"The color format to get config with (default: hex).","optional":true,"$ref":"ColorFormat"},{"name":"showAccessibilityInfo","description":"Whether to show accessibility info (default: true).","optional":true,"type":"boolean"}],"returns":[{"name":"highlight","description":"Highlight data for the node.","type":"object"}]},{"name":"getGridHighlightObjectsForTest","description":"For Persistent Grid testing.","parameters":[{"name":"nodeIds","description":"Ids of the node to get highlight object for.","type":"array","items":{"$ref":"DOM.NodeId"}}],"returns":[{"name":"highlights","description":"Grid Highlight data for the node ids provided.","type":"object"}]},{"name":"getSourceOrderHighlightObjectForTest","description":"For Source Order Viewer testing.","parameters":[{"name":"nodeId","description":"Id of the node to highlight.","$ref":"DOM.NodeId"}],"returns":[{"name":"highlight","description":"Source order highlight data for the node id provided.","type":"object"}]},{"name":"hideHighlight","description":"Hides any highlight."},{"name":"highlightFrame","description":"Highlights owner element of the frame with given id.\\nDeprecated: Doesn\'t work reliablity and cannot be fixed due to process\\nseparatation (the owner node might be in a different process). Determine\\nthe owner node in the client and use highlightNode.","deprecated":true,"parameters":[{"name":"frameId","description":"Identifier of the frame to highlight.","$ref":"Page.FrameId"},{"name":"contentColor","description":"The content box highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"contentOutlineColor","description":"The content box highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightNode","description":"Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\\nobjectId must be specified.","parameters":[{"name":"highlightConfig","description":"A descriptor for the highlight appearance.","$ref":"HighlightConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"},{"name":"selector","description":"Selectors to highlight relevant nodes.","optional":true,"type":"string"}]},{"name":"highlightQuad","description":"Highlights given quad. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"quad","description":"Quad to highlight","$ref":"DOM.Quad"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightRect","description":"Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.","parameters":[{"name":"x","description":"X coordinate","type":"integer"},{"name":"y","description":"Y coordinate","type":"integer"},{"name":"width","description":"Rectangle width","type":"integer"},{"name":"height","description":"Rectangle height","type":"integer"},{"name":"color","description":"The highlight fill color (default: transparent).","optional":true,"$ref":"DOM.RGBA"},{"name":"outlineColor","description":"The highlight outline color (default: transparent).","optional":true,"$ref":"DOM.RGBA"}]},{"name":"highlightSourceOrder","description":"Highlights the source order of the children of the DOM node with given id or with the given\\nJavaScript object wrapper. Either nodeId or objectId must be specified.","parameters":[{"name":"sourceOrderConfig","description":"A descriptor for the appearance of the overlay drawing.","$ref":"SourceOrderConfig"},{"name":"nodeId","description":"Identifier of the node to highlight.","optional":true,"$ref":"DOM.NodeId"},{"name":"backendNodeId","description":"Identifier of the backend node to highlight.","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"objectId","description":"JavaScript object id of the node to be highlighted.","optional":true,"$ref":"Runtime.RemoteObjectId"}]},{"name":"setInspectMode","description":"Enters the \'inspect\' mode. In this mode, elements that user is hovering over are highlighted.\\nBackend then generates \'inspectNodeRequested\' event upon element selection.","parameters":[{"name":"mode","description":"Set an inspection mode.","$ref":"InspectMode"},{"name":"highlightConfig","description":"A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\\n== false`.","optional":true,"$ref":"HighlightConfig"}]},{"name":"setShowAdHighlights","description":"Highlights owner element of all frames detected to be ads.","parameters":[{"name":"show","description":"True for showing ad highlights","type":"boolean"}]},{"name":"setPausedInDebuggerMessage","parameters":[{"name":"message","description":"The message to display, also triggers resume and step over controls.","optional":true,"type":"string"}]},{"name":"setShowDebugBorders","description":"Requests that backend shows debug borders on layers","parameters":[{"name":"show","description":"True for showing debug borders","type":"boolean"}]},{"name":"setShowFPSCounter","description":"Requests that backend shows the FPS counter","parameters":[{"name":"show","description":"True for showing the FPS counter","type":"boolean"}]},{"name":"setShowGridOverlays","description":"Highlight multiple elements with the CSS Grid overlay.","parameters":[{"name":"gridNodeHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"GridNodeHighlightConfig"}}]},{"name":"setShowFlexOverlays","parameters":[{"name":"flexNodeHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"FlexNodeHighlightConfig"}}]},{"name":"setShowScrollSnapOverlays","parameters":[{"name":"scrollSnapHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"ScrollSnapHighlightConfig"}}]},{"name":"setShowContainerQueryOverlays","parameters":[{"name":"containerQueryHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"ContainerQueryHighlightConfig"}}]},{"name":"setShowPaintRects","description":"Requests that backend shows paint rectangles","parameters":[{"name":"result","description":"True for showing paint rectangles","type":"boolean"}]},{"name":"setShowLayoutShiftRegions","description":"Requests that backend shows layout shift regions","parameters":[{"name":"result","description":"True for showing layout shift regions","type":"boolean"}]},{"name":"setShowScrollBottleneckRects","description":"Requests that backend shows scroll bottleneck rects","parameters":[{"name":"show","description":"True for showing scroll bottleneck rects","type":"boolean"}]},{"name":"setShowHitTestBorders","description":"Deprecated, no longer has any effect.","deprecated":true,"parameters":[{"name":"show","description":"True for showing hit-test borders","type":"boolean"}]},{"name":"setShowWebVitals","description":"Request that backend shows an overlay with web vital metrics.","parameters":[{"name":"show","type":"boolean"}]},{"name":"setShowViewportSizeOnResize","description":"Paints viewport size upon main frame resize.","parameters":[{"name":"show","description":"Whether to paint size or not.","type":"boolean"}]},{"name":"setShowHinge","description":"Add a dual screen device hinge","parameters":[{"name":"hingeConfig","description":"hinge data, null means hideHinge","optional":true,"$ref":"HingeConfig"}]},{"name":"setShowIsolatedElements","description":"Show elements in isolation mode with overlays.","parameters":[{"name":"isolatedElementHighlightConfigs","description":"An array of node identifiers and descriptors for the highlight appearance.","type":"array","items":{"$ref":"IsolatedElementHighlightConfig"}}]}],"events":[{"name":"inspectNodeRequested","description":"Fired when the node should be inspected. This happens after call to `setInspectMode` or when\\nuser manually inspects an element.","parameters":[{"name":"backendNodeId","description":"Id of the node to inspect.","$ref":"DOM.BackendNodeId"}]},{"name":"nodeHighlightRequested","description":"Fired when the node should be highlighted. This happens after call to `setInspectMode`.","parameters":[{"name":"nodeId","$ref":"DOM.NodeId"}]},{"name":"screenshotRequested","description":"Fired when user asks to capture screenshot of some area on the page.","parameters":[{"name":"viewport","description":"Viewport to capture, in device independent pixels (dip).","$ref":"Page.Viewport"}]},{"name":"inspectModeCanceled","description":"Fired when user cancels the inspect mode."}]},{"domain":"Page","description":"Actions and events related to the inspected page belong to the page domain.","dependencies":["Debugger","DOM","IO","Network","Runtime"],"types":[{"id":"FrameId","description":"Unique frame identifier.","type":"string"},{"id":"AdFrameType","description":"Indicates whether a frame has been identified as an ad.","experimental":true,"type":"string","enum":["none","child","root"]},{"id":"AdFrameExplanation","experimental":true,"type":"string","enum":["ParentIsAd","CreatedByAdScript","MatchedBlockingRule"]},{"id":"AdFrameStatus","description":"Indicates whether a frame has been identified as an ad and why.","experimental":true,"type":"object","properties":[{"name":"adFrameType","$ref":"AdFrameType"},{"name":"explanations","optional":true,"type":"array","items":{"$ref":"AdFrameExplanation"}}]},{"id":"AdScriptId","description":"Identifies the bottom-most script which caused the frame to be labelled\\nas an ad.","experimental":true,"type":"object","properties":[{"name":"scriptId","description":"Script Id of the bottom-most script which caused the frame to be labelled\\nas an ad.","$ref":"Runtime.ScriptId"},{"name":"debuggerId","description":"Id of adScriptId\'s debugger.","$ref":"Runtime.UniqueDebuggerId"}]},{"id":"SecureContextType","description":"Indicates whether the frame is a secure context and why it is the case.","experimental":true,"type":"string","enum":["Secure","SecureLocalhost","InsecureScheme","InsecureAncestor"]},{"id":"CrossOriginIsolatedContextType","description":"Indicates whether the frame is cross-origin isolated and why it is the case.","experimental":true,"type":"string","enum":["Isolated","NotIsolated","NotIsolatedFeatureDisabled"]},{"id":"GatedAPIFeatures","experimental":true,"type":"string","enum":["SharedArrayBuffers","SharedArrayBuffersTransferAllowed","PerformanceMeasureMemory","PerformanceProfile"]},{"id":"PermissionsPolicyFeature","description":"All Permissions Policy features. This enum should match the one defined\\nin third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.","experimental":true,"type":"string","enum":["accelerometer","ambient-light-sensor","attribution-reporting","autoplay","bluetooth","browsing-topics","camera","ch-dpr","ch-device-memory","ch-downlink","ch-ect","ch-prefers-color-scheme","ch-prefers-reduced-motion","ch-rtt","ch-save-data","ch-ua","ch-ua-arch","ch-ua-bitness","ch-ua-platform","ch-ua-model","ch-ua-mobile","ch-ua-form-factor","ch-ua-full-version","ch-ua-full-version-list","ch-ua-platform-version","ch-ua-wow64","ch-viewport-height","ch-viewport-width","ch-width","clipboard-read","clipboard-write","compute-pressure","cross-origin-isolated","direct-sockets","display-capture","document-domain","encrypted-media","execution-while-out-of-viewport","execution-while-not-rendered","focus-without-user-activation","fullscreen","frobulate","gamepad","geolocation","gyroscope","hid","identity-credentials-get","idle-detection","interest-cohort","join-ad-interest-group","keyboard-map","local-fonts","magnetometer","microphone","midi","otp-credentials","payment","picture-in-picture","private-aggregation","private-state-token-issuance","private-state-token-redemption","publickey-credentials-get","run-ad-auction","screen-wake-lock","serial","shared-autofill","shared-storage","shared-storage-select-url","smart-card","storage-access","sync-xhr","unload","usb","vertical-scroll","web-share","window-management","window-placement","xr-spatial-tracking"]},{"id":"PermissionsPolicyBlockReason","description":"Reason for a permissions policy feature to be disabled.","experimental":true,"type":"string","enum":["Header","IframeAttribute","InFencedFrameTree","InIsolatedApp"]},{"id":"PermissionsPolicyBlockLocator","experimental":true,"type":"object","properties":[{"name":"frameId","$ref":"FrameId"},{"name":"blockReason","$ref":"PermissionsPolicyBlockReason"}]},{"id":"PermissionsPolicyFeatureState","experimental":true,"type":"object","properties":[{"name":"feature","$ref":"PermissionsPolicyFeature"},{"name":"allowed","type":"boolean"},{"name":"locator","optional":true,"$ref":"PermissionsPolicyBlockLocator"}]},{"id":"OriginTrialTokenStatus","description":"Origin Trial(https://www.chromium.org/blink/origin-trials) support.\\nStatus for an Origin Trial token.","experimental":true,"type":"string","enum":["Success","NotSupported","Insecure","Expired","WrongOrigin","InvalidSignature","Malformed","WrongVersion","FeatureDisabled","TokenDisabled","FeatureDisabledForUser","UnknownTrial"]},{"id":"OriginTrialStatus","description":"Status for an Origin Trial.","experimental":true,"type":"string","enum":["Enabled","ValidTokenNotProvided","OSNotSupported","TrialNotAllowed"]},{"id":"OriginTrialUsageRestriction","experimental":true,"type":"string","enum":["None","Subset"]},{"id":"OriginTrialToken","experimental":true,"type":"object","properties":[{"name":"origin","type":"string"},{"name":"matchSubDomains","type":"boolean"},{"name":"trialName","type":"string"},{"name":"expiryTime","$ref":"Network.TimeSinceEpoch"},{"name":"isThirdParty","type":"boolean"},{"name":"usageRestriction","$ref":"OriginTrialUsageRestriction"}]},{"id":"OriginTrialTokenWithStatus","experimental":true,"type":"object","properties":[{"name":"rawTokenText","type":"string"},{"name":"parsedToken","description":"`parsedToken` is present only when the token is extractable and\\nparsable.","optional":true,"$ref":"OriginTrialToken"},{"name":"status","$ref":"OriginTrialTokenStatus"}]},{"id":"OriginTrial","experimental":true,"type":"object","properties":[{"name":"trialName","type":"string"},{"name":"status","$ref":"OriginTrialStatus"},{"name":"tokensWithStatus","type":"array","items":{"$ref":"OriginTrialTokenWithStatus"}}]},{"id":"Frame","description":"Information about the Frame on the page.","type":"object","properties":[{"name":"id","description":"Frame unique identifier.","$ref":"FrameId"},{"name":"parentId","description":"Parent frame identifier.","optional":true,"$ref":"FrameId"},{"name":"loaderId","description":"Identifier of the loader associated with this frame.","$ref":"Network.LoaderId"},{"name":"name","description":"Frame\'s name as specified in the tag.","optional":true,"type":"string"},{"name":"url","description":"Frame document\'s URL without fragment.","type":"string"},{"name":"urlFragment","description":"Frame document\'s URL fragment including the \'#\'.","experimental":true,"optional":true,"type":"string"},{"name":"domainAndRegistry","description":"Frame document\'s registered domain, taking the public suffixes list into account.\\nExtracted from the Frame\'s url.\\nExample URLs: http://www.google.com/file.html -> \\"google.com\\"\\n http://a.b.co.uk/file.html -> \\"b.co.uk\\"","experimental":true,"type":"string"},{"name":"securityOrigin","description":"Frame document\'s security origin.","type":"string"},{"name":"mimeType","description":"Frame document\'s mimeType as determined by the browser.","type":"string"},{"name":"unreachableUrl","description":"If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.","experimental":true,"optional":true,"type":"string"},{"name":"adFrameStatus","description":"Indicates whether this frame was tagged as an ad and why.","experimental":true,"optional":true,"$ref":"AdFrameStatus"},{"name":"secureContextType","description":"Indicates whether the main document is a secure context and explains why that is the case.","experimental":true,"$ref":"SecureContextType"},{"name":"crossOriginIsolatedContextType","description":"Indicates whether this is a cross origin isolated context.","experimental":true,"$ref":"CrossOriginIsolatedContextType"},{"name":"gatedAPIFeatures","description":"Indicated which gated APIs / features are available.","experimental":true,"type":"array","items":{"$ref":"GatedAPIFeatures"}}]},{"id":"FrameResource","description":"Information about the Resource on the page.","experimental":true,"type":"object","properties":[{"name":"url","description":"Resource URL.","type":"string"},{"name":"type","description":"Type of this resource.","$ref":"Network.ResourceType"},{"name":"mimeType","description":"Resource mimeType as determined by the browser.","type":"string"},{"name":"lastModified","description":"last-modified timestamp as reported by server.","optional":true,"$ref":"Network.TimeSinceEpoch"},{"name":"contentSize","description":"Resource content size.","optional":true,"type":"number"},{"name":"failed","description":"True if the resource failed to load.","optional":true,"type":"boolean"},{"name":"canceled","description":"True if the resource was canceled during loading.","optional":true,"type":"boolean"}]},{"id":"FrameResourceTree","description":"Information about the Frame hierarchy along with their cached resources.","experimental":true,"type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameResourceTree"}},{"name":"resources","description":"Information about frame resources.","type":"array","items":{"$ref":"FrameResource"}}]},{"id":"FrameTree","description":"Information about the Frame hierarchy.","type":"object","properties":[{"name":"frame","description":"Frame information for this tree item.","$ref":"Frame"},{"name":"childFrames","description":"Child frames.","optional":true,"type":"array","items":{"$ref":"FrameTree"}}]},{"id":"ScriptIdentifier","description":"Unique script identifier.","type":"string"},{"id":"TransitionType","description":"Transition type.","type":"string","enum":["link","typed","address_bar","auto_bookmark","auto_subframe","manual_subframe","generated","auto_toplevel","form_submit","reload","keyword","keyword_generated","other"]},{"id":"NavigationEntry","description":"Navigation history entry.","type":"object","properties":[{"name":"id","description":"Unique id of the navigation history entry.","type":"integer"},{"name":"url","description":"URL of the navigation history entry.","type":"string"},{"name":"userTypedURL","description":"URL that the user typed in the url bar.","type":"string"},{"name":"title","description":"Title of the navigation history entry.","type":"string"},{"name":"transitionType","description":"Transition type.","$ref":"TransitionType"}]},{"id":"ScreencastFrameMetadata","description":"Screencast frame metadata.","experimental":true,"type":"object","properties":[{"name":"offsetTop","description":"Top offset in DIP.","type":"number"},{"name":"pageScaleFactor","description":"Page scale factor.","type":"number"},{"name":"deviceWidth","description":"Device screen width in DIP.","type":"number"},{"name":"deviceHeight","description":"Device screen height in DIP.","type":"number"},{"name":"scrollOffsetX","description":"Position of horizontal scroll in CSS pixels.","type":"number"},{"name":"scrollOffsetY","description":"Position of vertical scroll in CSS pixels.","type":"number"},{"name":"timestamp","description":"Frame swap timestamp.","optional":true,"$ref":"Network.TimeSinceEpoch"}]},{"id":"DialogType","description":"Javascript dialog type.","type":"string","enum":["alert","confirm","prompt","beforeunload"]},{"id":"AppManifestError","description":"Error while paring app manifest.","type":"object","properties":[{"name":"message","description":"Error message.","type":"string"},{"name":"critical","description":"If criticial, this is a non-recoverable parse error.","type":"integer"},{"name":"line","description":"Error line.","type":"integer"},{"name":"column","description":"Error column.","type":"integer"}]},{"id":"AppManifestParsedProperties","description":"Parsed app manifest properties.","experimental":true,"type":"object","properties":[{"name":"scope","description":"Computed scope value","type":"string"}]},{"id":"LayoutViewport","description":"Layout viewport position and dimensions.","type":"object","properties":[{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"integer"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"integer"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"integer"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"integer"}]},{"id":"VisualViewport","description":"Visual viewport position, dimensions, and scale.","type":"object","properties":[{"name":"offsetX","description":"Horizontal offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"offsetY","description":"Vertical offset relative to the layout viewport (CSS pixels).","type":"number"},{"name":"pageX","description":"Horizontal offset relative to the document (CSS pixels).","type":"number"},{"name":"pageY","description":"Vertical offset relative to the document (CSS pixels).","type":"number"},{"name":"clientWidth","description":"Width (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"clientHeight","description":"Height (CSS pixels), excludes scrollbar if present.","type":"number"},{"name":"scale","description":"Scale relative to the ideal viewport (size at width=device-width).","type":"number"},{"name":"zoom","description":"Page zoom factor (CSS to device independent pixels ratio).","optional":true,"type":"number"}]},{"id":"Viewport","description":"Viewport for capturing screenshot.","type":"object","properties":[{"name":"x","description":"X offset in device independent pixels (dip).","type":"number"},{"name":"y","description":"Y offset in device independent pixels (dip).","type":"number"},{"name":"width","description":"Rectangle width in device independent pixels (dip).","type":"number"},{"name":"height","description":"Rectangle height in device independent pixels (dip).","type":"number"},{"name":"scale","description":"Page scale factor.","type":"number"}]},{"id":"FontFamilies","description":"Generic font families collection.","experimental":true,"type":"object","properties":[{"name":"standard","description":"The standard font-family.","optional":true,"type":"string"},{"name":"fixed","description":"The fixed font-family.","optional":true,"type":"string"},{"name":"serif","description":"The serif font-family.","optional":true,"type":"string"},{"name":"sansSerif","description":"The sansSerif font-family.","optional":true,"type":"string"},{"name":"cursive","description":"The cursive font-family.","optional":true,"type":"string"},{"name":"fantasy","description":"The fantasy font-family.","optional":true,"type":"string"},{"name":"math","description":"The math font-family.","optional":true,"type":"string"}]},{"id":"ScriptFontFamilies","description":"Font families collection for a script.","experimental":true,"type":"object","properties":[{"name":"script","description":"Name of the script which these font families are defined for.","type":"string"},{"name":"fontFamilies","description":"Generic font families collection for the script.","$ref":"FontFamilies"}]},{"id":"FontSizes","description":"Default font sizes.","experimental":true,"type":"object","properties":[{"name":"standard","description":"Default standard font size.","optional":true,"type":"integer"},{"name":"fixed","description":"Default fixed font size.","optional":true,"type":"integer"}]},{"id":"ClientNavigationReason","experimental":true,"type":"string","enum":["formSubmissionGet","formSubmissionPost","httpHeaderRefresh","scriptInitiated","metaTagRefresh","pageBlockInterstitial","reload","anchorClick"]},{"id":"ClientNavigationDisposition","experimental":true,"type":"string","enum":["currentTab","newTab","newWindow","download"]},{"id":"InstallabilityErrorArgument","experimental":true,"type":"object","properties":[{"name":"name","description":"Argument name (e.g. name:\'minimum-icon-size-in-pixels\').","type":"string"},{"name":"value","description":"Argument value (e.g. value:\'64\').","type":"string"}]},{"id":"InstallabilityError","description":"The installability error","experimental":true,"type":"object","properties":[{"name":"errorId","description":"The error id (e.g. \'manifest-missing-suitable-icon\').","type":"string"},{"name":"errorArguments","description":"The list of error arguments (e.g. {name:\'minimum-icon-size-in-pixels\', value:\'64\'}).","type":"array","items":{"$ref":"InstallabilityErrorArgument"}}]},{"id":"ReferrerPolicy","description":"The referring-policy used for the navigation.","experimental":true,"type":"string","enum":["noReferrer","noReferrerWhenDowngrade","origin","originWhenCrossOrigin","sameOrigin","strictOrigin","strictOriginWhenCrossOrigin","unsafeUrl"]},{"id":"CompilationCacheParams","description":"Per-script compilation cache parameters for `Page.produceCompilationCache`","experimental":true,"type":"object","properties":[{"name":"url","description":"The URL of the script to produce a compilation cache entry for.","type":"string"},{"name":"eager","description":"A hint to the backend whether eager compilation is recommended.\\n(the actual compilation mode used is upon backend discretion).","optional":true,"type":"boolean"}]},{"id":"AutoResponseMode","description":"Enum of possible auto-reponse for permisison / prompt dialogs.","experimental":true,"type":"string","enum":["none","autoAccept","autoReject","autoOptOut"]},{"id":"NavigationType","description":"The type of a frameNavigated event.","experimental":true,"type":"string","enum":["Navigation","BackForwardCacheRestore"]},{"id":"BackForwardCacheNotRestoredReason","description":"List of not restored reasons for back-forward cache.","experimental":true,"type":"string","enum":["NotPrimaryMainFrame","BackForwardCacheDisabled","RelatedActiveContentsExist","HTTPStatusNotOK","SchemeNotHTTPOrHTTPS","Loading","WasGrantedMediaAccess","DisableForRenderFrameHostCalled","DomainNotAllowed","HTTPMethodNotGET","SubframeIsNavigating","Timeout","CacheLimit","JavaScriptExecution","RendererProcessKilled","RendererProcessCrashed","SchedulerTrackedFeatureUsed","ConflictingBrowsingInstance","CacheFlushed","ServiceWorkerVersionActivation","SessionRestored","ServiceWorkerPostMessage","EnteredBackForwardCacheBeforeServiceWorkerHostAdded","RenderFrameHostReused_SameSite","RenderFrameHostReused_CrossSite","ServiceWorkerClaim","IgnoreEventAndEvict","HaveInnerContents","TimeoutPuttingInCache","BackForwardCacheDisabledByLowMemory","BackForwardCacheDisabledByCommandLine","NetworkRequestDatapipeDrainedAsBytesConsumer","NetworkRequestRedirected","NetworkRequestTimeout","NetworkExceedsBufferLimit","NavigationCancelledWhileRestoring","NotMostRecentNavigationEntry","BackForwardCacheDisabledForPrerender","UserAgentOverrideDiffers","ForegroundCacheLimit","BrowsingInstanceNotSwapped","BackForwardCacheDisabledForDelegate","UnloadHandlerExistsInMainFrame","UnloadHandlerExistsInSubFrame","ServiceWorkerUnregistration","CacheControlNoStore","CacheControlNoStoreCookieModified","CacheControlNoStoreHTTPOnlyCookieModified","NoResponseHead","Unknown","ActivationNavigationsDisallowedForBug1234857","ErrorDocument","FencedFramesEmbedder","CookieDisabled","HTTPAuthRequired","CookieFlushed","WebSocket","WebTransport","WebRTC","MainResourceHasCacheControlNoStore","MainResourceHasCacheControlNoCache","SubresourceHasCacheControlNoStore","SubresourceHasCacheControlNoCache","ContainsPlugins","DocumentLoaded","DedicatedWorkerOrWorklet","OutstandingNetworkRequestOthers","RequestedMIDIPermission","RequestedAudioCapturePermission","RequestedVideoCapturePermission","RequestedBackForwardCacheBlockedSensors","RequestedBackgroundWorkPermission","BroadcastChannel","WebXR","SharedWorker","WebLocks","WebHID","WebShare","RequestedStorageAccessGrant","WebNfc","OutstandingNetworkRequestFetch","OutstandingNetworkRequestXHR","AppBanner","Printing","WebDatabase","PictureInPicture","Portal","SpeechRecognizer","IdleManager","PaymentManager","SpeechSynthesis","KeyboardLock","WebOTPService","OutstandingNetworkRequestDirectSocket","InjectedJavascript","InjectedStyleSheet","KeepaliveRequest","IndexedDBEvent","Dummy","JsNetworkRequestReceivedCacheControlNoStoreResource","WebRTCSticky","WebTransportSticky","WebSocketSticky","ContentSecurityHandler","ContentWebAuthenticationAPI","ContentFileChooser","ContentSerial","ContentFileSystemAccess","ContentMediaDevicesDispatcherHost","ContentWebBluetooth","ContentWebUSB","ContentMediaSessionService","ContentScreenReader","EmbedderPopupBlockerTabHelper","EmbedderSafeBrowsingTriggeredPopupBlocker","EmbedderSafeBrowsingThreatDetails","EmbedderAppBannerManager","EmbedderDomDistillerViewerSource","EmbedderDomDistillerSelfDeletingRequestDelegate","EmbedderOomInterventionTabHelper","EmbedderOfflinePage","EmbedderChromePasswordManagerClientBindCredentialManager","EmbedderPermissionRequestManager","EmbedderModalDialog","EmbedderExtensions","EmbedderExtensionMessaging","EmbedderExtensionMessagingForOpenPort","EmbedderExtensionSentMessageToCachedFrame"]},{"id":"BackForwardCacheNotRestoredReasonType","description":"Types of not restored reasons for back-forward cache.","experimental":true,"type":"string","enum":["SupportPending","PageSupportNeeded","Circumstantial"]},{"id":"BackForwardCacheNotRestoredExplanation","experimental":true,"type":"object","properties":[{"name":"type","description":"Type of the reason","$ref":"BackForwardCacheNotRestoredReasonType"},{"name":"reason","description":"Not restored reason","$ref":"BackForwardCacheNotRestoredReason"},{"name":"context","description":"Context associated with the reason. The meaning of this context is\\ndependent on the reason:\\n- EmbedderExtensionSentMessageToCachedFrame: the extension ID.","optional":true,"type":"string"}]},{"id":"BackForwardCacheNotRestoredExplanationTree","experimental":true,"type":"object","properties":[{"name":"url","description":"URL of each frame","type":"string"},{"name":"explanations","description":"Not restored reasons of each frame","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanation"}},{"name":"children","description":"Array of children frame","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanationTree"}}]}],"commands":[{"name":"addScriptToEvaluateOnLoad","description":"Deprecated, please use addScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"scriptSource","type":"string"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"addScriptToEvaluateOnNewDocument","description":"Evaluates given script in every frame upon creation (before loading frame\'s scripts).","parameters":[{"name":"source","type":"string"},{"name":"worldName","description":"If specified, creates an isolated world with the given name and evaluates given script in it.\\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\\nevent is emitted.","experimental":true,"optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the script, defaults\\nto false.","experimental":true,"optional":true,"type":"boolean"},{"name":"runImmediately","description":"If true, runs the script immediately on existing execution contexts or worlds.\\nDefault: false.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"identifier","description":"Identifier of the added script.","$ref":"ScriptIdentifier"}]},{"name":"bringToFront","description":"Brings page to front (activates tab)."},{"name":"captureScreenshot","description":"Capture page screenshot.","parameters":[{"name":"format","description":"Image compression format (defaults to png).","optional":true,"type":"string","enum":["jpeg","png","webp"]},{"name":"quality","description":"Compression quality from range [0..100] (jpeg only).","optional":true,"type":"integer"},{"name":"clip","description":"Capture the screenshot of a given region only.","optional":true,"$ref":"Viewport"},{"name":"fromSurface","description":"Capture the screenshot from the surface, rather than the view. Defaults to true.","experimental":true,"optional":true,"type":"boolean"},{"name":"captureBeyondViewport","description":"Capture the screenshot beyond the viewport. Defaults to false.","experimental":true,"optional":true,"type":"boolean"},{"name":"optimizeForSpeed","description":"Optimize image encoding for speed, not for resulting size (defaults to false)","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"data","description":"Base64-encoded image data. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"captureSnapshot","description":"Returns a snapshot of the page as a string. For MHTML format, the serialization includes\\niframes, shadow DOM, external resources, and element-inline styles.","experimental":true,"parameters":[{"name":"format","description":"Format (defaults to mhtml).","optional":true,"type":"string","enum":["mhtml"]}],"returns":[{"name":"data","description":"Serialized page data.","type":"string"}]},{"name":"clearDeviceMetricsOverride","description":"Clears the overridden device metrics.","experimental":true,"deprecated":true,"redirect":"Emulation"},{"name":"clearDeviceOrientationOverride","description":"Clears the overridden Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation"},{"name":"clearGeolocationOverride","description":"Clears the overridden Geolocation Position and Error.","deprecated":true,"redirect":"Emulation"},{"name":"createIsolatedWorld","description":"Creates an isolated world for the given frame.","parameters":[{"name":"frameId","description":"Id of the frame in which the isolated world should be created.","$ref":"FrameId"},{"name":"worldName","description":"An optional name which is reported in the Execution Context.","optional":true,"type":"string"},{"name":"grantUniveralAccess","description":"Whether or not universal access should be granted to the isolated world. This is a powerful\\noption, use with caution.","optional":true,"type":"boolean"}],"returns":[{"name":"executionContextId","description":"Execution context of the isolated world.","$ref":"Runtime.ExecutionContextId"}]},{"name":"deleteCookie","description":"Deletes browser cookie with given name, domain and path.","experimental":true,"deprecated":true,"redirect":"Network","parameters":[{"name":"cookieName","description":"Name of the cookie to remove.","type":"string"},{"name":"url","description":"URL to match cooke domain and path.","type":"string"}]},{"name":"disable","description":"Disables page domain notifications."},{"name":"enable","description":"Enables page domain notifications."},{"name":"getAppManifest","returns":[{"name":"url","description":"Manifest location.","type":"string"},{"name":"errors","type":"array","items":{"$ref":"AppManifestError"}},{"name":"data","description":"Manifest content.","optional":true,"type":"string"},{"name":"parsed","description":"Parsed manifest properties","experimental":true,"optional":true,"$ref":"AppManifestParsedProperties"}]},{"name":"getInstallabilityErrors","experimental":true,"returns":[{"name":"installabilityErrors","type":"array","items":{"$ref":"InstallabilityError"}}]},{"name":"getManifestIcons","description":"Deprecated because it\'s not guaranteed that the returned icon is in fact the one used for PWA installation.","experimental":true,"deprecated":true,"returns":[{"name":"primaryIcon","optional":true,"type":"string"}]},{"name":"getAppId","description":"Returns the unique (PWA) app id.\\nOnly returns values if the feature flag \'WebAppEnableManifestId\' is enabled","experimental":true,"returns":[{"name":"appId","description":"App id, either from manifest\'s id attribute or computed from start_url","optional":true,"type":"string"},{"name":"recommendedId","description":"Recommendation for manifest\'s id attribute to match current id computed from start_url","optional":true,"type":"string"}]},{"name":"getAdScriptId","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"adScriptId","description":"Identifies the bottom-most script which caused the frame to be labelled\\nas an ad. Only sent if frame is labelled as an ad and id is available.","optional":true,"$ref":"AdScriptId"}]},{"name":"getCookies","description":"Returns all browser cookies for the page and all of its subframes. Depending\\non the backend support, will return detailed cookie information in the\\n`cookies` field.","experimental":true,"deprecated":true,"redirect":"Network","returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"getFrameTree","description":"Returns present frame tree structure.","returns":[{"name":"frameTree","description":"Present frame tree structure.","$ref":"FrameTree"}]},{"name":"getLayoutMetrics","description":"Returns metrics relating to the layouting of the page, such as viewport bounds/scale.","returns":[{"name":"layoutViewport","description":"Deprecated metrics relating to the layout viewport. Is in device pixels. Use `cssLayoutViewport` instead.","deprecated":true,"$ref":"LayoutViewport"},{"name":"visualViewport","description":"Deprecated metrics relating to the visual viewport. Is in device pixels. Use `cssVisualViewport` instead.","deprecated":true,"$ref":"VisualViewport"},{"name":"contentSize","description":"Deprecated size of scrollable area. Is in DP. Use `cssContentSize` instead.","deprecated":true,"$ref":"DOM.Rect"},{"name":"cssLayoutViewport","description":"Metrics relating to the layout viewport in CSS pixels.","$ref":"LayoutViewport"},{"name":"cssVisualViewport","description":"Metrics relating to the visual viewport in CSS pixels.","$ref":"VisualViewport"},{"name":"cssContentSize","description":"Size of scrollable area in CSS pixels.","$ref":"DOM.Rect"}]},{"name":"getNavigationHistory","description":"Returns navigation history for the current page.","returns":[{"name":"currentIndex","description":"Index of the current navigation history entry.","type":"integer"},{"name":"entries","description":"Array of navigation history entries.","type":"array","items":{"$ref":"NavigationEntry"}}]},{"name":"resetNavigationHistory","description":"Resets navigation history for the current page."},{"name":"getResourceContent","description":"Returns content of the given resource.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id to get resource for.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to get content for.","type":"string"}],"returns":[{"name":"content","description":"Resource content.","type":"string"},{"name":"base64Encoded","description":"True, if content was served as base64.","type":"boolean"}]},{"name":"getResourceTree","description":"Returns present frame / resource tree structure.","experimental":true,"returns":[{"name":"frameTree","description":"Present frame / resource tree structure.","$ref":"FrameResourceTree"}]},{"name":"handleJavaScriptDialog","description":"Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).","parameters":[{"name":"accept","description":"Whether to accept or dismiss the dialog.","type":"boolean"},{"name":"promptText","description":"The text to enter into the dialog prompt before accepting. Used only if this is a prompt\\ndialog.","optional":true,"type":"string"}]},{"name":"navigate","description":"Navigates current page to the given URL.","parameters":[{"name":"url","description":"URL to navigate the page to.","type":"string"},{"name":"referrer","description":"Referrer URL.","optional":true,"type":"string"},{"name":"transitionType","description":"Intended transition type.","optional":true,"$ref":"TransitionType"},{"name":"frameId","description":"Frame id to navigate, if not specified navigates the top frame.","optional":true,"$ref":"FrameId"},{"name":"referrerPolicy","description":"Referrer-policy used for the navigation.","experimental":true,"optional":true,"$ref":"ReferrerPolicy"}],"returns":[{"name":"frameId","description":"Frame id that has navigated (or failed to navigate)","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. This is omitted in case of same-document navigation,\\nas the previously committed loaderId would not change.","optional":true,"$ref":"Network.LoaderId"},{"name":"errorText","description":"User friendly error message, present if and only if navigation has failed.","optional":true,"type":"string"}]},{"name":"navigateToHistoryEntry","description":"Navigates current page to the given history entry.","parameters":[{"name":"entryId","description":"Unique id of the entry to navigate to.","type":"integer"}]},{"name":"printToPDF","description":"Print page as PDF.","parameters":[{"name":"landscape","description":"Paper orientation. Defaults to false.","optional":true,"type":"boolean"},{"name":"displayHeaderFooter","description":"Display header and footer. Defaults to false.","optional":true,"type":"boolean"},{"name":"printBackground","description":"Print background graphics. Defaults to false.","optional":true,"type":"boolean"},{"name":"scale","description":"Scale of the webpage rendering. Defaults to 1.","optional":true,"type":"number"},{"name":"paperWidth","description":"Paper width in inches. Defaults to 8.5 inches.","optional":true,"type":"number"},{"name":"paperHeight","description":"Paper height in inches. Defaults to 11 inches.","optional":true,"type":"number"},{"name":"marginTop","description":"Top margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginBottom","description":"Bottom margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginLeft","description":"Left margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"marginRight","description":"Right margin in inches. Defaults to 1cm (~0.4 inches).","optional":true,"type":"number"},{"name":"pageRanges","description":"Paper ranges to print, one based, e.g., \'1-5, 8, 11-13\'. Pages are\\nprinted in the document order, not in the order specified, and no\\nmore than once.\\nDefaults to empty string, which implies the entire document is printed.\\nThe page numbers are quietly capped to actual page count of the\\ndocument, and ranges beyond the end of the document are ignored.\\nIf this results in no pages to print, an error is reported.\\nIt is an error to specify a range with start greater than end.","optional":true,"type":"string"},{"name":"headerTemplate","description":"HTML template for the print header. Should be valid HTML markup with following\\nclasses used to inject printing values into them:\\n- `date`: formatted print date\\n- `title`: document title\\n- `url`: document location\\n- `pageNumber`: current page number\\n- `totalPages`: total pages in the document\\n\\nFor example, `<span class=title></span>` would generate span containing the title.","optional":true,"type":"string"},{"name":"footerTemplate","description":"HTML template for the print footer. Should use the same format as the `headerTemplate`.","optional":true,"type":"string"},{"name":"preferCSSPageSize","description":"Whether or not to prefer page size as defined by css. Defaults to false,\\nin which case the content will be scaled to fit the paper size.","optional":true,"type":"boolean"},{"name":"transferMode","description":"return as stream","experimental":true,"optional":true,"type":"string","enum":["ReturnAsBase64","ReturnAsStream"]}],"returns":[{"name":"data","description":"Base64-encoded pdf data. Empty if |returnAsStream| is specified. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"stream","description":"A handle of the stream that holds resulting PDF data.","experimental":true,"optional":true,"$ref":"IO.StreamHandle"}]},{"name":"reload","description":"Reloads given page optionally ignoring the cache.","parameters":[{"name":"ignoreCache","description":"If true, browser cache is ignored (as if the user pressed Shift+refresh).","optional":true,"type":"boolean"},{"name":"scriptToEvaluateOnLoad","description":"If set, the script will be injected into all frames of the inspected page after reload.\\nArgument will be ignored if reloading dataURL origin.","optional":true,"type":"string"}]},{"name":"removeScriptToEvaluateOnLoad","description":"Deprecated, please use removeScriptToEvaluateOnNewDocument instead.","experimental":true,"deprecated":true,"parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"removeScriptToEvaluateOnNewDocument","description":"Removes given script from the list.","parameters":[{"name":"identifier","$ref":"ScriptIdentifier"}]},{"name":"screencastFrameAck","description":"Acknowledges that a screencast frame has been received by the frontend.","experimental":true,"parameters":[{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"searchInResource","description":"Searches for given string in resource content.","experimental":true,"parameters":[{"name":"frameId","description":"Frame id for resource to search in.","$ref":"FrameId"},{"name":"url","description":"URL of the resource to search in.","type":"string"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"Debugger.SearchMatch"}}]},{"name":"setAdBlockingEnabled","description":"Enable Chrome\'s experimental ad filter on all sites.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to block ads.","type":"boolean"}]},{"name":"setBypassCSP","description":"Enable page Content Security Policy by-passing.","experimental":true,"parameters":[{"name":"enabled","description":"Whether to bypass page CSP.","type":"boolean"}]},{"name":"getPermissionsPolicyState","description":"Get Permissions Policy state on given frame.","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"states","type":"array","items":{"$ref":"PermissionsPolicyFeatureState"}}]},{"name":"getOriginTrials","description":"Get Origin Trials on given frame.","experimental":true,"parameters":[{"name":"frameId","$ref":"FrameId"}],"returns":[{"name":"originTrials","type":"array","items":{"$ref":"OriginTrial"}}]},{"name":"setDeviceMetricsOverride","description":"Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\\nwindow.innerWidth, window.innerHeight, and \\"device-width\\"/\\"device-height\\"-related CSS media\\nquery results).","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"width","description":"Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"height","description":"Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.","type":"integer"},{"name":"deviceScaleFactor","description":"Overriding device scale factor value. 0 disables the override.","type":"number"},{"name":"mobile","description":"Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\\nautosizing and more.","type":"boolean"},{"name":"scale","description":"Scale to apply to resulting view image.","optional":true,"type":"number"},{"name":"screenWidth","description":"Overriding screen width value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"screenHeight","description":"Overriding screen height value in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionX","description":"Overriding view X position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"positionY","description":"Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).","optional":true,"type":"integer"},{"name":"dontSetVisibleSize","description":"Do not set visible view size, rely upon explicit setVisibleSize call.","optional":true,"type":"boolean"},{"name":"screenOrientation","description":"Screen orientation override.","optional":true,"$ref":"Emulation.ScreenOrientation"},{"name":"viewport","description":"The viewport dimensions and scale. If not set, the override is cleared.","optional":true,"$ref":"Viewport"}]},{"name":"setDeviceOrientationOverride","description":"Overrides the Device Orientation.","experimental":true,"deprecated":true,"redirect":"DeviceOrientation","parameters":[{"name":"alpha","description":"Mock alpha","type":"number"},{"name":"beta","description":"Mock beta","type":"number"},{"name":"gamma","description":"Mock gamma","type":"number"}]},{"name":"setFontFamilies","description":"Set generic font families.","experimental":true,"parameters":[{"name":"fontFamilies","description":"Specifies font families to set. If a font family is not specified, it won\'t be changed.","$ref":"FontFamilies"},{"name":"forScripts","description":"Specifies font families to set for individual scripts.","optional":true,"type":"array","items":{"$ref":"ScriptFontFamilies"}}]},{"name":"setFontSizes","description":"Set default font sizes.","experimental":true,"parameters":[{"name":"fontSizes","description":"Specifies font sizes to set. If a font size is not specified, it won\'t be changed.","$ref":"FontSizes"}]},{"name":"setDocumentContent","description":"Sets given markup as the document\'s HTML.","parameters":[{"name":"frameId","description":"Frame id to set HTML for.","$ref":"FrameId"},{"name":"html","description":"HTML content to set.","type":"string"}]},{"name":"setDownloadBehavior","description":"Set the behavior when downloading a file.","experimental":true,"deprecated":true,"parameters":[{"name":"behavior","description":"Whether to allow all or deny all download requests, or use default Chrome behavior if\\navailable (otherwise deny).","type":"string","enum":["deny","allow","default"]},{"name":"downloadPath","description":"The default path to save downloaded files to. This is required if behavior is set to \'allow\'","optional":true,"type":"string"}]},{"name":"setGeolocationOverride","description":"Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\\nunavailable.","deprecated":true,"redirect":"Emulation","parameters":[{"name":"latitude","description":"Mock latitude","optional":true,"type":"number"},{"name":"longitude","description":"Mock longitude","optional":true,"type":"number"},{"name":"accuracy","description":"Mock accuracy","optional":true,"type":"number"}]},{"name":"setLifecycleEventsEnabled","description":"Controls whether page will emit lifecycle events.","experimental":true,"parameters":[{"name":"enabled","description":"If true, starts emitting lifecycle events.","type":"boolean"}]},{"name":"setTouchEmulationEnabled","description":"Toggles mouse event-based touch event emulation.","experimental":true,"deprecated":true,"redirect":"Emulation","parameters":[{"name":"enabled","description":"Whether the touch event emulation should be enabled.","type":"boolean"},{"name":"configuration","description":"Touch/gesture events configuration. Default: current platform.","optional":true,"type":"string","enum":["mobile","desktop"]}]},{"name":"startScreencast","description":"Starts sending each frame using the `screencastFrame` event.","experimental":true,"parameters":[{"name":"format","description":"Image compression format.","optional":true,"type":"string","enum":["jpeg","png"]},{"name":"quality","description":"Compression quality from range [0..100].","optional":true,"type":"integer"},{"name":"maxWidth","description":"Maximum screenshot width.","optional":true,"type":"integer"},{"name":"maxHeight","description":"Maximum screenshot height.","optional":true,"type":"integer"},{"name":"everyNthFrame","description":"Send every n-th frame.","optional":true,"type":"integer"}]},{"name":"stopLoading","description":"Force the page stop all navigations and pending resource fetches."},{"name":"crash","description":"Crashes renderer on the IO thread, generates minidumps.","experimental":true},{"name":"close","description":"Tries to close page, running its beforeunload hooks, if any.","experimental":true},{"name":"setWebLifecycleState","description":"Tries to update the web lifecycle state of the page.\\nIt will transition the page to the given state according to:\\nhttps://github.com/WICG/web-lifecycle/","experimental":true,"parameters":[{"name":"state","description":"Target lifecycle state","type":"string","enum":["frozen","active"]}]},{"name":"stopScreencast","description":"Stops sending each frame in the `screencastFrame`.","experimental":true},{"name":"produceCompilationCache","description":"Requests backend to produce compilation cache for the specified scripts.\\n`scripts` are appeneded to the list of scripts for which the cache\\nwould be produced. The list may be reset during page navigation.\\nWhen script with a matching URL is encountered, the cache is optionally\\nproduced upon backend discretion, based on internal heuristics.\\nSee also: `Page.compilationCacheProduced`.","experimental":true,"parameters":[{"name":"scripts","type":"array","items":{"$ref":"CompilationCacheParams"}}]},{"name":"addCompilationCache","description":"Seeds compilation cache for given url. Compilation cache does not survive\\ncross-process navigation.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"clearCompilationCache","description":"Clears seeded compilation cache.","experimental":true},{"name":"setSPCTransactionMode","description":"Sets the Secure Payment Confirmation transaction mode.\\nhttps://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode","experimental":true,"parameters":[{"name":"mode","$ref":"AutoResponseMode"}]},{"name":"setRPHRegistrationMode","description":"Extensions for Custom Handlers API:\\nhttps://html.spec.whatwg.org/multipage/system-state.html#rph-automation","experimental":true,"parameters":[{"name":"mode","$ref":"AutoResponseMode"}]},{"name":"generateTestReport","description":"Generates a report for testing.","experimental":true,"parameters":[{"name":"message","description":"Message to be displayed in the report.","type":"string"},{"name":"group","description":"Specifies the endpoint group to deliver the report to.","optional":true,"type":"string"}]},{"name":"waitForDebugger","description":"Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.","experimental":true},{"name":"setInterceptFileChooserDialog","description":"Intercept file chooser requests and transfer control to protocol clients.\\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\\nInstead, a protocol event `Page.fileChooserOpened` is emitted.","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setPrerenderingAllowed","description":"Enable/disable prerendering manually.\\n\\nThis command is a short-term solution for https://crbug.com/1440085.\\nSee https://docs.google.com/document/d/12HVmFxYj5Jc-eJr5OmWsa2bqTJsbgGLKI6ZIyx0_wpA\\nfor more details.\\n\\nTODO(https://crbug.com/1440085): Remove this once Puppeteer supports tab targets.","experimental":true,"parameters":[{"name":"isAllowed","type":"boolean"}]}],"events":[{"name":"domContentEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"fileChooserOpened","description":"Emitted only when `page.interceptFileChooser` is enabled.","parameters":[{"name":"frameId","description":"Id of the frame containing input node.","experimental":true,"$ref":"FrameId"},{"name":"mode","description":"Input mode.","type":"string","enum":["selectSingle","selectMultiple"]},{"name":"backendNodeId","description":"Input node id. Only present for file choosers opened via an `<input type=\\"file\\">` element.","experimental":true,"optional":true,"$ref":"DOM.BackendNodeId"}]},{"name":"frameAttached","description":"Fired when frame has been attached to its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been attached.","$ref":"FrameId"},{"name":"parentFrameId","description":"Parent frame identifier.","$ref":"FrameId"},{"name":"stack","description":"JavaScript stack trace of when frame was attached, only set if frame initiated from script.","optional":true,"$ref":"Runtime.StackTrace"}]},{"name":"frameClearedScheduledNavigation","description":"Fired when frame no longer has a scheduled navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has cleared its scheduled navigation.","$ref":"FrameId"}]},{"name":"frameDetached","description":"Fired when frame has been detached from its parent.","parameters":[{"name":"frameId","description":"Id of the frame that has been detached.","$ref":"FrameId"},{"name":"reason","experimental":true,"type":"string","enum":["remove","swap"]}]},{"name":"frameNavigated","description":"Fired once navigation of the frame has completed. Frame is now associated with the new loader.","parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"},{"name":"type","experimental":true,"$ref":"NavigationType"}]},{"name":"documentOpened","description":"Fired when opening document to write to.","experimental":true,"parameters":[{"name":"frame","description":"Frame object.","$ref":"Frame"}]},{"name":"frameResized","experimental":true},{"name":"frameRequestedNavigation","description":"Fired when a renderer-initiated navigation is requested.\\nNavigation may still be cancelled after the event is issued.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that is being navigated.","$ref":"FrameId"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the requested navigation.","type":"string"},{"name":"disposition","description":"The disposition for the navigation.","$ref":"ClientNavigationDisposition"}]},{"name":"frameScheduledNavigation","description":"Fired when frame schedules a potential navigation.","deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that has scheduled a navigation.","$ref":"FrameId"},{"name":"delay","description":"Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\\nguaranteed to start.","type":"number"},{"name":"reason","description":"The reason for the navigation.","$ref":"ClientNavigationReason"},{"name":"url","description":"The destination URL for the scheduled navigation.","type":"string"}]},{"name":"frameStartedLoading","description":"Fired when frame has started loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has started loading.","$ref":"FrameId"}]},{"name":"frameStoppedLoading","description":"Fired when frame has stopped loading.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame that has stopped loading.","$ref":"FrameId"}]},{"name":"downloadWillBegin","description":"Fired when page is about to start a download.\\nDeprecated. Use Browser.downloadWillBegin instead.","experimental":true,"deprecated":true,"parameters":[{"name":"frameId","description":"Id of the frame that caused download to begin.","$ref":"FrameId"},{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"url","description":"URL of the resource being downloaded.","type":"string"},{"name":"suggestedFilename","description":"Suggested file name of the resource (the actual name of the file saved on disk may differ).","type":"string"}]},{"name":"downloadProgress","description":"Fired when download makes progress. Last call has |done| == true.\\nDeprecated. Use Browser.downloadProgress instead.","experimental":true,"deprecated":true,"parameters":[{"name":"guid","description":"Global unique identifier of the download.","type":"string"},{"name":"totalBytes","description":"Total expected bytes to download.","type":"number"},{"name":"receivedBytes","description":"Total bytes received.","type":"number"},{"name":"state","description":"Download status.","type":"string","enum":["inProgress","completed","canceled"]}]},{"name":"interstitialHidden","description":"Fired when interstitial page was hidden"},{"name":"interstitialShown","description":"Fired when interstitial page was shown"},{"name":"javascriptDialogClosed","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\\nclosed.","parameters":[{"name":"result","description":"Whether dialog was confirmed.","type":"boolean"},{"name":"userInput","description":"User input in case of prompt.","type":"string"}]},{"name":"javascriptDialogOpening","description":"Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\\nopen.","parameters":[{"name":"url","description":"Frame url.","type":"string"},{"name":"message","description":"Message that will be displayed by the dialog.","type":"string"},{"name":"type","description":"Dialog type.","$ref":"DialogType"},{"name":"hasBrowserHandler","description":"True iff browser is capable showing or acting on the given dialog. When browser has no\\ndialog handler for given target, calling alert while Page domain is engaged will stall\\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.","type":"boolean"},{"name":"defaultPrompt","description":"Default dialog prompt.","optional":true,"type":"string"}]},{"name":"lifecycleEvent","description":"Fired for top level page lifecycle events such as navigation, load, paint, etc.","parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"loaderId","description":"Loader identifier. Empty string if the request is fetched from worker.","$ref":"Network.LoaderId"},{"name":"name","type":"string"},{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"backForwardCacheNotUsed","description":"Fired for failed bfcache history navigations if BackForwardCache feature is enabled. Do\\nnot assume any ordering with the Page.frameNavigated event. This event is fired only for\\nmain-frame history navigation where the document changes (non-same-document navigations),\\nwhen bfcache navigation fails.","experimental":true,"parameters":[{"name":"loaderId","description":"The loader id for the associated navgation.","$ref":"Network.LoaderId"},{"name":"frameId","description":"The frame id of the associated frame.","$ref":"FrameId"},{"name":"notRestoredExplanations","description":"Array of reasons why the page could not be cached. This must not be empty.","type":"array","items":{"$ref":"BackForwardCacheNotRestoredExplanation"}},{"name":"notRestoredExplanationsTree","description":"Tree structure of reasons why the page could not be cached for each frame.","optional":true,"$ref":"BackForwardCacheNotRestoredExplanationTree"}]},{"name":"loadEventFired","parameters":[{"name":"timestamp","$ref":"Network.MonotonicTime"}]},{"name":"navigatedWithinDocument","description":"Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.","experimental":true,"parameters":[{"name":"frameId","description":"Id of the frame.","$ref":"FrameId"},{"name":"url","description":"Frame\'s new url.","type":"string"}]},{"name":"screencastFrame","description":"Compressed image data requested by the `startScreencast`.","experimental":true,"parameters":[{"name":"data","description":"Base64-encoded compressed image. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"metadata","description":"Screencast frame metadata.","$ref":"ScreencastFrameMetadata"},{"name":"sessionId","description":"Frame number.","type":"integer"}]},{"name":"screencastVisibilityChanged","description":"Fired when the page with currently enabled screencast was shown or hidden `.","experimental":true,"parameters":[{"name":"visible","description":"True if the page is visible.","type":"boolean"}]},{"name":"windowOpen","description":"Fired when a new window is going to be opened, via window.open(), link click, form submission,\\netc.","parameters":[{"name":"url","description":"The URL for the new window.","type":"string"},{"name":"windowName","description":"Window name.","type":"string"},{"name":"windowFeatures","description":"An array of enabled window features.","type":"array","items":{"type":"string"}},{"name":"userGesture","description":"Whether or not it was triggered by user gesture.","type":"boolean"}]},{"name":"compilationCacheProduced","description":"Issued for every compilation cache generated. Is only available\\nif Page.setGenerateCompilationCache is enabled.","experimental":true,"parameters":[{"name":"url","type":"string"},{"name":"data","description":"Base64-encoded data (Encoded as a base64 string when passed over JSON)","type":"string"}]}]},{"domain":"Performance","types":[{"id":"Metric","description":"Run-time execution metric.","type":"object","properties":[{"name":"name","description":"Metric name.","type":"string"},{"name":"value","description":"Metric value.","type":"number"}]}],"commands":[{"name":"disable","description":"Disable collecting and reporting metrics."},{"name":"enable","description":"Enable collecting and reporting metrics.","parameters":[{"name":"timeDomain","description":"Time domain to use for collecting and reporting duration metrics.","optional":true,"type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"setTimeDomain","description":"Sets time domain to use for collecting and reporting duration metrics.\\nNote that this must be called before enabling metrics collection. Calling\\nthis method while metrics collection is enabled returns an error.","experimental":true,"deprecated":true,"parameters":[{"name":"timeDomain","description":"Time domain","type":"string","enum":["timeTicks","threadTicks"]}]},{"name":"getMetrics","description":"Retrieve current values of run-time metrics.","returns":[{"name":"metrics","description":"Current values for run-time metrics.","type":"array","items":{"$ref":"Metric"}}]}],"events":[{"name":"metrics","description":"Current values of the metrics.","parameters":[{"name":"metrics","description":"Current values of the metrics.","type":"array","items":{"$ref":"Metric"}},{"name":"title","description":"Timestamp title.","type":"string"}]}]},{"domain":"PerformanceTimeline","description":"Reporting of performance timeline events, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceobserver.","experimental":true,"dependencies":["DOM","Network"],"types":[{"id":"LargestContentfulPaint","description":"See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl","type":"object","properties":[{"name":"renderTime","$ref":"Network.TimeSinceEpoch"},{"name":"loadTime","$ref":"Network.TimeSinceEpoch"},{"name":"size","description":"The number of pixels being painted.","type":"number"},{"name":"elementId","description":"The id attribute of the element, if available.","optional":true,"type":"string"},{"name":"url","description":"The URL of the image (may be trimmed).","optional":true,"type":"string"},{"name":"nodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"LayoutShiftAttribution","type":"object","properties":[{"name":"previousRect","$ref":"DOM.Rect"},{"name":"currentRect","$ref":"DOM.Rect"},{"name":"nodeId","optional":true,"$ref":"DOM.BackendNodeId"}]},{"id":"LayoutShift","description":"See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl","type":"object","properties":[{"name":"value","description":"Score increment produced by this event.","type":"number"},{"name":"hadRecentInput","type":"boolean"},{"name":"lastInputTime","$ref":"Network.TimeSinceEpoch"},{"name":"sources","type":"array","items":{"$ref":"LayoutShiftAttribution"}}]},{"id":"TimelineEvent","type":"object","properties":[{"name":"frameId","description":"Identifies the frame that this event is related to. Empty for non-frame targets.","$ref":"Page.FrameId"},{"name":"type","description":"The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThis determines which of the optional \\"details\\" fiedls is present.","type":"string"},{"name":"name","description":"Name may be empty depending on the type.","type":"string"},{"name":"time","description":"Time in seconds since Epoch, monotonically increasing within document lifetime.","$ref":"Network.TimeSinceEpoch"},{"name":"duration","description":"Event duration, if applicable.","optional":true,"type":"number"},{"name":"lcpDetails","optional":true,"$ref":"LargestContentfulPaint"},{"name":"layoutShiftDetails","optional":true,"$ref":"LayoutShift"}]}],"commands":[{"name":"enable","description":"Previously buffered events would be reported before method returns.\\nSee also: timelineEventAdded","parameters":[{"name":"eventTypes","description":"The types of event to report, as specified in\\nhttps://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\\nThe specified filter overrides any previous filters, passing empty\\nfilter disables recording.\\nNote that not all types exposed to the web platform are currently supported.","type":"array","items":{"type":"string"}}]}],"events":[{"name":"timelineEventAdded","description":"Sent when a performance timeline event is added. See reportPerformanceTimeline method.","parameters":[{"name":"event","$ref":"TimelineEvent"}]}]},{"domain":"Security","description":"Security","types":[{"id":"CertificateId","description":"An internal certificate ID value.","type":"integer"},{"id":"MixedContentType","description":"A description of mixed content (HTTP resources on HTTPS pages), as defined by\\nhttps://www.w3.org/TR/mixed-content/#categories","type":"string","enum":["blockable","optionally-blockable","none"]},{"id":"SecurityState","description":"The security level of a page or resource.","type":"string","enum":["unknown","neutral","insecure","secure","info","insecure-broken"]},{"id":"CertificateSecurityState","description":"Details about the security state of the page certificate.","experimental":true,"type":"object","properties":[{"name":"protocol","description":"Protocol name (e.g. \\"TLS 1.2\\" or \\"QUIC\\").","type":"string"},{"name":"keyExchange","description":"Key Exchange used by the connection, or the empty string if not applicable.","type":"string"},{"name":"keyExchangeGroup","description":"(EC)DH group used by the connection, if applicable.","optional":true,"type":"string"},{"name":"cipher","description":"Cipher name.","type":"string"},{"name":"mac","description":"TLS MAC. Note that AEAD ciphers do not have separate MACs.","optional":true,"type":"string"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"subjectName","description":"Certificate subject name.","type":"string"},{"name":"issuer","description":"Name of the issuing CA.","type":"string"},{"name":"validFrom","description":"Certificate valid from date.","$ref":"Network.TimeSinceEpoch"},{"name":"validTo","description":"Certificate valid to (expiration) date","$ref":"Network.TimeSinceEpoch"},{"name":"certificateNetworkError","description":"The highest priority network error code, if the certificate has an error.","optional":true,"type":"string"},{"name":"certificateHasWeakSignature","description":"True if the certificate uses a weak signature aglorithm.","type":"boolean"},{"name":"certificateHasSha1Signature","description":"True if the certificate has a SHA1 signature in the chain.","type":"boolean"},{"name":"modernSSL","description":"True if modern SSL","type":"boolean"},{"name":"obsoleteSslProtocol","description":"True if the connection is using an obsolete SSL protocol.","type":"boolean"},{"name":"obsoleteSslKeyExchange","description":"True if the connection is using an obsolete SSL key exchange.","type":"boolean"},{"name":"obsoleteSslCipher","description":"True if the connection is using an obsolete SSL cipher.","type":"boolean"},{"name":"obsoleteSslSignature","description":"True if the connection is using an obsolete SSL signature.","type":"boolean"}]},{"id":"SafetyTipStatus","experimental":true,"type":"string","enum":["badReputation","lookalike"]},{"id":"SafetyTipInfo","experimental":true,"type":"object","properties":[{"name":"safetyTipStatus","description":"Describes whether the page triggers any safety tips or reputation warnings. Default is unknown.","$ref":"SafetyTipStatus"},{"name":"safeUrl","description":"The URL the safety tip suggested (\\"Did you mean?\\"). Only filled in for lookalike matches.","optional":true,"type":"string"}]},{"id":"VisibleSecurityState","description":"Security state information about the page.","experimental":true,"type":"object","properties":[{"name":"securityState","description":"The security level of the page.","$ref":"SecurityState"},{"name":"certificateSecurityState","description":"Security state details about the page certificate.","optional":true,"$ref":"CertificateSecurityState"},{"name":"safetyTipInfo","description":"The type of Safety Tip triggered on the page. Note that this field will be set even if the Safety Tip UI was not actually shown.","optional":true,"$ref":"SafetyTipInfo"},{"name":"securityStateIssueIds","description":"Array of security state issues ids.","type":"array","items":{"type":"string"}}]},{"id":"SecurityStateExplanation","description":"An explanation of an factor contributing to the security state.","type":"object","properties":[{"name":"securityState","description":"Security state representing the severity of the factor being explained.","$ref":"SecurityState"},{"name":"title","description":"Title describing the type of factor.","type":"string"},{"name":"summary","description":"Short phrase describing the type of factor.","type":"string"},{"name":"description","description":"Full text explanation of the factor.","type":"string"},{"name":"mixedContentType","description":"The type of mixed content described by the explanation.","$ref":"MixedContentType"},{"name":"certificate","description":"Page certificate.","type":"array","items":{"type":"string"}},{"name":"recommendations","description":"Recommendations to fix any issues.","optional":true,"type":"array","items":{"type":"string"}}]},{"id":"InsecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"type":"object","properties":[{"name":"ranMixedContent","description":"Always false.","type":"boolean"},{"name":"displayedMixedContent","description":"Always false.","type":"boolean"},{"name":"containedMixedForm","description":"Always false.","type":"boolean"},{"name":"ranContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"displayedContentWithCertErrors","description":"Always false.","type":"boolean"},{"name":"ranInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"},{"name":"displayedInsecureContentStyle","description":"Always set to unknown.","$ref":"SecurityState"}]},{"id":"CertificateErrorAction","description":"The action to take when a certificate error occurs. continue will continue processing the\\nrequest and cancel will cancel the request.","type":"string","enum":["continue","cancel"]}],"commands":[{"name":"disable","description":"Disables tracking security state changes."},{"name":"enable","description":"Enables tracking security state changes."},{"name":"setIgnoreCertificateErrors","description":"Enable/disable whether all certificate errors should be ignored.","experimental":true,"parameters":[{"name":"ignore","description":"If true, all certificate errors will be ignored.","type":"boolean"}]},{"name":"handleCertificateError","description":"Handles a certificate error that fired a certificateError event.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"action","description":"The action to take on the certificate error.","$ref":"CertificateErrorAction"}]},{"name":"setOverrideCertificateErrors","description":"Enable/disable overriding certificate errors. If enabled, all certificate error events need to\\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.","deprecated":true,"parameters":[{"name":"override","description":"If true, certificate errors will be overridden.","type":"boolean"}]}],"events":[{"name":"certificateError","description":"There is a certificate error. If overriding certificate errors is enabled, then it should be\\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\\ncertificate error has been allowed internally. Only one client per target should override\\ncertificate errors at the same time.","deprecated":true,"parameters":[{"name":"eventId","description":"The ID of the event.","type":"integer"},{"name":"errorType","description":"The type of the error.","type":"string"},{"name":"requestURL","description":"The url that was requested.","type":"string"}]},{"name":"visibleSecurityStateChanged","description":"The security state of the page changed.","experimental":true,"parameters":[{"name":"visibleSecurityState","description":"Security state information about the page.","$ref":"VisibleSecurityState"}]},{"name":"securityStateChanged","description":"The security state of the page changed. No longer being sent.","deprecated":true,"parameters":[{"name":"securityState","description":"Security state.","$ref":"SecurityState"},{"name":"schemeIsCryptographic","description":"True if the page was loaded over cryptographic transport such as HTTPS.","deprecated":true,"type":"boolean"},{"name":"explanations","description":"Previously a list of explanations for the security state. Now always\\nempty.","deprecated":true,"type":"array","items":{"$ref":"SecurityStateExplanation"}},{"name":"insecureContentStatus","description":"Information about insecure content on the page.","deprecated":true,"$ref":"InsecureContentStatus"},{"name":"summary","description":"Overrides user-visible description of the state. Always omitted.","deprecated":true,"optional":true,"type":"string"}]}]},{"domain":"ServiceWorker","experimental":true,"dependencies":["Target"],"types":[{"id":"RegistrationID","type":"string"},{"id":"ServiceWorkerRegistration","description":"ServiceWorker registration.","type":"object","properties":[{"name":"registrationId","$ref":"RegistrationID"},{"name":"scopeURL","type":"string"},{"name":"isDeleted","type":"boolean"}]},{"id":"ServiceWorkerVersionRunningStatus","type":"string","enum":["stopped","starting","running","stopping"]},{"id":"ServiceWorkerVersionStatus","type":"string","enum":["new","installing","installed","activating","activated","redundant"]},{"id":"ServiceWorkerVersion","description":"ServiceWorker version.","type":"object","properties":[{"name":"versionId","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"scriptURL","type":"string"},{"name":"runningStatus","$ref":"ServiceWorkerVersionRunningStatus"},{"name":"status","$ref":"ServiceWorkerVersionStatus"},{"name":"scriptLastModified","description":"The Last-Modified header value of the main script.","optional":true,"type":"number"},{"name":"scriptResponseTime","description":"The time at which the response headers of the main script were received from the server.\\nFor cached script it is the last time the cache entry was validated.","optional":true,"type":"number"},{"name":"controlledClients","optional":true,"type":"array","items":{"$ref":"Target.TargetID"}},{"name":"targetId","optional":true,"$ref":"Target.TargetID"}]},{"id":"ServiceWorkerErrorMessage","description":"ServiceWorker error message.","type":"object","properties":[{"name":"errorMessage","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"versionId","type":"string"},{"name":"sourceURL","type":"string"},{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]}],"commands":[{"name":"deliverPushMessage","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"data","type":"string"}]},{"name":"disable"},{"name":"dispatchSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"},{"name":"lastChance","type":"boolean"}]},{"name":"dispatchPeriodicSyncEvent","parameters":[{"name":"origin","type":"string"},{"name":"registrationId","$ref":"RegistrationID"},{"name":"tag","type":"string"}]},{"name":"enable"},{"name":"inspectWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"setForceUpdateOnPageLoad","parameters":[{"name":"forceUpdateOnPageLoad","type":"boolean"}]},{"name":"skipWaiting","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"startWorker","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"stopAllWorkers"},{"name":"stopWorker","parameters":[{"name":"versionId","type":"string"}]},{"name":"unregister","parameters":[{"name":"scopeURL","type":"string"}]},{"name":"updateRegistration","parameters":[{"name":"scopeURL","type":"string"}]}],"events":[{"name":"workerErrorReported","parameters":[{"name":"errorMessage","$ref":"ServiceWorkerErrorMessage"}]},{"name":"workerRegistrationUpdated","parameters":[{"name":"registrations","type":"array","items":{"$ref":"ServiceWorkerRegistration"}}]},{"name":"workerVersionUpdated","parameters":[{"name":"versions","type":"array","items":{"$ref":"ServiceWorkerVersion"}}]}]},{"domain":"Storage","experimental":true,"dependencies":["Browser","Network"],"types":[{"id":"SerializedStorageKey","type":"string"},{"id":"StorageType","description":"Enum of possible storage types.","type":"string","enum":["appcache","cookies","file_systems","indexeddb","local_storage","shader_cache","websql","service_workers","cache_storage","interest_groups","shared_storage","storage_buckets","all","other"]},{"id":"UsageForType","description":"Usage for a storage type.","type":"object","properties":[{"name":"storageType","description":"Name of storage type.","$ref":"StorageType"},{"name":"usage","description":"Storage usage (bytes).","type":"number"}]},{"id":"TrustTokens","description":"Pair of issuer origin and number of available (signed, but not used) Trust\\nTokens from that issuer.","experimental":true,"type":"object","properties":[{"name":"issuerOrigin","type":"string"},{"name":"count","type":"number"}]},{"id":"InterestGroupAccessType","description":"Enum of interest group access types.","type":"string","enum":["join","leave","update","loaded","bid","win"]},{"id":"InterestGroupAd","description":"Ad advertising element inside an interest group.","type":"object","properties":[{"name":"renderUrl","type":"string"},{"name":"metadata","optional":true,"type":"string"}]},{"id":"InterestGroupDetails","description":"The full details of an interest group.","type":"object","properties":[{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"},{"name":"expirationTime","$ref":"Network.TimeSinceEpoch"},{"name":"joiningOrigin","type":"string"},{"name":"biddingUrl","optional":true,"type":"string"},{"name":"biddingWasmHelperUrl","optional":true,"type":"string"},{"name":"updateUrl","optional":true,"type":"string"},{"name":"trustedBiddingSignalsUrl","optional":true,"type":"string"},{"name":"trustedBiddingSignalsKeys","type":"array","items":{"type":"string"}},{"name":"userBiddingSignals","optional":true,"type":"string"},{"name":"ads","type":"array","items":{"$ref":"InterestGroupAd"}},{"name":"adComponents","type":"array","items":{"$ref":"InterestGroupAd"}}]},{"id":"SharedStorageAccessType","description":"Enum of shared storage access types.","type":"string","enum":["documentAddModule","documentSelectURL","documentRun","documentSet","documentAppend","documentDelete","documentClear","workletSet","workletAppend","workletDelete","workletClear","workletGet","workletKeys","workletEntries","workletLength","workletRemainingBudget"]},{"id":"SharedStorageEntry","description":"Struct for a single key-value pair in an origin\'s shared storage.","type":"object","properties":[{"name":"key","type":"string"},{"name":"value","type":"string"}]},{"id":"SharedStorageMetadata","description":"Details for an origin\'s shared storage.","type":"object","properties":[{"name":"creationTime","$ref":"Network.TimeSinceEpoch"},{"name":"length","type":"integer"},{"name":"remainingBudget","type":"number"}]},{"id":"SharedStorageReportingMetadata","description":"Pair of reporting metadata details for a candidate URL for `selectURL()`.","type":"object","properties":[{"name":"eventType","type":"string"},{"name":"reportingUrl","type":"string"}]},{"id":"SharedStorageUrlWithMetadata","description":"Bundles a candidate URL with its reporting metadata.","type":"object","properties":[{"name":"url","description":"Spec of candidate URL.","type":"string"},{"name":"reportingMetadata","description":"Any associated reporting metadata.","type":"array","items":{"$ref":"SharedStorageReportingMetadata"}}]},{"id":"SharedStorageAccessParams","description":"Bundles the parameters for shared storage access events whose\\npresence/absence can vary according to SharedStorageAccessType.","type":"object","properties":[{"name":"scriptSourceUrl","description":"Spec of the module script URL.\\nPresent only for SharedStorageAccessType.documentAddModule.","optional":true,"type":"string"},{"name":"operationName","description":"Name of the registered operation to be run.\\nPresent only for SharedStorageAccessType.documentRun and\\nSharedStorageAccessType.documentSelectURL.","optional":true,"type":"string"},{"name":"serializedData","description":"The operation\'s serialized data in bytes (converted to a string).\\nPresent only for SharedStorageAccessType.documentRun and\\nSharedStorageAccessType.documentSelectURL.","optional":true,"type":"string"},{"name":"urlsWithMetadata","description":"Array of candidate URLs\' specs, along with any associated metadata.\\nPresent only for SharedStorageAccessType.documentSelectURL.","optional":true,"type":"array","items":{"$ref":"SharedStorageUrlWithMetadata"}},{"name":"key","description":"Key for a specific entry in an origin\'s shared storage.\\nPresent only for SharedStorageAccessType.documentSet,\\nSharedStorageAccessType.documentAppend,\\nSharedStorageAccessType.documentDelete,\\nSharedStorageAccessType.workletSet,\\nSharedStorageAccessType.workletAppend,\\nSharedStorageAccessType.workletDelete, and\\nSharedStorageAccessType.workletGet.","optional":true,"type":"string"},{"name":"value","description":"Value for a specific entry in an origin\'s shared storage.\\nPresent only for SharedStorageAccessType.documentSet,\\nSharedStorageAccessType.documentAppend,\\nSharedStorageAccessType.workletSet, and\\nSharedStorageAccessType.workletAppend.","optional":true,"type":"string"},{"name":"ignoreIfPresent","description":"Whether or not to set an entry for a key if that key is already present.\\nPresent only for SharedStorageAccessType.documentSet and\\nSharedStorageAccessType.workletSet.","optional":true,"type":"boolean"}]},{"id":"StorageBucketsDurability","type":"string","enum":["relaxed","strict"]},{"id":"StorageBucket","type":"object","properties":[{"name":"storageKey","$ref":"SerializedStorageKey"},{"name":"name","description":"If not specified, it is the default bucket of the storageKey.","optional":true,"type":"string"}]},{"id":"StorageBucketInfo","type":"object","properties":[{"name":"bucket","$ref":"StorageBucket"},{"name":"id","type":"string"},{"name":"expiration","$ref":"Network.TimeSinceEpoch"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"persistent","type":"boolean"},{"name":"durability","$ref":"StorageBucketsDurability"}]},{"id":"AttributionReportingSourceType","experimental":true,"type":"string","enum":["navigation","event"]},{"id":"UnsignedInt64AsBase10","experimental":true,"type":"string"},{"id":"UnsignedInt128AsBase16","experimental":true,"type":"string"},{"id":"SignedInt64AsBase10","experimental":true,"type":"string"},{"id":"AttributionReportingFilterDataEntry","experimental":true,"type":"object","properties":[{"name":"key","type":"string"},{"name":"values","type":"array","items":{"type":"string"}}]},{"id":"AttributionReportingAggregationKeysEntry","experimental":true,"type":"object","properties":[{"name":"key","type":"string"},{"name":"value","$ref":"UnsignedInt128AsBase16"}]},{"id":"AttributionReportingSourceRegistration","experimental":true,"type":"object","properties":[{"name":"time","$ref":"Network.TimeSinceEpoch"},{"name":"expiry","description":"duration in seconds","optional":true,"type":"integer"},{"name":"eventReportWindow","description":"duration in seconds","optional":true,"type":"integer"},{"name":"aggregatableReportWindow","description":"duration in seconds","optional":true,"type":"integer"},{"name":"type","$ref":"AttributionReportingSourceType"},{"name":"sourceOrigin","type":"string"},{"name":"reportingOrigin","type":"string"},{"name":"destinationSites","type":"array","items":{"type":"string"}},{"name":"eventId","$ref":"UnsignedInt64AsBase10"},{"name":"priority","$ref":"SignedInt64AsBase10"},{"name":"filterData","type":"array","items":{"$ref":"AttributionReportingFilterDataEntry"}},{"name":"aggregationKeys","type":"array","items":{"$ref":"AttributionReportingAggregationKeysEntry"}},{"name":"debugKey","optional":true,"$ref":"UnsignedInt64AsBase10"}]},{"id":"AttributionReportingSourceRegistrationResult","experimental":true,"type":"string","enum":["success","internalError","insufficientSourceCapacity","insufficientUniqueDestinationCapacity","excessiveReportingOrigins","prohibitedByBrowserPolicy","successNoised","destinationReportingLimitReached","destinationGlobalLimitReached","destinationBothLimitsReached","reportingOriginsPerSiteLimitReached"]}],"commands":[{"name":"getStorageKeyForFrame","description":"Returns a storage key given a frame id.","parameters":[{"name":"frameId","$ref":"Page.FrameId"}],"returns":[{"name":"storageKey","$ref":"SerializedStorageKey"}]},{"name":"clearDataForOrigin","description":"Clears storage for origin.","parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"clearDataForStorageKey","description":"Clears storage for storage key.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"},{"name":"storageTypes","description":"Comma separated list of StorageType to clear.","type":"string"}]},{"name":"getCookies","description":"Returns all browser cookies.","parameters":[{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}],"returns":[{"name":"cookies","description":"Array of cookie objects.","type":"array","items":{"$ref":"Network.Cookie"}}]},{"name":"setCookies","description":"Sets given cookies.","parameters":[{"name":"cookies","description":"Cookies to be set.","type":"array","items":{"$ref":"Network.CookieParam"}},{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}]},{"name":"clearCookies","description":"Clears cookies.","parameters":[{"name":"browserContextId","description":"Browser context to use when called on the browser endpoint.","optional":true,"$ref":"Browser.BrowserContextID"}]},{"name":"getUsageAndQuota","description":"Returns usage and quota in bytes.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}],"returns":[{"name":"usage","description":"Storage usage (bytes).","type":"number"},{"name":"quota","description":"Storage quota (bytes).","type":"number"},{"name":"overrideActive","description":"Whether or not the origin has an active storage quota override","type":"boolean"},{"name":"usageBreakdown","description":"Storage usage per type (bytes).","type":"array","items":{"$ref":"UsageForType"}}]},{"name":"overrideQuotaForOrigin","description":"Override quota for the specified origin","experimental":true,"parameters":[{"name":"origin","description":"Security origin.","type":"string"},{"name":"quotaSize","description":"The quota size (in bytes) to override the original quota with.\\nIf this is called multiple times, the overridden quota will be equal to\\nthe quotaSize provided in the final call. If this is called without\\nspecifying a quotaSize, the quota will be reset to the default value for\\nthe specified origin. If this is called multiple times with different\\norigins, the override will be maintained for each origin until it is\\ndisabled (called without a quotaSize).","optional":true,"type":"number"}]},{"name":"trackCacheStorageForOrigin","description":"Registers origin to be notified when an update occurs to its cache storage list.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackCacheStorageForStorageKey","description":"Registers storage key to be notified when an update occurs to its cache storage list.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"trackIndexedDBForOrigin","description":"Registers origin to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"trackIndexedDBForStorageKey","description":"Registers storage key to be notified when an update occurs to its IndexedDB.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"untrackCacheStorageForOrigin","description":"Unregisters origin from receiving notifications for cache storage.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackCacheStorageForStorageKey","description":"Unregisters storage key from receiving notifications for cache storage.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"untrackIndexedDBForOrigin","description":"Unregisters origin from receiving notifications for IndexedDB.","parameters":[{"name":"origin","description":"Security origin.","type":"string"}]},{"name":"untrackIndexedDBForStorageKey","description":"Unregisters storage key from receiving notifications for IndexedDB.","parameters":[{"name":"storageKey","description":"Storage key.","type":"string"}]},{"name":"getTrustTokens","description":"Returns the number of stored Trust Tokens per issuer for the\\ncurrent browsing context.","experimental":true,"returns":[{"name":"tokens","type":"array","items":{"$ref":"TrustTokens"}}]},{"name":"clearTrustTokens","description":"Removes all Trust Tokens issued by the provided issuerOrigin.\\nLeaves other stored data, including the issuer\'s Redemption Records, intact.","experimental":true,"parameters":[{"name":"issuerOrigin","type":"string"}],"returns":[{"name":"didDeleteTokens","description":"True if any tokens were deleted, false otherwise.","type":"boolean"}]},{"name":"getInterestGroupDetails","description":"Gets details for a named interest group.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"}],"returns":[{"name":"details","$ref":"InterestGroupDetails"}]},{"name":"setInterestGroupTracking","description":"Enables/Disables issuing of interestGroupAccessed events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]},{"name":"getSharedStorageMetadata","description":"Gets metadata for an origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}],"returns":[{"name":"metadata","$ref":"SharedStorageMetadata"}]},{"name":"getSharedStorageEntries","description":"Gets the entries in an given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}],"returns":[{"name":"entries","type":"array","items":{"$ref":"SharedStorageEntry"}}]},{"name":"setSharedStorageEntry","description":"Sets entry with `key` and `value` for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"key","type":"string"},{"name":"value","type":"string"},{"name":"ignoreIfPresent","description":"If `ignoreIfPresent` is included and true, then only sets the entry if\\n`key` doesn\'t already exist.","optional":true,"type":"boolean"}]},{"name":"deleteSharedStorageEntry","description":"Deletes entry for `key` (if it exists) for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"},{"name":"key","type":"string"}]},{"name":"clearSharedStorageEntries","description":"Clears all entries for a given origin\'s shared storage.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}]},{"name":"resetSharedStorageBudget","description":"Resets the budget for `ownerOrigin` by clearing all budget withdrawals.","experimental":true,"parameters":[{"name":"ownerOrigin","type":"string"}]},{"name":"setSharedStorageTracking","description":"Enables/disables issuing of sharedStorageAccessed events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]},{"name":"setStorageBucketTracking","description":"Set tracking for a storage key\'s buckets.","experimental":true,"parameters":[{"name":"storageKey","type":"string"},{"name":"enable","type":"boolean"}]},{"name":"deleteStorageBucket","description":"Deletes the Storage Bucket with the given storage key and bucket name.","experimental":true,"parameters":[{"name":"bucket","$ref":"StorageBucket"}]},{"name":"runBounceTrackingMitigations","description":"Deletes state for sites identified as potential bounce trackers, immediately.","experimental":true,"returns":[{"name":"deletedSites","type":"array","items":{"type":"string"}}]},{"name":"setAttributionReportingLocalTestingMode","description":"https://wicg.github.io/attribution-reporting-api/","experimental":true,"parameters":[{"name":"enabled","description":"If enabled, noise is suppressed and reports are sent immediately.","type":"boolean"}]},{"name":"setAttributionReportingTracking","description":"Enables/disables issuing of Attribution Reporting events.","experimental":true,"parameters":[{"name":"enable","type":"boolean"}]}],"events":[{"name":"cacheStorageContentUpdated","description":"A cache\'s contents have been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"},{"name":"cacheName","description":"Name of cache in origin.","type":"string"}]},{"name":"cacheStorageListUpdated","description":"A cache has been added/deleted.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"}]},{"name":"indexedDBContentUpdated","description":"The origin\'s IndexedDB object store has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"},{"name":"databaseName","description":"Database to update.","type":"string"},{"name":"objectStoreName","description":"ObjectStore to update.","type":"string"}]},{"name":"indexedDBListUpdated","description":"The origin\'s IndexedDB database list has been modified.","parameters":[{"name":"origin","description":"Origin to update.","type":"string"},{"name":"storageKey","description":"Storage key to update.","type":"string"},{"name":"bucketId","description":"Storage bucket to update.","type":"string"}]},{"name":"interestGroupAccessed","description":"One of the interest groups was accessed by the associated page.","parameters":[{"name":"accessTime","$ref":"Network.TimeSinceEpoch"},{"name":"type","$ref":"InterestGroupAccessType"},{"name":"ownerOrigin","type":"string"},{"name":"name","type":"string"}]},{"name":"sharedStorageAccessed","description":"Shared storage was accessed by the associated page.\\nThe following parameters are included in all events.","parameters":[{"name":"accessTime","description":"Time of the access.","$ref":"Network.TimeSinceEpoch"},{"name":"type","description":"Enum value indicating the Shared Storage API method invoked.","$ref":"SharedStorageAccessType"},{"name":"mainFrameId","description":"DevTools Frame Token for the primary frame tree\'s root.","$ref":"Page.FrameId"},{"name":"ownerOrigin","description":"Serialized origin for the context that invoked the Shared Storage API.","type":"string"},{"name":"params","description":"The sub-parameters warapped by `params` are all optional and their\\npresence/absence depends on `type`.","$ref":"SharedStorageAccessParams"}]},{"name":"storageBucketCreatedOrUpdated","parameters":[{"name":"bucketInfo","$ref":"StorageBucketInfo"}]},{"name":"storageBucketDeleted","parameters":[{"name":"bucketId","type":"string"}]},{"name":"attributionReportingSourceRegistered","description":"TODO(crbug.com/1458532): Add other Attribution Reporting events, e.g.\\ntrigger registration.","experimental":true,"parameters":[{"name":"registration","$ref":"AttributionReportingSourceRegistration"},{"name":"result","$ref":"AttributionReportingSourceRegistrationResult"}]}]},{"domain":"SystemInfo","description":"The SystemInfo domain defines methods and events for querying low-level system information.","experimental":true,"types":[{"id":"GPUDevice","description":"Describes a single graphics processor (GPU).","type":"object","properties":[{"name":"vendorId","description":"PCI ID of the GPU vendor, if available; 0 otherwise.","type":"number"},{"name":"deviceId","description":"PCI ID of the GPU device, if available; 0 otherwise.","type":"number"},{"name":"subSysId","description":"Sub sys ID of the GPU, only available on Windows.","optional":true,"type":"number"},{"name":"revision","description":"Revision of the GPU, only available on Windows.","optional":true,"type":"number"},{"name":"vendorString","description":"String description of the GPU vendor, if the PCI ID is not available.","type":"string"},{"name":"deviceString","description":"String description of the GPU device, if the PCI ID is not available.","type":"string"},{"name":"driverVendor","description":"String description of the GPU driver vendor.","type":"string"},{"name":"driverVersion","description":"String description of the GPU driver version.","type":"string"}]},{"id":"Size","description":"Describes the width and height dimensions of an entity.","type":"object","properties":[{"name":"width","description":"Width in pixels.","type":"integer"},{"name":"height","description":"Height in pixels.","type":"integer"}]},{"id":"VideoDecodeAcceleratorCapability","description":"Describes a supported video decoding profile with its associated minimum and\\nmaximum resolutions.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g. VP9 Profile 2.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"minResolution","description":"Minimum video dimensions in pixels supported for this |profile|.","$ref":"Size"}]},{"id":"VideoEncodeAcceleratorCapability","description":"Describes a supported video encoding profile with its associated maximum\\nresolution and maximum framerate.","type":"object","properties":[{"name":"profile","description":"Video codec profile that is supported, e.g H264 Main.","type":"string"},{"name":"maxResolution","description":"Maximum video dimensions in pixels supported for this |profile|.","$ref":"Size"},{"name":"maxFramerateNumerator","description":"Maximum encoding framerate in frames per second supported for this\\n|profile|, as fraction\'s numerator and denominator, e.g. 24/1 fps,\\n24000/1001 fps, etc.","type":"integer"},{"name":"maxFramerateDenominator","type":"integer"}]},{"id":"SubsamplingFormat","description":"YUV subsampling type of the pixels of a given image.","type":"string","enum":["yuv420","yuv422","yuv444"]},{"id":"ImageType","description":"Image format of a given image.","type":"string","enum":["jpeg","webp","unknown"]},{"id":"ImageDecodeAcceleratorCapability","description":"Describes a supported image decoding profile with its associated minimum and\\nmaximum resolutions and subsampling.","type":"object","properties":[{"name":"imageType","description":"Image coded, e.g. Jpeg.","$ref":"ImageType"},{"name":"maxDimensions","description":"Maximum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"minDimensions","description":"Minimum supported dimensions of the image in pixels.","$ref":"Size"},{"name":"subsamplings","description":"Optional array of supported subsampling formats, e.g. 4:2:0, if known.","type":"array","items":{"$ref":"SubsamplingFormat"}}]},{"id":"GPUInfo","description":"Provides information about the GPU(s) on the system.","type":"object","properties":[{"name":"devices","description":"The graphics devices on the system. Element 0 is the primary GPU.","type":"array","items":{"$ref":"GPUDevice"}},{"name":"auxAttributes","description":"An optional dictionary of additional GPU related attributes.","optional":true,"type":"object"},{"name":"featureStatus","description":"An optional dictionary of graphics features and their status.","optional":true,"type":"object"},{"name":"driverBugWorkarounds","description":"An optional array of GPU driver bug workarounds.","type":"array","items":{"type":"string"}},{"name":"videoDecoding","description":"Supported accelerated video decoding capabilities.","type":"array","items":{"$ref":"VideoDecodeAcceleratorCapability"}},{"name":"videoEncoding","description":"Supported accelerated video encoding capabilities.","type":"array","items":{"$ref":"VideoEncodeAcceleratorCapability"}},{"name":"imageDecoding","description":"Supported accelerated image decoding capabilities.","type":"array","items":{"$ref":"ImageDecodeAcceleratorCapability"}}]},{"id":"ProcessInfo","description":"Represents process info.","type":"object","properties":[{"name":"type","description":"Specifies process type.","type":"string"},{"name":"id","description":"Specifies process id.","type":"integer"},{"name":"cpuTime","description":"Specifies cumulative CPU usage in seconds across all threads of the\\nprocess since the process start.","type":"number"}]}],"commands":[{"name":"getInfo","description":"Returns information about the system.","returns":[{"name":"gpu","description":"Information about the GPUs on the system.","$ref":"GPUInfo"},{"name":"modelName","description":"A platform-dependent description of the model of the machine. On Mac OS, this is, for\\nexample, \'MacBookPro\'. Will be the empty string if not supported.","type":"string"},{"name":"modelVersion","description":"A platform-dependent description of the version of the machine. On Mac OS, this is, for\\nexample, \'10.1\'. Will be the empty string if not supported.","type":"string"},{"name":"commandLine","description":"The command line string used to launch the browser. Will be the empty string if not\\nsupported.","type":"string"}]},{"name":"getFeatureState","description":"Returns information about the feature state.","parameters":[{"name":"featureState","type":"string"}],"returns":[{"name":"featureEnabled","type":"boolean"}]},{"name":"getProcessInfo","description":"Returns information about all running processes.","returns":[{"name":"processInfo","description":"An array of process info blocks.","type":"array","items":{"$ref":"ProcessInfo"}}]}]},{"domain":"Target","description":"Supports additional targets discovery and allows to attach to them.","types":[{"id":"TargetID","type":"string"},{"id":"SessionID","description":"Unique identifier of attached debugging session.","type":"string"},{"id":"TargetInfo","type":"object","properties":[{"name":"targetId","$ref":"TargetID"},{"name":"type","type":"string"},{"name":"title","type":"string"},{"name":"url","type":"string"},{"name":"attached","description":"Whether the target has an attached client.","type":"boolean"},{"name":"openerId","description":"Opener target Id","optional":true,"$ref":"TargetID"},{"name":"canAccessOpener","description":"Whether the target has access to the originating window.","experimental":true,"type":"boolean"},{"name":"openerFrameId","description":"Frame id of originating window (is only set if target has an opener).","experimental":true,"optional":true,"$ref":"Page.FrameId"},{"name":"browserContextId","experimental":true,"optional":true,"$ref":"Browser.BrowserContextID"},{"name":"subtype","description":"Provides additional details for specific target types. For example, for\\nthe type of \\"page\\", this may be set to \\"portal\\" or \\"prerender\\".","experimental":true,"optional":true,"type":"string"}]},{"id":"FilterEntry","description":"A filter used by target query/discovery/auto-attach operations.","experimental":true,"type":"object","properties":[{"name":"exclude","description":"If set, causes exclusion of mathcing targets from the list.","optional":true,"type":"boolean"},{"name":"type","description":"If not present, matches any type.","optional":true,"type":"string"}]},{"id":"TargetFilter","description":"The entries in TargetFilter are matched sequentially against targets and\\nthe first entry that matches determines if the target is included or not,\\ndepending on the value of `exclude` field in the entry.\\nIf filter is not specified, the one assumed is\\n[{type: \\"browser\\", exclude: true}, {type: \\"tab\\", exclude: true}, {}]\\n(i.e. include everything but `browser` and `tab`).","experimental":true,"type":"array","items":{"$ref":"FilterEntry"}},{"id":"RemoteLocation","experimental":true,"type":"object","properties":[{"name":"host","type":"string"},{"name":"port","type":"integer"}]}],"commands":[{"name":"activateTarget","description":"Activates (focuses) the target.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"attachToTarget","description":"Attaches to the target with given id.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.","optional":true,"type":"boolean"}],"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"attachToBrowserTarget","description":"Attaches to the browser target, only uses flat sessionId mode.","experimental":true,"returns":[{"name":"sessionId","description":"Id assigned to the session.","$ref":"SessionID"}]},{"name":"closeTarget","description":"Closes the target. If the target is a page that gets closed too.","parameters":[{"name":"targetId","$ref":"TargetID"}],"returns":[{"name":"success","description":"Always set to true. If an error occurs, the response indicates protocol error.","deprecated":true,"type":"boolean"}]},{"name":"exposeDevToolsProtocol","description":"Inject object to the target\'s main frame that provides a communication\\nchannel with browser target.\\n\\nInjected object will be available as `window[bindingName]`.\\n\\nThe object has the follwing API:\\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"bindingName","description":"Binding name, \'cdp\' if not specified.","optional":true,"type":"string"}]},{"name":"createBrowserContext","description":"Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\\none.","experimental":true,"parameters":[{"name":"disposeOnDetach","description":"If specified, disposes this context when debugging session disconnects.","optional":true,"type":"boolean"},{"name":"proxyServer","description":"Proxy server, similar to the one passed to --proxy-server","optional":true,"type":"string"},{"name":"proxyBypassList","description":"Proxy bypass list, similar to the one passed to --proxy-bypass-list","optional":true,"type":"string"},{"name":"originsWithUniversalNetworkAccess","description":"An optional list of origins to grant unlimited cross-origin access to.\\nParts of the URL other than those constituting origin are ignored.","optional":true,"type":"array","items":{"type":"string"}}],"returns":[{"name":"browserContextId","description":"The id of the context created.","$ref":"Browser.BrowserContextID"}]},{"name":"getBrowserContexts","description":"Returns all browser contexts created with `Target.createBrowserContext` method.","experimental":true,"returns":[{"name":"browserContextIds","description":"An array of browser context ids.","type":"array","items":{"$ref":"Browser.BrowserContextID"}}]},{"name":"createTarget","description":"Creates a new page.","parameters":[{"name":"url","description":"The initial URL the page will be navigated to. An empty string indicates about:blank.","type":"string"},{"name":"width","description":"Frame width in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"height","description":"Frame height in DIP (headless chrome only).","optional":true,"type":"integer"},{"name":"browserContextId","description":"The browser context to create the page in.","experimental":true,"optional":true,"$ref":"Browser.BrowserContextID"},{"name":"enableBeginFrameControl","description":"Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\\nnot supported on MacOS yet, false by default).","experimental":true,"optional":true,"type":"boolean"},{"name":"newWindow","description":"Whether to create a new Window or Tab (chrome-only, false by default).","optional":true,"type":"boolean"},{"name":"background","description":"Whether to create the target in background or foreground (chrome-only,\\nfalse by default).","optional":true,"type":"boolean"},{"name":"forTab","description":"Whether to create the target of type \\"tab\\".","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"targetId","description":"The id of the page opened.","$ref":"TargetID"}]},{"name":"detachFromTarget","description":"Detaches session with given id.","parameters":[{"name":"sessionId","description":"Session to detach.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"disposeBrowserContext","description":"Deletes a BrowserContext. All the belonging pages will be closed without calling their\\nbeforeunload hooks.","experimental":true,"parameters":[{"name":"browserContextId","$ref":"Browser.BrowserContextID"}]},{"name":"getTargetInfo","description":"Returns information about a target.","experimental":true,"parameters":[{"name":"targetId","optional":true,"$ref":"TargetID"}],"returns":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"getTargets","description":"Retrieves a list of available targets.","parameters":[{"name":"filter","description":"Only targets matching filter will be reported. If filter is not specified\\nand target discovery is currently enabled, a filter used for target discovery\\nis used for consistency.","experimental":true,"optional":true,"$ref":"TargetFilter"}],"returns":[{"name":"targetInfos","description":"The list of targets.","type":"array","items":{"$ref":"TargetInfo"}}]},{"name":"sendMessageToTarget","description":"Sends protocol message over session with given id.\\nConsider using flat mode instead; see commands attachToTarget, setAutoAttach,\\nand crbug.com/991325.","deprecated":true,"parameters":[{"name":"message","type":"string"},{"name":"sessionId","description":"Identifier of the session.","optional":true,"$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"setAutoAttach","description":"Controls whether to automatically attach to new targets which are considered to be related to\\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\\nautomatically detaches from all currently attached targets.\\nThis also clears all targets added by `autoAttachRelated` from the list of targets to watch\\nfor creation of related targets.","experimental":true,"parameters":[{"name":"autoAttach","description":"Whether to auto-attach to related targets.","type":"boolean"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"flatten","description":"Enables \\"flat\\" access to the session via specifying sessionId attribute in the commands.\\nWe plan to make this the default, deprecate non-flattened mode,\\nand eventually retire it. See crbug.com/991325.","optional":true,"type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"autoAttachRelated","description":"Adds the specified target to the list of targets that will be monitored for any related target\\ncreation (such as child frames, child workers and new versions of service worker) and reported\\nthrough `attachedToTarget`. The specified target is also auto-attached.\\nThis cancels the effect of any previous `setAutoAttach` and is also cancelled by subsequent\\n`setAutoAttach`. Only available at the Browser target.","experimental":true,"parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"waitForDebuggerOnStart","description":"Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\\nto run paused targets.","type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"setDiscoverTargets","description":"Controls whether to discover available targets and notify via\\n`targetCreated/targetInfoChanged/targetDestroyed` events.","parameters":[{"name":"discover","description":"Whether to discover available targets.","type":"boolean"},{"name":"filter","description":"Only targets matching filter will be attached. If `discover` is false,\\n`filter` must be omitted or empty.","experimental":true,"optional":true,"$ref":"TargetFilter"}]},{"name":"setRemoteLocations","description":"Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\\n`true`.","experimental":true,"parameters":[{"name":"locations","description":"List of remote locations.","type":"array","items":{"$ref":"RemoteLocation"}}]}],"events":[{"name":"attachedToTarget","description":"Issued when attached to target because of auto-attach or `attachToTarget` command.","experimental":true,"parameters":[{"name":"sessionId","description":"Identifier assigned to the session used to send/receive messages.","$ref":"SessionID"},{"name":"targetInfo","$ref":"TargetInfo"},{"name":"waitingForDebugger","type":"boolean"}]},{"name":"detachedFromTarget","description":"Issued when detached from target for any reason (including `detachFromTarget` command). Can be\\nissued multiple times per target if multiple sessions have been attached to it.","experimental":true,"parameters":[{"name":"sessionId","description":"Detached session identifier.","$ref":"SessionID"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"receivedMessageFromTarget","description":"Notifies about a new protocol message received from the session (as reported in\\n`attachedToTarget` event).","parameters":[{"name":"sessionId","description":"Identifier of a session which sends a message.","$ref":"SessionID"},{"name":"message","type":"string"},{"name":"targetId","description":"Deprecated.","deprecated":true,"optional":true,"$ref":"TargetID"}]},{"name":"targetCreated","description":"Issued when a possible inspection target is created.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]},{"name":"targetDestroyed","description":"Issued when a target is destroyed.","parameters":[{"name":"targetId","$ref":"TargetID"}]},{"name":"targetCrashed","description":"Issued when a target has crashed.","parameters":[{"name":"targetId","$ref":"TargetID"},{"name":"status","description":"Termination status type.","type":"string"},{"name":"errorCode","description":"Termination error code.","type":"integer"}]},{"name":"targetInfoChanged","description":"Issued when some information about a target has changed. This only happens between\\n`targetCreated` and `targetDestroyed`.","parameters":[{"name":"targetInfo","$ref":"TargetInfo"}]}]},{"domain":"Tethering","description":"The Tethering domain defines methods and events for browser port binding.","experimental":true,"commands":[{"name":"bind","description":"Request browser port binding.","parameters":[{"name":"port","description":"Port number to bind.","type":"integer"}]},{"name":"unbind","description":"Request browser port unbinding.","parameters":[{"name":"port","description":"Port number to unbind.","type":"integer"}]}],"events":[{"name":"accepted","description":"Informs that port was successfully bound and got a specified connection id.","parameters":[{"name":"port","description":"Port number that was successfully bound.","type":"integer"},{"name":"connectionId","description":"Connection id to be used.","type":"string"}]}]},{"domain":"Tracing","experimental":true,"dependencies":["IO"],"types":[{"id":"MemoryDumpConfig","description":"Configuration for memory dump. Used only when \\"memory-infra\\" category is enabled.","type":"object"},{"id":"TraceConfig","type":"object","properties":[{"name":"recordMode","description":"Controls how the trace buffer stores data.","optional":true,"type":"string","enum":["recordUntilFull","recordContinuously","recordAsMuchAsPossible","echoToConsole"]},{"name":"traceBufferSizeInKb","description":"Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value\\nof 200 MB would be used.","optional":true,"type":"number"},{"name":"enableSampling","description":"Turns on JavaScript stack sampling.","optional":true,"type":"boolean"},{"name":"enableSystrace","description":"Turns on system tracing.","optional":true,"type":"boolean"},{"name":"enableArgumentFilter","description":"Turns on argument filter.","optional":true,"type":"boolean"},{"name":"includedCategories","description":"Included category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"excludedCategories","description":"Excluded category filters.","optional":true,"type":"array","items":{"type":"string"}},{"name":"syntheticDelays","description":"Configuration to synthesize the delays in tracing.","optional":true,"type":"array","items":{"type":"string"}},{"name":"memoryDumpConfig","description":"Configuration for memory dump triggers. Used only when \\"memory-infra\\" category is enabled.","optional":true,"$ref":"MemoryDumpConfig"}]},{"id":"StreamFormat","description":"Data format of a trace. Can be either the legacy JSON format or the\\nprotocol buffer format. Note that the JSON format will be deprecated soon.","type":"string","enum":["json","proto"]},{"id":"StreamCompression","description":"Compression type to use for traces returned via streams.","type":"string","enum":["none","gzip"]},{"id":"MemoryDumpLevelOfDetail","description":"Details exposed when memory request explicitly declared.\\nKeep consistent with memory_dump_request_args.h and\\nmemory_instrumentation.mojom","type":"string","enum":["background","light","detailed"]},{"id":"TracingBackend","description":"Backend type to use for tracing. `chrome` uses the Chrome-integrated\\ntracing service and is supported on all platforms. `system` is only\\nsupported on Chrome OS and uses the Perfetto system tracing service.\\n`auto` chooses `system` when the perfettoConfig provided to Tracing.start\\nspecifies at least one non-Chrome data source; otherwise uses `chrome`.","type":"string","enum":["auto","chrome","system"]}],"commands":[{"name":"end","description":"Stop trace events collection."},{"name":"getCategories","description":"Gets supported tracing categories.","returns":[{"name":"categories","description":"A list of supported tracing categories.","type":"array","items":{"type":"string"}}]},{"name":"recordClockSyncMarker","description":"Record a clock sync marker in the trace.","parameters":[{"name":"syncId","description":"The ID of this clock sync marker","type":"string"}]},{"name":"requestMemoryDump","description":"Request a global memory dump.","parameters":[{"name":"deterministic","description":"Enables more deterministic results by forcing garbage collection","optional":true,"type":"boolean"},{"name":"levelOfDetail","description":"Specifies level of details in memory dump. Defaults to \\"detailed\\".","optional":true,"$ref":"MemoryDumpLevelOfDetail"}],"returns":[{"name":"dumpGuid","description":"GUID of the resulting global memory dump.","type":"string"},{"name":"success","description":"True iff the global memory dump succeeded.","type":"boolean"}]},{"name":"start","description":"Start trace events collection.","parameters":[{"name":"categories","description":"Category/tag filter","deprecated":true,"optional":true,"type":"string"},{"name":"options","description":"Tracing options","deprecated":true,"optional":true,"type":"string"},{"name":"bufferUsageReportingInterval","description":"If set, the agent will issue bufferUsage events at this interval, specified in milliseconds","optional":true,"type":"number"},{"name":"transferMode","description":"Whether to report trace events as series of dataCollected events or to save trace to a\\nstream (defaults to `ReportEvents`).","optional":true,"type":"string","enum":["ReportEvents","ReturnAsStream"]},{"name":"streamFormat","description":"Trace data format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `json`).","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format to use. This only applies when using `ReturnAsStream`\\ntransfer mode (defaults to `none`)","optional":true,"$ref":"StreamCompression"},{"name":"traceConfig","optional":true,"$ref":"TraceConfig"},{"name":"perfettoConfig","description":"Base64-encoded serialized perfetto.protos.TraceConfig protobuf message\\nWhen specified, the parameters `categories`, `options`, `traceConfig`\\nare ignored. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"tracingBackend","description":"Backend type (defaults to `auto`)","optional":true,"$ref":"TracingBackend"}]}],"events":[{"name":"bufferUsage","parameters":[{"name":"percentFull","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"},{"name":"eventCount","description":"An approximate number of events in the trace log.","optional":true,"type":"number"},{"name":"value","description":"A number in range [0..1] that indicates the used size of event buffer as a fraction of its\\ntotal size.","optional":true,"type":"number"}]},{"name":"dataCollected","description":"Contains a bucket of collected trace events. When tracing is stopped collected events will be\\nsent as a sequence of dataCollected events followed by tracingComplete event.","parameters":[{"name":"value","type":"array","items":{"type":"object"}}]},{"name":"tracingComplete","description":"Signals that tracing is stopped and there is no trace buffers pending flush, all data were\\ndelivered via dataCollected events.","parameters":[{"name":"dataLossOccurred","description":"Indicates whether some trace data is known to have been lost, e.g. because the trace ring\\nbuffer wrapped around.","type":"boolean"},{"name":"stream","description":"A handle of the stream that holds resulting trace data.","optional":true,"$ref":"IO.StreamHandle"},{"name":"traceFormat","description":"Trace data format of returned stream.","optional":true,"$ref":"StreamFormat"},{"name":"streamCompression","description":"Compression format of returned stream.","optional":true,"$ref":"StreamCompression"}]}]},{"domain":"Fetch","description":"A domain for letting clients substitute browser\'s network layer with client code.","dependencies":["Network","IO","Page"],"types":[{"id":"RequestId","description":"Unique request identifier.","type":"string"},{"id":"RequestStage","description":"Stages of the request to handle. Request will intercept before the request is\\nsent. Response will intercept after the response is received (but before response\\nbody is received).","type":"string","enum":["Request","Response"]},{"id":"RequestPattern","type":"object","properties":[{"name":"urlPattern","description":"Wildcards (`\'*\'` -> zero or more, `\'?\'` -> exactly one) are allowed. Escape character is\\nbackslash. Omitting is equivalent to `\\"*\\"`.","optional":true,"type":"string"},{"name":"resourceType","description":"If set, only requests for matching resource types will be intercepted.","optional":true,"$ref":"Network.ResourceType"},{"name":"requestStage","description":"Stage at which to begin intercepting requests. Default is Request.","optional":true,"$ref":"RequestStage"}]},{"id":"HeaderEntry","description":"Response HTTP header entry","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"AuthChallenge","description":"Authorization challenge for HTTP status code 401 or 407.","type":"object","properties":[{"name":"source","description":"Source of the authentication challenge.","optional":true,"type":"string","enum":["Server","Proxy"]},{"name":"origin","description":"Origin of the challenger.","type":"string"},{"name":"scheme","description":"The authentication scheme used, such as basic or digest","type":"string"},{"name":"realm","description":"The realm of the challenge. May be empty.","type":"string"}]},{"id":"AuthChallengeResponse","description":"Response to an AuthChallenge.","type":"object","properties":[{"name":"response","description":"The decision on what to do in response to the authorization challenge. Default means\\ndeferring to the default behavior of the net stack, which will likely either the Cancel\\nauthentication or display a popup dialog box.","type":"string","enum":["Default","CancelAuth","ProvideCredentials"]},{"name":"username","description":"The username to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"},{"name":"password","description":"The password to provide, possibly empty. Should only be set if response is\\nProvideCredentials.","optional":true,"type":"string"}]}],"commands":[{"name":"disable","description":"Disables the fetch domain."},{"name":"enable","description":"Enables issuing of requestPaused events. A request will be paused until client\\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.","parameters":[{"name":"patterns","description":"If specified, only requests matching any of these patterns will produce\\nfetchRequested event and will be paused until clients response. If not set,\\nall requests will be affected.","optional":true,"type":"array","items":{"$ref":"RequestPattern"}},{"name":"handleAuthRequests","description":"If true, authRequired events will be issued and requests will be paused\\nexpecting a call to continueWithAuth.","optional":true,"type":"boolean"}]},{"name":"failRequest","description":"Causes the request to fail with specified reason.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"errorReason","description":"Causes the request to fail with the given reason.","$ref":"Network.ErrorReason"}]},{"name":"fulfillRequest","description":"Provides response to the request.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code.","type":"integer"},{"name":"responseHeaders","description":"Response headers.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"binaryResponseHeaders","description":"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can\'t be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"body","description":"A response body. If absent, original response body will be used if\\nthe request is intercepted at the response stage and empty body\\nwill be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.","optional":true,"type":"string"}]},{"name":"continueRequest","description":"Continues the request, optionally modifying some of its parameters.","parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"url","description":"If set, the request url will be modified in a way that\'s not observable by page.","optional":true,"type":"string"},{"name":"method","description":"If set, the request method is overridden.","optional":true,"type":"string"},{"name":"postData","description":"If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"headers","description":"If set, overrides the request headers. Note that the overrides do not\\nextend to subsequent redirect hops, if a redirect happens. Another override\\nmay be applied to a different request produced by a redirect.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"interceptResponse","description":"If set, overrides response interception behavior for this request.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"continueWithAuth","description":"Continues a request supplying authChallengeResponse following authRequired event.","parameters":[{"name":"requestId","description":"An id the client received in authRequired event.","$ref":"RequestId"},{"name":"authChallengeResponse","description":"Response to with an authChallenge.","$ref":"AuthChallengeResponse"}]},{"name":"continueResponse","description":"Continues loading of the paused response, optionally modifying the\\nresponse headers. If either responseCode or headers are modified, all of them\\nmust be present.","experimental":true,"parameters":[{"name":"requestId","description":"An id the client received in requestPaused event.","$ref":"RequestId"},{"name":"responseCode","description":"An HTTP response code. If absent, original response code will be used.","optional":true,"type":"integer"},{"name":"responsePhrase","description":"A textual representation of responseCode.\\nIf absent, a standard phrase matching responseCode is used.","optional":true,"type":"string"},{"name":"responseHeaders","description":"Response headers. If absent, original response headers will be used.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"binaryResponseHeaders","description":"Alternative way of specifying response headers as a \\\\0-separated\\nseries of name: value pairs. Prefer the above method unless you\\nneed to represent some non-UTF8 values that can\'t be transmitted\\nover the protocol as text. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"getResponseBody","description":"Causes the body of the response to be received from the server and\\nreturned as a single string. May only be issued for a request that\\nis paused in the Response stage and is mutually exclusive with\\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\\naffect the request or disabling fetch domain before body is received\\nresults in an undefined behavior.\\nNote that the response body is not available for redirects. Requests\\npaused in the _redirect received_ state may be differentiated by\\n`responseCode` and presence of `location` response header, see\\ncomments to `requestPaused` for details.","parameters":[{"name":"requestId","description":"Identifier for the intercepted request to get body for.","$ref":"RequestId"}],"returns":[{"name":"body","description":"Response body.","type":"string"},{"name":"base64Encoded","description":"True, if content was sent as base64.","type":"boolean"}]},{"name":"takeResponseBodyAsStream","description":"Returns a handle to the stream representing the response body.\\nThe request must be paused in the HeadersReceived stage.\\nNote that after this command the request can\'t be continued\\nas is -- client either needs to cancel it or to provide the\\nresponse body.\\nThe stream only supports sequential read, IO.read will fail if the position\\nis specified.\\nThis method is mutually exclusive with getResponseBody.\\nCalling other methods that affect the request or disabling fetch\\ndomain before body is received results in an undefined behavior.","parameters":[{"name":"requestId","$ref":"RequestId"}],"returns":[{"name":"stream","$ref":"IO.StreamHandle"}]}],"events":[{"name":"requestPaused","description":"Issued when the domain is enabled and the request URL matches the\\nspecified filter. The request is paused until the client responds\\nwith one of continueRequest, failRequest or fulfillRequest.\\nThe stage of the request can be determined by presence of responseErrorReason\\nand responseStatusCode -- the request is at the response stage if either\\nof these fields is present and in the request stage otherwise.\\nRedirect responses and subsequent requests are reported similarly to regular\\nresponses and requests. Redirect responses may be distinguished by the value\\nof `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with\\npresence of the `location` header. Requests resulting from a redirect will\\nhave `redirectedRequestId` field set.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"responseErrorReason","description":"Response error if intercepted at response stage.","optional":true,"$ref":"Network.ErrorReason"},{"name":"responseStatusCode","description":"Response code if intercepted at response stage.","optional":true,"type":"integer"},{"name":"responseStatusText","description":"Response status text if intercepted at response stage.","optional":true,"type":"string"},{"name":"responseHeaders","description":"Response headers if intercepted at the response stage.","optional":true,"type":"array","items":{"$ref":"HeaderEntry"}},{"name":"networkId","description":"If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.","optional":true,"$ref":"Network.RequestId"},{"name":"redirectedRequestId","description":"If the request is due to a redirect response from the server, the id of the request that\\nhas caused the redirect.","experimental":true,"optional":true,"$ref":"RequestId"}]},{"name":"authRequired","description":"Issued when the domain is enabled with handleAuthRequests set to true.\\nThe request is paused until client responds with continueWithAuth.","parameters":[{"name":"requestId","description":"Each request the page makes will have a unique id.","$ref":"RequestId"},{"name":"request","description":"The details of the request.","$ref":"Network.Request"},{"name":"frameId","description":"The id of the frame that initiated the request.","$ref":"Page.FrameId"},{"name":"resourceType","description":"How the requested resource will be used.","$ref":"Network.ResourceType"},{"name":"authChallenge","description":"Details of the Authorization Challenge encountered.\\nIf this is set, client should respond with continueRequest that\\ncontains AuthChallengeResponse.","$ref":"AuthChallenge"}]}]},{"domain":"WebAudio","description":"This domain allows inspection of Web Audio API.\\nhttps://webaudio.github.io/web-audio-api/","experimental":true,"types":[{"id":"GraphObjectId","description":"An unique ID for a graph object (AudioContext, AudioNode, AudioParam) in Web Audio API","type":"string"},{"id":"ContextType","description":"Enum of BaseAudioContext types","type":"string","enum":["realtime","offline"]},{"id":"ContextState","description":"Enum of AudioContextState from the spec","type":"string","enum":["suspended","running","closed"]},{"id":"NodeType","description":"Enum of AudioNode types","type":"string"},{"id":"ChannelCountMode","description":"Enum of AudioNode::ChannelCountMode from the spec","type":"string","enum":["clamped-max","explicit","max"]},{"id":"ChannelInterpretation","description":"Enum of AudioNode::ChannelInterpretation from the spec","type":"string","enum":["discrete","speakers"]},{"id":"ParamType","description":"Enum of AudioParam types","type":"string"},{"id":"AutomationRate","description":"Enum of AudioParam::AutomationRate from the spec","type":"string","enum":["a-rate","k-rate"]},{"id":"ContextRealtimeData","description":"Fields in AudioContext that change in real-time.","type":"object","properties":[{"name":"currentTime","description":"The current context time in second in BaseAudioContext.","type":"number"},{"name":"renderCapacity","description":"The time spent on rendering graph divided by render quantum duration,\\nand multiplied by 100. 100 means the audio renderer reached the full\\ncapacity and glitch may occur.","type":"number"},{"name":"callbackIntervalMean","description":"A running mean of callback interval.","type":"number"},{"name":"callbackIntervalVariance","description":"A running variance of callback interval.","type":"number"}]},{"id":"BaseAudioContext","description":"Protocol object for BaseAudioContext","type":"object","properties":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"contextType","$ref":"ContextType"},{"name":"contextState","$ref":"ContextState"},{"name":"realtimeData","optional":true,"$ref":"ContextRealtimeData"},{"name":"callbackBufferSize","description":"Platform-dependent callback buffer size.","type":"number"},{"name":"maxOutputChannelCount","description":"Number of output channels supported by audio hardware in use.","type":"number"},{"name":"sampleRate","description":"Context sample rate.","type":"number"}]},{"id":"AudioListener","description":"Protocol object for AudioListener","type":"object","properties":[{"name":"listenerId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"}]},{"id":"AudioNode","description":"Protocol object for AudioNode","type":"object","properties":[{"name":"nodeId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeType","$ref":"NodeType"},{"name":"numberOfInputs","type":"number"},{"name":"numberOfOutputs","type":"number"},{"name":"channelCount","type":"number"},{"name":"channelCountMode","$ref":"ChannelCountMode"},{"name":"channelInterpretation","$ref":"ChannelInterpretation"}]},{"id":"AudioParam","description":"Protocol object for AudioParam","type":"object","properties":[{"name":"paramId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"},{"name":"contextId","$ref":"GraphObjectId"},{"name":"paramType","$ref":"ParamType"},{"name":"rate","$ref":"AutomationRate"},{"name":"defaultValue","type":"number"},{"name":"minValue","type":"number"},{"name":"maxValue","type":"number"}]}],"commands":[{"name":"enable","description":"Enables the WebAudio domain and starts sending context lifetime events."},{"name":"disable","description":"Disables the WebAudio domain."},{"name":"getRealtimeData","description":"Fetch the realtime data from the registered contexts.","parameters":[{"name":"contextId","$ref":"GraphObjectId"}],"returns":[{"name":"realtimeData","$ref":"ContextRealtimeData"}]}],"events":[{"name":"contextCreated","description":"Notifies that a new BaseAudioContext has been created.","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"contextWillBeDestroyed","description":"Notifies that an existing BaseAudioContext will be destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"}]},{"name":"contextChanged","description":"Notifies that existing BaseAudioContext has changed some properties (id stays the same)..","parameters":[{"name":"context","$ref":"BaseAudioContext"}]},{"name":"audioListenerCreated","description":"Notifies that the construction of an AudioListener has finished.","parameters":[{"name":"listener","$ref":"AudioListener"}]},{"name":"audioListenerWillBeDestroyed","description":"Notifies that a new AudioListener has been created.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"listenerId","$ref":"GraphObjectId"}]},{"name":"audioNodeCreated","description":"Notifies that a new AudioNode has been created.","parameters":[{"name":"node","$ref":"AudioNode"}]},{"name":"audioNodeWillBeDestroyed","description":"Notifies that an existing AudioNode has been destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"}]},{"name":"audioParamCreated","description":"Notifies that a new AudioParam has been created.","parameters":[{"name":"param","$ref":"AudioParam"}]},{"name":"audioParamWillBeDestroyed","description":"Notifies that an existing AudioParam has been destroyed.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"nodeId","$ref":"GraphObjectId"},{"name":"paramId","$ref":"GraphObjectId"}]},{"name":"nodesConnected","description":"Notifies that two AudioNodes are connected.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"},{"name":"destinationInputIndex","optional":true,"type":"number"}]},{"name":"nodesDisconnected","description":"Notifies that AudioNodes are disconnected. The destination can be null, and it means all the outgoing connections from the source are disconnected.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"},{"name":"destinationInputIndex","optional":true,"type":"number"}]},{"name":"nodeParamConnected","description":"Notifies that an AudioNode is connected to an AudioParam.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"}]},{"name":"nodeParamDisconnected","description":"Notifies that an AudioNode is disconnected to an AudioParam.","parameters":[{"name":"contextId","$ref":"GraphObjectId"},{"name":"sourceId","$ref":"GraphObjectId"},{"name":"destinationId","$ref":"GraphObjectId"},{"name":"sourceOutputIndex","optional":true,"type":"number"}]}]},{"domain":"WebAuthn","description":"This domain allows configuring virtual authenticators to test the WebAuthn\\nAPI.","experimental":true,"types":[{"id":"AuthenticatorId","type":"string"},{"id":"AuthenticatorProtocol","type":"string","enum":["u2f","ctap2"]},{"id":"Ctap2Version","type":"string","enum":["ctap2_0","ctap2_1"]},{"id":"AuthenticatorTransport","type":"string","enum":["usb","nfc","ble","cable","internal"]},{"id":"VirtualAuthenticatorOptions","type":"object","properties":[{"name":"protocol","$ref":"AuthenticatorProtocol"},{"name":"ctap2Version","description":"Defaults to ctap2_0. Ignored if |protocol| == u2f.","optional":true,"$ref":"Ctap2Version"},{"name":"transport","$ref":"AuthenticatorTransport"},{"name":"hasResidentKey","description":"Defaults to false.","optional":true,"type":"boolean"},{"name":"hasUserVerification","description":"Defaults to false.","optional":true,"type":"boolean"},{"name":"hasLargeBlob","description":"If set to true, the authenticator will support the largeBlob extension.\\nhttps://w3c.github.io/webauthn#largeBlob\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasCredBlob","description":"If set to true, the authenticator will support the credBlob extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasMinPinLength","description":"If set to true, the authenticator will support the minPinLength extension.\\nhttps://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-minpinlength-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"hasPrf","description":"If set to true, the authenticator will support the prf extension.\\nhttps://w3c.github.io/webauthn/#prf-extension\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"automaticPresenceSimulation","description":"If set to true, tests of user presence will succeed immediately.\\nOtherwise, they will not be resolved. Defaults to true.","optional":true,"type":"boolean"},{"name":"isUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nDefaults to false.","optional":true,"type":"boolean"}]},{"id":"Credential","type":"object","properties":[{"name":"credentialId","type":"string"},{"name":"isResidentCredential","type":"boolean"},{"name":"rpId","description":"Relying Party ID the credential is scoped to. Must be set when adding a\\ncredential.","optional":true,"type":"string"},{"name":"privateKey","description":"The ECDSA P-256 private key in PKCS#8 format. (Encoded as a base64 string when passed over JSON)","type":"string"},{"name":"userHandle","description":"An opaque byte sequence with a maximum size of 64 bytes mapping the\\ncredential to a specific user. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"},{"name":"signCount","description":"Signature counter. This is incremented by one for each successful\\nassertion.\\nSee https://w3c.github.io/webauthn/#signature-counter","type":"integer"},{"name":"largeBlob","description":"The large blob associated with the credential.\\nSee https://w3c.github.io/webauthn/#sctn-large-blob-extension (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]}],"commands":[{"name":"enable","description":"Enable the WebAuthn domain and start intercepting credential storage and\\nretrieval with a virtual authenticator.","parameters":[{"name":"enableUI","description":"Whether to enable the WebAuthn user interface. Enabling the UI is\\nrecommended for debugging and demo purposes, as it is closer to the real\\nexperience. Disabling the UI is recommended for automated testing.\\nSupported at the embedder\'s discretion if UI is available.\\nDefaults to false.","optional":true,"type":"boolean"}]},{"name":"disable","description":"Disable the WebAuthn domain."},{"name":"addVirtualAuthenticator","description":"Creates and adds a virtual authenticator.","parameters":[{"name":"options","$ref":"VirtualAuthenticatorOptions"}],"returns":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setResponseOverrideBits","description":"Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isBogusSignature","description":"If isBogusSignature is set, overrides the signature in the authenticator response to be zero.\\nDefaults to false.","optional":true,"type":"boolean"},{"name":"isBadUV","description":"If isBadUV is set, overrides the UV bit in the flags in the authenticator response to\\nbe zero. Defaults to false.","optional":true,"type":"boolean"},{"name":"isBadUP","description":"If isBadUP is set, overrides the UP bit in the flags in the authenticator response to\\nbe zero. Defaults to false.","optional":true,"type":"boolean"}]},{"name":"removeVirtualAuthenticator","description":"Removes the given authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"addCredential","description":"Adds the credential to the specified authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"getCredential","description":"Returns a single credential stored in the given virtual authenticator that\\nmatches the credential ID.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credentialId","type":"string"}],"returns":[{"name":"credential","$ref":"Credential"}]},{"name":"getCredentials","description":"Returns all the credentials stored in the given virtual authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}],"returns":[{"name":"credentials","type":"array","items":{"$ref":"Credential"}}]},{"name":"removeCredential","description":"Removes a credential from the authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credentialId","type":"string"}]},{"name":"clearCredentials","description":"Clears all the credentials from the specified device.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"}]},{"name":"setUserVerified","description":"Sets whether User Verification succeeds or fails for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"isUserVerified","type":"boolean"}]},{"name":"setAutomaticPresenceSimulation","description":"Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.\\nThe default is true.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"enabled","type":"boolean"}]}],"events":[{"name":"credentialAdded","description":"Triggered when a credential is added to an authenticator.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]},{"name":"credentialAsserted","description":"Triggered when a credential is used in a webauthn assertion.","parameters":[{"name":"authenticatorId","$ref":"AuthenticatorId"},{"name":"credential","$ref":"Credential"}]}]},{"domain":"Media","description":"This domain allows detailed inspection of media elements","experimental":true,"types":[{"id":"PlayerId","description":"Players will get an ID that is unique within the agent context.","type":"string"},{"id":"Timestamp","type":"number"},{"id":"PlayerMessage","description":"Have one type per entry in MediaLogRecord::Type\\nCorresponds to kMessage","type":"object","properties":[{"name":"level","description":"Keep in sync with MediaLogMessageLevel\\nWe are currently keeping the message level \'error\' separate from the\\nPlayerError type because right now they represent different things,\\nthis one being a DVLOG(ERROR) style log message that gets printed\\nbased on what log level is selected in the UI, and the other is a\\nrepresentation of a media::PipelineStatus object. Soon however we\'re\\ngoing to be moving away from using PipelineStatus for errors and\\nintroducing a new error type which should hopefully let us integrate\\nthe error log level into the PlayerError type.","type":"string","enum":["error","warning","info","debug"]},{"name":"message","type":"string"}]},{"id":"PlayerProperty","description":"Corresponds to kMediaPropertyChange","type":"object","properties":[{"name":"name","type":"string"},{"name":"value","type":"string"}]},{"id":"PlayerEvent","description":"Corresponds to kMediaEventTriggered","type":"object","properties":[{"name":"timestamp","$ref":"Timestamp"},{"name":"value","type":"string"}]},{"id":"PlayerErrorSourceLocation","description":"Represents logged source line numbers reported in an error.\\nNOTE: file and line are from chromium c++ implementation code, not js.","type":"object","properties":[{"name":"file","type":"string"},{"name":"line","type":"integer"}]},{"id":"PlayerError","description":"Corresponds to kMediaError","type":"object","properties":[{"name":"errorType","type":"string"},{"name":"code","description":"Code is the numeric enum entry for a specific set of error codes, such\\nas PipelineStatusCodes in media/base/pipeline_status.h","type":"integer"},{"name":"stack","description":"A trace of where this error was caused / where it passed through.","type":"array","items":{"$ref":"PlayerErrorSourceLocation"}},{"name":"cause","description":"Errors potentially have a root cause error, ie, a DecoderError might be\\ncaused by an WindowsError","type":"array","items":{"$ref":"PlayerError"}},{"name":"data","description":"Extra data attached to an error, such as an HRESULT, Video Codec, etc.","type":"object"}]}],"events":[{"name":"playerPropertiesChanged","description":"This can be called multiple times, and can be used to set / override /\\nremove player properties. A null propValue indicates removal.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"properties","type":"array","items":{"$ref":"PlayerProperty"}}]},{"name":"playerEventsAdded","description":"Send events as a list, allowing them to be batched on the browser for less\\ncongestion. If batched, events must ALWAYS be in chronological order.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"events","type":"array","items":{"$ref":"PlayerEvent"}}]},{"name":"playerMessagesLogged","description":"Send a list of any messages that need to be delivered.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"messages","type":"array","items":{"$ref":"PlayerMessage"}}]},{"name":"playerErrorsRaised","description":"Send a list of any errors that need to be delivered.","parameters":[{"name":"playerId","$ref":"PlayerId"},{"name":"errors","type":"array","items":{"$ref":"PlayerError"}}]},{"name":"playersCreated","description":"Called whenever a player is created, or when a new agent joins and receives\\na list of active players. If an agent is restored, it will receive the full\\nlist of player ids and all events again.","parameters":[{"name":"players","type":"array","items":{"$ref":"PlayerId"}}]}],"commands":[{"name":"enable","description":"Enables the Media domain"},{"name":"disable","description":"Disables the Media domain."}]},{"domain":"DeviceAccess","experimental":true,"types":[{"id":"RequestId","description":"Device request id.","type":"string"},{"id":"DeviceId","description":"A device id.","type":"string"},{"id":"PromptDevice","description":"Device information displayed in a user prompt to select a device.","type":"object","properties":[{"name":"id","$ref":"DeviceId"},{"name":"name","description":"Display name as it appears in a device request user prompt.","type":"string"}]}],"commands":[{"name":"enable","description":"Enable events in this domain."},{"name":"disable","description":"Disable events in this domain."},{"name":"selectPrompt","description":"Select a device in response to a DeviceAccess.deviceRequestPrompted event.","parameters":[{"name":"id","$ref":"RequestId"},{"name":"deviceId","$ref":"DeviceId"}]},{"name":"cancelPrompt","description":"Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.","parameters":[{"name":"id","$ref":"RequestId"}]}],"events":[{"name":"deviceRequestPrompted","description":"A device request opened a user prompt to select a device. Respond with the\\nselectPrompt or cancelPrompt command.","parameters":[{"name":"id","$ref":"RequestId"},{"name":"devices","type":"array","items":{"$ref":"PromptDevice"}}]}]},{"domain":"Preload","experimental":true,"types":[{"id":"RuleSetId","description":"Unique id","type":"string"},{"id":"RuleSet","description":"Corresponds to SpeculationRuleSet","type":"object","properties":[{"name":"id","$ref":"RuleSetId"},{"name":"loaderId","description":"Identifies a document which the rule set is associated with.","$ref":"Network.LoaderId"},{"name":"sourceText","description":"Source text of JSON representing the rule set. If it comes from\\n`<script>` tag, it is the textContent of the node. Note that it is\\na JSON for valid case.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html\\n- https://github.com/WICG/nav-speculation/blob/main/triggers.md","type":"string"},{"name":"backendNodeId","description":"A speculation rule set is either added through an inline\\n`<script>` tag or through an external resource via the\\n\'Speculation-Rules\' HTTP header. For the first case, we include\\nthe BackendNodeId of the relevant `<script>` tag. For the second\\ncase, we include the external URL where the rule set was loaded\\nfrom, and also RequestId if Network domain is enabled.\\n\\nSee also:\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-script\\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-header","optional":true,"$ref":"DOM.BackendNodeId"},{"name":"url","optional":true,"type":"string"},{"name":"requestId","optional":true,"$ref":"Network.RequestId"},{"name":"errorType","description":"Error information\\n`errorMessage` is null iff `errorType` is null.","optional":true,"$ref":"RuleSetErrorType"},{"name":"errorMessage","description":"TODO(https://crbug.com/1425354): Replace this property with structured error.","deprecated":true,"optional":true,"type":"string"}]},{"id":"RuleSetErrorType","type":"string","enum":["SourceIsNotJsonObject","InvalidRulesSkipped"]},{"id":"SpeculationAction","description":"The type of preloading attempted. It corresponds to\\nmojom::SpeculationAction (although PrefetchWithSubresources is omitted as it\\nisn\'t being used by clients).","type":"string","enum":["Prefetch","Prerender"]},{"id":"SpeculationTargetHint","description":"Corresponds to mojom::SpeculationTargetHint.\\nSee https://github.com/WICG/nav-speculation/blob/main/triggers.md#window-name-targeting-hints","type":"string","enum":["Blank","Self"]},{"id":"PreloadingAttemptKey","description":"A key that identifies a preloading attempt.\\n\\nThe url used is the url specified by the trigger (i.e. the initial URL), and\\nnot the final url that is navigated to. For example, prerendering allows\\nsame-origin main frame navigations during the attempt, but the attempt is\\nstill keyed with the initial URL.","type":"object","properties":[{"name":"loaderId","$ref":"Network.LoaderId"},{"name":"action","$ref":"SpeculationAction"},{"name":"url","type":"string"},{"name":"targetHint","optional":true,"$ref":"SpeculationTargetHint"}]},{"id":"PreloadingAttemptSource","description":"Lists sources for a preloading attempt, specifically the ids of rule sets\\nthat had a speculation rule that triggered the attempt, and the\\nBackendNodeIds of <a href> or <area href> elements that triggered the\\nattempt (in the case of attempts triggered by a document rule). It is\\npossible for mulitple rule sets and links to trigger a single attempt.","type":"object","properties":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"ruleSetIds","type":"array","items":{"$ref":"RuleSetId"}},{"name":"nodeIds","type":"array","items":{"$ref":"DOM.BackendNodeId"}}]},{"id":"PrerenderFinalStatus","description":"List of FinalStatus reasons for Prerender2.","type":"string","enum":["Activated","Destroyed","LowEndDevice","InvalidSchemeRedirect","InvalidSchemeNavigation","InProgressNavigation","NavigationRequestBlockedByCsp","MainFrameNavigation","MojoBinderPolicy","RendererProcessCrashed","RendererProcessKilled","Download","TriggerDestroyed","NavigationNotCommitted","NavigationBadHttpStatus","ClientCertRequested","NavigationRequestNetworkError","MaxNumOfRunningPrerendersExceeded","CancelAllHostsForTesting","DidFailLoad","Stop","SslCertificateError","LoginAuthRequested","UaChangeRequiresReload","BlockedByClient","AudioOutputDeviceRequested","MixedContent","TriggerBackgrounded","MemoryLimitExceeded","FailToGetMemoryUsage","DataSaverEnabled","HasEffectiveUrl","ActivatedBeforeStarted","InactivePageRestriction","StartFailed","TimeoutBackgrounded","CrossSiteRedirectInInitialNavigation","CrossSiteNavigationInInitialNavigation","SameSiteCrossOriginRedirectNotOptInInInitialNavigation","SameSiteCrossOriginNavigationNotOptInInInitialNavigation","ActivationNavigationParameterMismatch","ActivatedInBackground","EmbedderHostDisallowed","ActivationNavigationDestroyedBeforeSuccess","TabClosedByUserGesture","TabClosedWithoutUserGesture","PrimaryMainFrameRendererProcessCrashed","PrimaryMainFrameRendererProcessKilled","ActivationFramePolicyNotCompatible","PreloadingDisabled","BatterySaverEnabled","ActivatedDuringMainFrameNavigation","PreloadingUnsupportedByWebContents","CrossSiteRedirectInMainFrameNavigation","CrossSiteNavigationInMainFrameNavigation","SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation","SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation","MemoryPressureOnTrigger","MemoryPressureAfterTriggered","PrerenderingDisabledByDevTools","ResourceLoadBlockedByClient","SpeculationRuleRemoved","ActivatedWithAuxiliaryBrowsingContexts"]},{"id":"PreloadingStatus","description":"Preloading status values, see also PreloadingTriggeringOutcome. This\\nstatus is shared by prefetchStatusUpdated and prerenderStatusUpdated.","type":"string","enum":["Pending","Running","Ready","Success","Failure","NotSupported"]},{"id":"PrefetchStatus","description":"TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and\\nfilter out the ones that aren\'t necessary to the developers.","type":"string","enum":["PrefetchAllowed","PrefetchFailedIneligibleRedirect","PrefetchFailedInvalidRedirect","PrefetchFailedMIMENotSupported","PrefetchFailedNetError","PrefetchFailedNon2XX","PrefetchFailedPerPageLimitExceeded","PrefetchEvicted","PrefetchHeldback","PrefetchIneligibleRetryAfter","PrefetchIsPrivacyDecoy","PrefetchIsStale","PrefetchNotEligibleBrowserContextOffTheRecord","PrefetchNotEligibleDataSaverEnabled","PrefetchNotEligibleExistingProxy","PrefetchNotEligibleHostIsNonUnique","PrefetchNotEligibleNonDefaultStoragePartition","PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy","PrefetchNotEligibleSchemeIsNotHttps","PrefetchNotEligibleUserHasCookies","PrefetchNotEligibleUserHasServiceWorker","PrefetchNotEligibleBatterySaverEnabled","PrefetchNotEligiblePreloadingDisabled","PrefetchNotFinishedInTime","PrefetchNotStarted","PrefetchNotUsedCookiesChanged","PrefetchProxyNotAvailable","PrefetchResponseUsed","PrefetchSuccessfulButNotUsed","PrefetchNotUsedProbeFailed"]}],"commands":[{"name":"enable"},{"name":"disable"}],"events":[{"name":"ruleSetUpdated","description":"Upsert. Currently, it is only emitted when a rule set added.","parameters":[{"name":"ruleSet","$ref":"RuleSet"}]},{"name":"ruleSetRemoved","parameters":[{"name":"id","$ref":"RuleSetId"}]},{"name":"prerenderAttemptCompleted","description":"Fired when a prerender attempt is completed.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"initiatingFrameId","description":"The frame id of the frame initiating prerendering.","$ref":"Page.FrameId"},{"name":"prerenderingUrl","type":"string"},{"name":"finalStatus","$ref":"PrerenderFinalStatus"},{"name":"disallowedApiMethod","description":"This is used to give users more information about the name of the API call\\nthat is incompatible with prerender and has caused the cancellation of the attempt","optional":true,"type":"string"}]},{"name":"preloadEnabledStateUpdated","description":"Fired when a preload enabled state is updated.","parameters":[{"name":"disabledByPreference","type":"boolean"},{"name":"disabledByDataSaver","type":"boolean"},{"name":"disabledByBatterySaver","type":"boolean"},{"name":"disabledByHoldbackPrefetchSpeculationRules","type":"boolean"},{"name":"disabledByHoldbackPrerenderSpeculationRules","type":"boolean"}]},{"name":"prefetchStatusUpdated","description":"Fired when a prefetch attempt is updated.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"initiatingFrameId","description":"The frame id of the frame initiating prefetch.","$ref":"Page.FrameId"},{"name":"prefetchUrl","type":"string"},{"name":"status","$ref":"PreloadingStatus"},{"name":"prefetchStatus","$ref":"PrefetchStatus"},{"name":"requestId","$ref":"Network.RequestId"}]},{"name":"prerenderStatusUpdated","description":"Fired when a prerender attempt is updated.","parameters":[{"name":"key","$ref":"PreloadingAttemptKey"},{"name":"status","$ref":"PreloadingStatus"},{"name":"prerenderStatus","optional":true,"$ref":"PrerenderFinalStatus"},{"name":"disallowedMojoInterface","description":"This is used to give users more information about the name of Mojo interface\\nthat is incompatible with prerender and has caused the cancellation of the attempt.","optional":true,"type":"string"}]},{"name":"preloadingAttemptSourcesUpdated","description":"Send a list of sources for all preloading attempts in a document.","parameters":[{"name":"loaderId","$ref":"Network.LoaderId"},{"name":"preloadingAttemptSources","type":"array","items":{"$ref":"PreloadingAttemptSource"}}]}]},{"domain":"FedCm","description":"This domain allows interacting with the FedCM dialog.","experimental":true,"types":[{"id":"LoginState","description":"Whether this is a sign-up or sign-in action for this account, i.e.\\nwhether this account has ever been used to sign in to this RP before.","type":"string","enum":["SignIn","SignUp"]},{"id":"DialogType","description":"Whether the dialog shown is an account chooser or an auto re-authentication dialog.","type":"string","enum":["AccountChooser","AutoReauthn"]},{"id":"Account","description":"Corresponds to IdentityRequestAccount","type":"object","properties":[{"name":"accountId","type":"string"},{"name":"email","type":"string"},{"name":"name","type":"string"},{"name":"givenName","type":"string"},{"name":"pictureUrl","type":"string"},{"name":"idpConfigUrl","type":"string"},{"name":"idpSigninUrl","type":"string"},{"name":"loginState","$ref":"LoginState"},{"name":"termsOfServiceUrl","description":"These two are only set if the loginState is signUp","optional":true,"type":"string"},{"name":"privacyPolicyUrl","optional":true,"type":"string"}]}],"events":[{"name":"dialogShown","parameters":[{"name":"dialogId","type":"string"},{"name":"dialogType","$ref":"DialogType"},{"name":"accounts","type":"array","items":{"$ref":"Account"}},{"name":"title","description":"These exist primarily so that the caller can verify the\\nRP context was used appropriately.","type":"string"},{"name":"subtitle","optional":true,"type":"string"}]}],"commands":[{"name":"enable","parameters":[{"name":"disableRejectionDelay","description":"Allows callers to disable the promise rejection delay that would\\nnormally happen, if this is unimportant to what\'s being tested.\\n(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)","optional":true,"type":"boolean"}]},{"name":"disable"},{"name":"selectAccount","parameters":[{"name":"dialogId","type":"string"},{"name":"accountIndex","type":"integer"}]},{"name":"dismissDialog","parameters":[{"name":"dialogId","type":"string"},{"name":"triggerCooldown","optional":true,"type":"boolean"}]},{"name":"resetCooldown","description":"Resets the cooldown time, if any, to allow the next FedCM call to show\\na dialog even if one was recently dismissed by the user."}]},{"domain":"Console","description":"This domain is deprecated - use Runtime or Log instead.","deprecated":true,"dependencies":["Runtime"],"types":[{"id":"ConsoleMessage","description":"Console message.","type":"object","properties":[{"name":"source","description":"Message source.","type":"string","enum":["xml","javascript","network","console-api","storage","appcache","rendering","security","other","deprecation","worker"]},{"name":"level","description":"Message severity.","type":"string","enum":["log","warning","error","debug","info"]},{"name":"text","description":"Message text.","type":"string"},{"name":"url","description":"URL of the message origin.","optional":true,"type":"string"},{"name":"line","description":"Line number in the resource that generated this message (1-based).","optional":true,"type":"integer"},{"name":"column","description":"Column number in the resource that generated this message (1-based).","optional":true,"type":"integer"}]}],"commands":[{"name":"clearMessages","description":"Does nothing."},{"name":"disable","description":"Disables console domain, prevents further console messages from being reported to the client."},{"name":"enable","description":"Enables console domain, sends the messages collected so far to the client by means of the\\n`messageAdded` notification."}],"events":[{"name":"messageAdded","description":"Issued when new console message is added.","parameters":[{"name":"message","description":"Console message that has been added.","$ref":"ConsoleMessage"}]}]},{"domain":"Debugger","description":"Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\\nbreakpoints, stepping through execution, exploring stack traces, etc.","dependencies":["Runtime"],"types":[{"id":"BreakpointId","description":"Breakpoint identifier.","type":"string"},{"id":"CallFrameId","description":"Call frame identifier.","type":"string"},{"id":"Location","description":"Location in the source code.","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"}]},{"id":"ScriptPosition","description":"Location in the source code.","experimental":true,"type":"object","properties":[{"name":"lineNumber","type":"integer"},{"name":"columnNumber","type":"integer"}]},{"id":"LocationRange","description":"Location range within one script.","experimental":true,"type":"object","properties":[{"name":"scriptId","$ref":"Runtime.ScriptId"},{"name":"start","$ref":"ScriptPosition"},{"name":"end","$ref":"ScriptPosition"}]},{"id":"CallFrame","description":"JavaScript call frame. Array of call frames form the call stack.","type":"object","properties":[{"name":"callFrameId","description":"Call frame identifier. This identifier is only valid while the virtual machine is paused.","$ref":"CallFrameId"},{"name":"functionName","description":"Name of the JavaScript function called on this call frame.","type":"string"},{"name":"functionLocation","description":"Location in the source code.","optional":true,"$ref":"Location"},{"name":"location","description":"Location in the source code.","$ref":"Location"},{"name":"url","description":"JavaScript script name or url.\\nDeprecated in favor of using the `location.scriptId` to resolve the URL via a previously\\nsent `Debugger.scriptParsed` event.","deprecated":true,"type":"string"},{"name":"scopeChain","description":"Scope chain for this call frame.","type":"array","items":{"$ref":"Scope"}},{"name":"this","description":"`this` object for this call frame.","$ref":"Runtime.RemoteObject"},{"name":"returnValue","description":"The value being returned, if the function is at return point.","optional":true,"$ref":"Runtime.RemoteObject"},{"name":"canBeRestarted","description":"Valid only while the VM is paused and indicates whether this frame\\ncan be restarted or not. Note that a `true` value here does not\\nguarantee that Debugger#restartFrame with this CallFrameId will be\\nsuccessful, but it is very likely.","experimental":true,"optional":true,"type":"boolean"}]},{"id":"Scope","description":"Scope description.","type":"object","properties":[{"name":"type","description":"Scope type.","type":"string","enum":["global","local","with","closure","catch","block","script","eval","module","wasm-expression-stack"]},{"name":"object","description":"Object representing the scope. For `global` and `with` scopes it represents the actual\\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\\nvariables as its properties.","$ref":"Runtime.RemoteObject"},{"name":"name","optional":true,"type":"string"},{"name":"startLocation","description":"Location in the source code where scope starts","optional":true,"$ref":"Location"},{"name":"endLocation","description":"Location in the source code where scope ends","optional":true,"$ref":"Location"}]},{"id":"SearchMatch","description":"Search match for resource.","type":"object","properties":[{"name":"lineNumber","description":"Line number in resource content.","type":"number"},{"name":"lineContent","description":"Line with match content.","type":"string"}]},{"id":"BreakLocation","type":"object","properties":[{"name":"scriptId","description":"Script identifier as reported in the `Debugger.scriptParsed`.","$ref":"Runtime.ScriptId"},{"name":"lineNumber","description":"Line number in the script (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number in the script (0-based).","optional":true,"type":"integer"},{"name":"type","optional":true,"type":"string","enum":["debuggerStatement","call","return"]}]},{"id":"WasmDisassemblyChunk","experimental":true,"type":"object","properties":[{"name":"lines","description":"The next chunk of disassembled lines.","type":"array","items":{"type":"string"}},{"name":"bytecodeOffsets","description":"The bytecode offsets describing the start of each line.","type":"array","items":{"type":"integer"}}]},{"id":"ScriptLanguage","description":"Enum of possible script languages.","type":"string","enum":["JavaScript","WebAssembly"]},{"id":"DebugSymbols","description":"Debug symbols available for a wasm script.","type":"object","properties":[{"name":"type","description":"Type of the debug symbols.","type":"string","enum":["None","SourceMap","EmbeddedDWARF","ExternalDWARF"]},{"name":"externalURL","description":"URL of the external symbol source.","optional":true,"type":"string"}]}],"commands":[{"name":"continueToLocation","description":"Continues execution until specific location is reached.","parameters":[{"name":"location","description":"Location to continue to.","$ref":"Location"},{"name":"targetCallFrames","optional":true,"type":"string","enum":["any","current"]}]},{"name":"disable","description":"Disables debugger for given page."},{"name":"enable","description":"Enables debugger for the given page. Clients should not assume that the debugging has been\\nenabled until the result for this command is received.","parameters":[{"name":"maxScriptsCacheSize","description":"The maximum size in bytes of collected scripts (not referenced by other heap objects)\\nthe debugger can hold. Puts no limit if parameter is omitted.","experimental":true,"optional":true,"type":"number"}],"returns":[{"name":"debuggerId","description":"Unique identifier of the debugger.","experimental":true,"$ref":"Runtime.UniqueDebuggerId"}]},{"name":"evaluateOnCallFrame","description":"Evaluates expression on a given call frame.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"String object group name to put result into (allows rapid releasing resulting object handles\\nusing `releaseObjectGroup`).","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Specifies whether command line API should be available to the evaluated expression, defaults\\nto false.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"Runtime.TimeDelta"}],"returns":[{"name":"result","description":"Object wrapper for the evaluation result.","$ref":"Runtime.RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"getPossibleBreakpoints","description":"Returns possible locations for breakpoint. scriptId in start and end range locations should be\\nthe same.","parameters":[{"name":"start","description":"Start of range to search possible breakpoint locations in.","$ref":"Location"},{"name":"end","description":"End of range to search possible breakpoint locations in (excluding). When not specified, end\\nof scripts is used as end of range.","optional":true,"$ref":"Location"},{"name":"restrictToFunction","description":"Only consider locations which are in the same (non-nested) function as start.","optional":true,"type":"boolean"}],"returns":[{"name":"locations","description":"List of the possible breakpoint locations.","type":"array","items":{"$ref":"BreakLocation"}}]},{"name":"getScriptSource","description":"Returns source for the script with given id.","parameters":[{"name":"scriptId","description":"Id of the script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"scriptSource","description":"Script source (empty in case of Wasm bytecode).","type":"string"},{"name":"bytecode","description":"Wasm bytecode. (Encoded as a base64 string when passed over JSON)","optional":true,"type":"string"}]},{"name":"disassembleWasmModule","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script to disassemble","$ref":"Runtime.ScriptId"}],"returns":[{"name":"streamId","description":"For large modules, return a stream from which additional chunks of\\ndisassembly can be read successively.","optional":true,"type":"string"},{"name":"totalNumberOfLines","description":"The total number of lines in the disassembly text.","type":"integer"},{"name":"functionBodyOffsets","description":"The offsets of all function bodies, in the format [start1, end1,\\nstart2, end2, ...] where all ends are exclusive.","type":"array","items":{"type":"integer"}},{"name":"chunk","description":"The first chunk of disassembly.","$ref":"WasmDisassemblyChunk"}]},{"name":"nextWasmDisassemblyChunk","description":"Disassemble the next chunk of lines for the module corresponding to the\\nstream. If disassembly is complete, this API will invalidate the streamId\\nand return an empty chunk. Any subsequent calls for the now invalid stream\\nwill return errors.","experimental":true,"parameters":[{"name":"streamId","type":"string"}],"returns":[{"name":"chunk","description":"The next chunk of disassembly.","$ref":"WasmDisassemblyChunk"}]},{"name":"getWasmBytecode","description":"This command is deprecated. Use getScriptSource instead.","deprecated":true,"parameters":[{"name":"scriptId","description":"Id of the Wasm script to get source for.","$ref":"Runtime.ScriptId"}],"returns":[{"name":"bytecode","description":"Script source. (Encoded as a base64 string when passed over JSON)","type":"string"}]},{"name":"getStackTrace","description":"Returns stack trace with given `stackTraceId`.","experimental":true,"parameters":[{"name":"stackTraceId","$ref":"Runtime.StackTraceId"}],"returns":[{"name":"stackTrace","$ref":"Runtime.StackTrace"}]},{"name":"pause","description":"Stops on the next JavaScript statement."},{"name":"pauseOnAsyncCall","experimental":true,"deprecated":true,"parameters":[{"name":"parentStackTraceId","description":"Debugger will pause when async call with given stack trace is started.","$ref":"Runtime.StackTraceId"}]},{"name":"removeBreakpoint","description":"Removes JavaScript breakpoint.","parameters":[{"name":"breakpointId","$ref":"BreakpointId"}]},{"name":"restartFrame","description":"Restarts particular call frame from the beginning. The old, deprecated\\nbehavior of `restartFrame` is to stay paused and allow further CDP commands\\nafter a restart was scheduled. This can cause problems with restarting, so\\nwe now continue execution immediatly after it has been scheduled until we\\nreach the beginning of the restarted frame.\\n\\nTo stay back-wards compatible, `restartFrame` now expects a `mode`\\nparameter to be present. If the `mode` parameter is missing, `restartFrame`\\nerrors out.\\n\\nThe various return values are deprecated and `callFrames` is always empty.\\nUse the call frames from the `Debugger#paused` events instead, that fires\\nonce V8 pauses at the beginning of the restarted function.","parameters":[{"name":"callFrameId","description":"Call frame identifier to evaluate on.","$ref":"CallFrameId"},{"name":"mode","description":"The `mode` parameter must be present and set to \'StepInto\', otherwise\\n`restartFrame` will error out.","experimental":true,"optional":true,"type":"string","enum":["StepInto"]}],"returns":[{"name":"callFrames","description":"New stack trace.","deprecated":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resume","description":"Resumes JavaScript execution.","parameters":[{"name":"terminateOnResume","description":"Set to true to terminate execution upon resuming execution. In contrast\\nto Runtime.terminateExecution, this will allows to execute further\\nJavaScript (i.e. via evaluation) until execution of the paused code\\nis actually resumed, at which point termination is triggered.\\nIf execution is currently not paused, this parameter has no effect.","optional":true,"type":"boolean"}]},{"name":"searchInContent","description":"Searches for given string in script content.","parameters":[{"name":"scriptId","description":"Id of the script to search in.","$ref":"Runtime.ScriptId"},{"name":"query","description":"String to search for.","type":"string"},{"name":"caseSensitive","description":"If true, search is case sensitive.","optional":true,"type":"boolean"},{"name":"isRegex","description":"If true, treats string parameter as regex.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"List of search matches.","type":"array","items":{"$ref":"SearchMatch"}}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setBlackboxPatterns","description":"Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\\nperforming \'step in\' several times, finally resorting to \'step out\' if unsuccessful.","experimental":true,"parameters":[{"name":"patterns","description":"Array of regexps that will be used to check script url for blackbox state.","type":"array","items":{"type":"string"}}]},{"name":"setBlackboxedRanges","description":"Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\\nscripts by performing \'step in\' several times, finally resorting to \'step out\' if unsuccessful.\\nPositions array contains positions where blackbox state is changed. First interval isn\'t\\nblackboxed. Array should be sorted.","experimental":true,"parameters":[{"name":"scriptId","description":"Id of the script.","$ref":"Runtime.ScriptId"},{"name":"positions","type":"array","items":{"$ref":"ScriptPosition"}}]},{"name":"setBreakpoint","description":"Sets JavaScript breakpoint at a given location.","parameters":[{"name":"location","description":"Location to set breakpoint in.","$ref":"Location"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"actualLocation","description":"Location this breakpoint resolved into.","$ref":"Location"}]},{"name":"setInstrumentationBreakpoint","description":"Sets instrumentation breakpoint.","parameters":[{"name":"instrumentation","description":"Instrumentation name.","type":"string","enum":["beforeScriptExecution","beforeScriptWithSourceMapExecution"]}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointByUrl","description":"Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\\n`locations` property. Further matching script parsing will result in subsequent\\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.","parameters":[{"name":"lineNumber","description":"Line number to set breakpoint at.","type":"integer"},{"name":"url","description":"URL of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"urlRegex","description":"Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\\n`urlRegex` must be specified.","optional":true,"type":"string"},{"name":"scriptHash","description":"Script hash of the resources to set breakpoint on.","optional":true,"type":"string"},{"name":"columnNumber","description":"Offset in the line to set breakpoint at.","optional":true,"type":"integer"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will only stop on the\\nbreakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"},{"name":"locations","description":"List of the locations this breakpoint resolved into upon addition.","type":"array","items":{"$ref":"Location"}}]},{"name":"setBreakpointOnFunctionCall","description":"Sets JavaScript breakpoint before each call to the given function.\\nIf another function was created from the same source as a given one,\\ncalling it will also trigger the breakpoint.","experimental":true,"parameters":[{"name":"objectId","description":"Function object id.","$ref":"Runtime.RemoteObjectId"},{"name":"condition","description":"Expression to use as a breakpoint condition. When specified, debugger will\\nstop on the breakpoint if this expression evaluates to true.","optional":true,"type":"string"}],"returns":[{"name":"breakpointId","description":"Id of the created breakpoint for further reference.","$ref":"BreakpointId"}]},{"name":"setBreakpointsActive","description":"Activates / deactivates all breakpoints on the page.","parameters":[{"name":"active","description":"New value for breakpoints active state.","type":"boolean"}]},{"name":"setPauseOnExceptions","description":"Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions,\\nor caught exceptions, no exceptions. Initial pause on exceptions state is `none`.","parameters":[{"name":"state","description":"Pause on exceptions mode.","type":"string","enum":["none","caught","uncaught","all"]}]},{"name":"setReturnValue","description":"Changes return value in top frame. Available only at return break position.","experimental":true,"parameters":[{"name":"newValue","description":"New return value.","$ref":"Runtime.CallArgument"}]},{"name":"setScriptSource","description":"Edits JavaScript source live.\\n\\nIn general, functions that are currently on the stack can not be edited with\\na single exception: If the edited function is the top-most stack frame and\\nthat is the only activation of that function on the stack. In this case\\nthe live edit will be successful and a `Debugger.restartFrame` for the\\ntop-most function is automatically triggered.","parameters":[{"name":"scriptId","description":"Id of the script to edit.","$ref":"Runtime.ScriptId"},{"name":"scriptSource","description":"New content of the script.","type":"string"},{"name":"dryRun","description":"If true the change will not actually be applied. Dry run may be used to get result\\ndescription without actually modifying the code.","optional":true,"type":"boolean"},{"name":"allowTopFrameEditing","description":"If true, then `scriptSource` is allowed to change the function on top of the stack\\nas long as the top-most stack frame is the only activation of that function.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"callFrames","description":"New stack trace in case editing has happened while VM was stopped.","deprecated":true,"optional":true,"type":"array","items":{"$ref":"CallFrame"}},{"name":"stackChanged","description":"Whether current call stack was modified after applying the changes.","deprecated":true,"optional":true,"type":"boolean"},{"name":"asyncStackTrace","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"status","description":"Whether the operation was successful or not. Only `Ok` denotes a\\nsuccessful live edit while the other enum variants denote why\\nthe live edit failed.","experimental":true,"type":"string","enum":["Ok","CompileError","BlockedByActiveGenerator","BlockedByActiveFunction","BlockedByTopLevelEsModuleChange"]},{"name":"exceptionDetails","description":"Exception details if any. Only present when `status` is `CompileError`.","optional":true,"$ref":"Runtime.ExceptionDetails"}]},{"name":"setSkipAllPauses","description":"Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).","parameters":[{"name":"skip","description":"New value for skip pauses state.","type":"boolean"}]},{"name":"setVariableValue","description":"Changes value of variable in a callframe. Object-based scopes are not supported and must be\\nmutated manually.","parameters":[{"name":"scopeNumber","description":"0-based number of scope as was listed in scope chain. Only \'local\', \'closure\' and \'catch\'\\nscope types are allowed. Other scopes could be manipulated manually.","type":"integer"},{"name":"variableName","description":"Variable name.","type":"string"},{"name":"newValue","description":"New variable value.","$ref":"Runtime.CallArgument"},{"name":"callFrameId","description":"Id of callframe that holds variable.","$ref":"CallFrameId"}]},{"name":"stepInto","description":"Steps into the function call.","parameters":[{"name":"breakOnAsyncCall","description":"Debugger will pause on the execution of the first async task which was scheduled\\nbefore next pause.","experimental":true,"optional":true,"type":"boolean"},{"name":"skipList","description":"The skipList specifies location ranges that should be skipped on step into.","experimental":true,"optional":true,"type":"array","items":{"$ref":"LocationRange"}}]},{"name":"stepOut","description":"Steps out of the function call."},{"name":"stepOver","description":"Steps over the statement.","parameters":[{"name":"skipList","description":"The skipList specifies location ranges that should be skipped on step over.","experimental":true,"optional":true,"type":"array","items":{"$ref":"LocationRange"}}]}],"events":[{"name":"breakpointResolved","description":"Fired when breakpoint is resolved to an actual script and location.","parameters":[{"name":"breakpointId","description":"Breakpoint unique identifier.","$ref":"BreakpointId"},{"name":"location","description":"Actual breakpoint location.","$ref":"Location"}]},{"name":"paused","description":"Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.","parameters":[{"name":"callFrames","description":"Call stack the virtual machine stopped on.","type":"array","items":{"$ref":"CallFrame"}},{"name":"reason","description":"Pause reason.","type":"string","enum":["ambiguous","assert","CSPViolation","debugCommand","DOM","EventListener","exception","instrumentation","OOM","other","promiseRejection","XHR","step"]},{"name":"data","description":"Object containing break-specific auxiliary properties.","optional":true,"type":"object"},{"name":"hitBreakpoints","description":"Hit breakpoints IDs","optional":true,"type":"array","items":{"type":"string"}},{"name":"asyncStackTrace","description":"Async stack trace, if any.","optional":true,"$ref":"Runtime.StackTrace"},{"name":"asyncStackTraceId","description":"Async stack trace, if any.","experimental":true,"optional":true,"$ref":"Runtime.StackTraceId"},{"name":"asyncCallStackTraceId","description":"Never present, will be removed.","experimental":true,"deprecated":true,"optional":true,"$ref":"Runtime.StackTraceId"}]},{"name":"resumed","description":"Fired when the virtual machine resumed execution."},{"name":"scriptFailedToParse","description":"Fired when virtual machine fails to parse the script.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script, SHA-256.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"codeOffset","description":"If the scriptLanguage is WebAssembly, the code section offset in the module.","experimental":true,"optional":true,"type":"integer"},{"name":"scriptLanguage","description":"The language of the script.","experimental":true,"optional":true,"$ref":"Debugger.ScriptLanguage"},{"name":"embedderName","description":"The name the embedder supplied for this script.","experimental":true,"optional":true,"type":"string"}]},{"name":"scriptParsed","description":"Fired when virtual machine parses script. This event is also fired for all known and uncollected\\nscripts upon enabling debugger.","parameters":[{"name":"scriptId","description":"Identifier of the script parsed.","$ref":"Runtime.ScriptId"},{"name":"url","description":"URL or name of the script parsed (if any).","type":"string"},{"name":"startLine","description":"Line offset of the script within the resource with given URL (for script tags).","type":"integer"},{"name":"startColumn","description":"Column offset of the script within the resource with given URL.","type":"integer"},{"name":"endLine","description":"Last line of the script.","type":"integer"},{"name":"endColumn","description":"Length of the last line of the script.","type":"integer"},{"name":"executionContextId","description":"Specifies script creation context.","$ref":"Runtime.ExecutionContextId"},{"name":"hash","description":"Content hash of the script, SHA-256.","type":"string"},{"name":"executionContextAuxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"},{"name":"isLiveEdit","description":"True, if this script is generated as a result of the live edit operation.","experimental":true,"optional":true,"type":"boolean"},{"name":"sourceMapURL","description":"URL of source map associated with script (if any).","optional":true,"type":"string"},{"name":"hasSourceURL","description":"True, if this script has sourceURL.","optional":true,"type":"boolean"},{"name":"isModule","description":"True, if this script is ES6 module.","optional":true,"type":"boolean"},{"name":"length","description":"This script length.","optional":true,"type":"integer"},{"name":"stackTrace","description":"JavaScript top stack frame of where the script parsed event was triggered if available.","experimental":true,"optional":true,"$ref":"Runtime.StackTrace"},{"name":"codeOffset","description":"If the scriptLanguage is WebAssembly, the code section offset in the module.","experimental":true,"optional":true,"type":"integer"},{"name":"scriptLanguage","description":"The language of the script.","experimental":true,"optional":true,"$ref":"Debugger.ScriptLanguage"},{"name":"debugSymbols","description":"If the scriptLanguage is WebASsembly, the source of debug symbols for the module.","experimental":true,"optional":true,"$ref":"Debugger.DebugSymbols"},{"name":"embedderName","description":"The name the embedder supplied for this script.","experimental":true,"optional":true,"type":"string"}]}]},{"domain":"HeapProfiler","experimental":true,"dependencies":["Runtime"],"types":[{"id":"HeapSnapshotObjectId","description":"Heap snapshot object id.","type":"string"},{"id":"SamplingHeapProfileNode","description":"Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.","type":"object","properties":[{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"selfSize","description":"Allocations size in bytes for the node excluding children.","type":"number"},{"name":"id","description":"Node id. Ids are unique across all profiles collected between startSampling and stopSampling.","type":"integer"},{"name":"children","description":"Child nodes.","type":"array","items":{"$ref":"SamplingHeapProfileNode"}}]},{"id":"SamplingHeapProfileSample","description":"A single sample from a sampling profile.","type":"object","properties":[{"name":"size","description":"Allocation size in bytes attributed to the sample.","type":"number"},{"name":"nodeId","description":"Id of the corresponding profile tree node.","type":"integer"},{"name":"ordinal","description":"Time-ordered sample ordinal number. It is unique across all profiles retrieved\\nbetween startSampling and stopSampling.","type":"number"}]},{"id":"SamplingHeapProfile","description":"Sampling profile.","type":"object","properties":[{"name":"head","$ref":"SamplingHeapProfileNode"},{"name":"samples","type":"array","items":{"$ref":"SamplingHeapProfileSample"}}]}],"commands":[{"name":"addInspectedHeapObject","description":"Enables console to refer to the node with given id via $x (see Command Line API for more details\\n$x functions).","parameters":[{"name":"heapObjectId","description":"Heap snapshot object id to be accessible by means of $x command line API.","$ref":"HeapSnapshotObjectId"}]},{"name":"collectGarbage"},{"name":"disable"},{"name":"enable"},{"name":"getHeapObjectId","parameters":[{"name":"objectId","description":"Identifier of the object to get heap object id for.","$ref":"Runtime.RemoteObjectId"}],"returns":[{"name":"heapSnapshotObjectId","description":"Id of the heap snapshot object corresponding to the passed remote object id.","$ref":"HeapSnapshotObjectId"}]},{"name":"getObjectByHeapObjectId","parameters":[{"name":"objectId","$ref":"HeapSnapshotObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"Runtime.RemoteObject"}]},{"name":"getSamplingProfile","returns":[{"name":"profile","description":"Return the sampling profile being collected.","$ref":"SamplingHeapProfile"}]},{"name":"startSampling","parameters":[{"name":"samplingInterval","description":"Average sample interval in bytes. Poisson distribution is used for the intervals. The\\ndefault value is 32768 bytes.","optional":true,"type":"number"},{"name":"includeObjectsCollectedByMajorGC","description":"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nmajor GC, which will show which functions cause large temporary memory\\nusage or long GC pauses.","optional":true,"type":"boolean"},{"name":"includeObjectsCollectedByMinorGC","description":"By default, the sampling heap profiler reports only objects which are\\nstill alive when the profile is returned via getSamplingProfile or\\nstopSampling, which is useful for determining what functions contribute\\nthe most to steady-state memory usage. This flag instructs the sampling\\nheap profiler to also include information about objects discarded by\\nminor GC, which is useful when tuning a latency-sensitive application\\nfor minimal GC activity.","optional":true,"type":"boolean"}]},{"name":"startTrackingHeapObjects","parameters":[{"name":"trackAllocations","optional":true,"type":"boolean"}]},{"name":"stopSampling","returns":[{"name":"profile","description":"Recorded sampling heap profile.","$ref":"SamplingHeapProfile"}]},{"name":"stopTrackingHeapObjects","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken\\nwhen the tracking is stopped.","optional":true,"type":"boolean"},{"name":"treatGlobalObjectsAsRoots","description":"Deprecated in favor of `exposeInternals`.","deprecated":true,"optional":true,"type":"boolean"},{"name":"captureNumericValue","description":"If true, numerical values are included in the snapshot","optional":true,"type":"boolean"},{"name":"exposeInternals","description":"If true, exposes internals of the snapshot.","experimental":true,"optional":true,"type":"boolean"}]},{"name":"takeHeapSnapshot","parameters":[{"name":"reportProgress","description":"If true \'reportHeapSnapshotProgress\' events will be generated while snapshot is being taken.","optional":true,"type":"boolean"},{"name":"treatGlobalObjectsAsRoots","description":"If true, a raw snapshot without artificial roots will be generated.\\nDeprecated in favor of `exposeInternals`.","deprecated":true,"optional":true,"type":"boolean"},{"name":"captureNumericValue","description":"If true, numerical values are included in the snapshot","optional":true,"type":"boolean"},{"name":"exposeInternals","description":"If true, exposes internals of the snapshot.","experimental":true,"optional":true,"type":"boolean"}]}],"events":[{"name":"addHeapSnapshotChunk","parameters":[{"name":"chunk","type":"string"}]},{"name":"heapStatsUpdate","description":"If heap objects tracking has been started then backend may send update for one or more fragments","parameters":[{"name":"statsUpdate","description":"An array of triplets. Each triplet describes a fragment. The first integer is the fragment\\nindex, the second integer is a total count of objects for the fragment, the third integer is\\na total size of the objects for the fragment.","type":"array","items":{"type":"integer"}}]},{"name":"lastSeenObjectId","description":"If heap objects tracking has been started then backend regularly sends a current value for last\\nseen object id and corresponding timestamp. If the were changes in the heap since last event\\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.","parameters":[{"name":"lastSeenObjectId","type":"integer"},{"name":"timestamp","type":"number"}]},{"name":"reportHeapSnapshotProgress","parameters":[{"name":"done","type":"integer"},{"name":"total","type":"integer"},{"name":"finished","optional":true,"type":"boolean"}]},{"name":"resetProfiles"}]},{"domain":"Profiler","dependencies":["Runtime","Debugger"],"types":[{"id":"ProfileNode","description":"Profile node. Holds callsite information, execution statistics and child nodes.","type":"object","properties":[{"name":"id","description":"Unique id of the node.","type":"integer"},{"name":"callFrame","description":"Function location.","$ref":"Runtime.CallFrame"},{"name":"hitCount","description":"Number of samples where this node was on top of the call stack.","optional":true,"type":"integer"},{"name":"children","description":"Child node ids.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"deoptReason","description":"The reason of being not optimized. The function may be deoptimized or marked as don\'t\\noptimize.","optional":true,"type":"string"},{"name":"positionTicks","description":"An array of source position ticks.","optional":true,"type":"array","items":{"$ref":"PositionTickInfo"}}]},{"id":"Profile","description":"Profile.","type":"object","properties":[{"name":"nodes","description":"The list of profile nodes. First item is the root node.","type":"array","items":{"$ref":"ProfileNode"}},{"name":"startTime","description":"Profiling start timestamp in microseconds.","type":"number"},{"name":"endTime","description":"Profiling end timestamp in microseconds.","type":"number"},{"name":"samples","description":"Ids of samples top nodes.","optional":true,"type":"array","items":{"type":"integer"}},{"name":"timeDeltas","description":"Time intervals between adjacent samples in microseconds. The first delta is relative to the\\nprofile startTime.","optional":true,"type":"array","items":{"type":"integer"}}]},{"id":"PositionTickInfo","description":"Specifies a number of samples attributed to a certain source position.","type":"object","properties":[{"name":"line","description":"Source line number (1-based).","type":"integer"},{"name":"ticks","description":"Number of samples attributed to the source line.","type":"integer"}]},{"id":"CoverageRange","description":"Coverage data for a source range.","type":"object","properties":[{"name":"startOffset","description":"JavaScript script source offset for the range start.","type":"integer"},{"name":"endOffset","description":"JavaScript script source offset for the range end.","type":"integer"},{"name":"count","description":"Collected execution count of the source range.","type":"integer"}]},{"id":"FunctionCoverage","description":"Coverage data for a JavaScript function.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"ranges","description":"Source ranges inside the function with coverage data.","type":"array","items":{"$ref":"CoverageRange"}},{"name":"isBlockCoverage","description":"Whether coverage data for this function has block granularity.","type":"boolean"}]},{"id":"ScriptCoverage","description":"Coverage data for a JavaScript script.","type":"object","properties":[{"name":"scriptId","description":"JavaScript script id.","$ref":"Runtime.ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"functions","description":"Functions contained in the script that has coverage data.","type":"array","items":{"$ref":"FunctionCoverage"}}]}],"commands":[{"name":"disable"},{"name":"enable"},{"name":"getBestEffortCoverage","description":"Collect coverage data for the current isolate. The coverage data may be incomplete due to\\ngarbage collection.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]},{"name":"setSamplingInterval","description":"Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.","parameters":[{"name":"interval","description":"New sampling interval in microseconds.","type":"integer"}]},{"name":"start"},{"name":"startPreciseCoverage","description":"Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\\ncounters.","parameters":[{"name":"callCount","description":"Collect accurate call counts beyond simple \'covered\' or \'not covered\'.","optional":true,"type":"boolean"},{"name":"detailed","description":"Collect block-based coverage.","optional":true,"type":"boolean"},{"name":"allowTriggeredUpdates","description":"Allow the backend to send updates on its own initiative","optional":true,"type":"boolean"}],"returns":[{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"}]},{"name":"stop","returns":[{"name":"profile","description":"Recorded profile.","$ref":"Profile"}]},{"name":"stopPreciseCoverage","description":"Disable precise code coverage. Disabling releases unnecessary execution count records and allows\\nexecuting optimized code."},{"name":"takePreciseCoverage","description":"Collect coverage data for the current isolate, and resets execution counters. Precise code\\ncoverage needs to have started.","returns":[{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}},{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"}]}],"events":[{"name":"consoleProfileFinished","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profileEnd().","$ref":"Debugger.Location"},{"name":"profile","$ref":"Profile"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"consoleProfileStarted","description":"Sent when new profile recording is started using console.profile() call.","parameters":[{"name":"id","type":"string"},{"name":"location","description":"Location of console.profile().","$ref":"Debugger.Location"},{"name":"title","description":"Profile title passed as an argument to console.profile().","optional":true,"type":"string"}]},{"name":"preciseCoverageDeltaUpdate","description":"Reports coverage delta since the last poll (either from an event like this, or from\\n`takePreciseCoverage` for the current isolate. May only be sent if precise code\\ncoverage has been started. This event can be trigged by the embedder to, for example,\\ntrigger collection of coverage data immediately at a certain point in time.","experimental":true,"parameters":[{"name":"timestamp","description":"Monotonically increasing time (in seconds) when the coverage update was taken in the backend.","type":"number"},{"name":"occasion","description":"Identifier for distinguishing coverage events.","type":"string"},{"name":"result","description":"Coverage data for the current isolate.","type":"array","items":{"$ref":"ScriptCoverage"}}]}]},{"domain":"Runtime","description":"Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\\nEvaluation results are returned as mirror object that expose object type, string representation\\nand unique identifier that can be used for further object reference. Original objects are\\nmaintained in memory unless they are either explicitly released or are released along with the\\nother objects in their object group.","types":[{"id":"ScriptId","description":"Unique script identifier.","type":"string"},{"id":"SerializationOptions","description":"Represents options for serialization. Overrides `generatePreview`, `returnByValue` and\\n`generateWebDriverValue`.","type":"object","properties":[{"name":"serialization","type":"string","enum":["deep","json","idOnly"]},{"name":"maxDepth","description":"Deep serialization depth. Default is full depth. Respected only in `deep` serialization mode.","optional":true,"type":"integer"},{"name":"additionalParameters","description":"Embedder-specific parameters. For example if connected to V8 in Chrome these control DOM\\nserialization via `maxNodeDepth: integer` and `includeShadowTree: \\"none\\" | \\"open\\" | \\"all\\"`.\\nValues can be only of type string or integer.","optional":true,"type":"object"}]},{"id":"DeepSerializedValue","description":"Represents deep serialized value.","type":"object","properties":[{"name":"type","type":"string","enum":["undefined","null","string","number","boolean","bigint","regexp","date","symbol","array","object","function","map","set","weakmap","weakset","error","proxy","promise","typedarray","arraybuffer","node","window"]},{"name":"value","optional":true,"type":"any"},{"name":"objectId","optional":true,"type":"string"},{"name":"weakLocalObjectReference","description":"Set if value reference met more then once during serialization. In such\\ncase, value is provided only to one of the serialized values. Unique\\nper value in the scope of one CDP call.","optional":true,"type":"integer"}]},{"id":"RemoteObjectId","description":"Unique object identifier.","type":"string"},{"id":"UnserializableValue","description":"Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\\n`-Infinity`, and bigint literals.","type":"string"},{"id":"RemoteObject","description":"Mirror object referencing original JavaScript object.","type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.\\nNOTE: If you change anything here, make sure to also update\\n`subtype` in `ObjectPreview` and `PropertyPreview` below.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]},{"name":"className","description":"Object class (constructor) name. Specified for `object` type values only.","optional":true,"type":"string"},{"name":"value","description":"Remote object value in case of primitive values or JSON values (if it was requested).","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified does not have `value`, but gets this\\nproperty.","optional":true,"$ref":"UnserializableValue"},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"webDriverValue","description":"Deprecated. Use `deepSerializedValue` instead. WebDriver BiDi representation of the value.","deprecated":true,"optional":true,"$ref":"DeepSerializedValue"},{"name":"deepSerializedValue","description":"Deep serialized value.","experimental":true,"optional":true,"$ref":"DeepSerializedValue"},{"name":"objectId","description":"Unique object identifier (for non-primitive values).","optional":true,"$ref":"RemoteObjectId"},{"name":"preview","description":"Preview containing abbreviated property values. Specified for `object` type values only.","experimental":true,"optional":true,"$ref":"ObjectPreview"},{"name":"customPreview","experimental":true,"optional":true,"$ref":"CustomPreview"}]},{"id":"CustomPreview","experimental":true,"type":"object","properties":[{"name":"header","description":"The JSON-stringified result of formatter.header(object, config) call.\\nIt contains json ML array that represents RemoteObject.","type":"string"},{"name":"bodyGetterId","description":"If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\\nThe result value is json ML array.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ObjectPreview","description":"Object containing abbreviated remote object value.","experimental":true,"type":"object","properties":[{"name":"type","description":"Object type.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","bigint"]},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]},{"name":"description","description":"String representation of the object.","optional":true,"type":"string"},{"name":"overflow","description":"True iff some of the properties or entries of the original object did not fit.","type":"boolean"},{"name":"properties","description":"List of the properties.","type":"array","items":{"$ref":"PropertyPreview"}},{"name":"entries","description":"List of the entries. Specified for `map` and `set` subtype values only.","optional":true,"type":"array","items":{"$ref":"EntryPreview"}}]},{"id":"PropertyPreview","experimental":true,"type":"object","properties":[{"name":"name","description":"Property name.","type":"string"},{"name":"type","description":"Object type. Accessor means that the property itself is an accessor property.","type":"string","enum":["object","function","undefined","string","number","boolean","symbol","accessor","bigint"]},{"name":"value","description":"User-friendly property value string.","optional":true,"type":"string"},{"name":"valuePreview","description":"Nested value preview.","optional":true,"$ref":"ObjectPreview"},{"name":"subtype","description":"Object subtype hint. Specified for `object` type values only.","optional":true,"type":"string","enum":["array","null","node","regexp","date","map","set","weakmap","weakset","iterator","generator","error","proxy","promise","typedarray","arraybuffer","dataview","webassemblymemory","wasmvalue"]}]},{"id":"EntryPreview","experimental":true,"type":"object","properties":[{"name":"key","description":"Preview of the key. Specified for map-like collection entries.","optional":true,"$ref":"ObjectPreview"},{"name":"value","description":"Preview of the value.","$ref":"ObjectPreview"}]},{"id":"PropertyDescriptor","description":"Object property descriptor.","type":"object","properties":[{"name":"name","description":"Property name or symbol description.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"},{"name":"writable","description":"True if the value associated with the property may be changed (data descriptors only).","optional":true,"type":"boolean"},{"name":"get","description":"A function which serves as a getter for the property, or `undefined` if there is no getter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the property, or `undefined` if there is no setter\\n(accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"configurable","description":"True if the type of this property descriptor may be changed and if the property may be\\ndeleted from the corresponding object.","type":"boolean"},{"name":"enumerable","description":"True if this property shows up during enumeration of the properties on the corresponding\\nobject.","type":"boolean"},{"name":"wasThrown","description":"True if the result was thrown during the evaluation.","optional":true,"type":"boolean"},{"name":"isOwn","description":"True if the property is owned for the object.","optional":true,"type":"boolean"},{"name":"symbol","description":"Property symbol object, if the property is of the `symbol` type.","optional":true,"$ref":"RemoteObject"}]},{"id":"InternalPropertyDescriptor","description":"Object internal property descriptor. This property isn\'t normally visible in JavaScript code.","type":"object","properties":[{"name":"name","description":"Conventional property name.","type":"string"},{"name":"value","description":"The value associated with the property.","optional":true,"$ref":"RemoteObject"}]},{"id":"PrivatePropertyDescriptor","description":"Object private field descriptor.","experimental":true,"type":"object","properties":[{"name":"name","description":"Private property name.","type":"string"},{"name":"value","description":"The value associated with the private property.","optional":true,"$ref":"RemoteObject"},{"name":"get","description":"A function which serves as a getter for the private property,\\nor `undefined` if there is no getter (accessor descriptors only).","optional":true,"$ref":"RemoteObject"},{"name":"set","description":"A function which serves as a setter for the private property,\\nor `undefined` if there is no setter (accessor descriptors only).","optional":true,"$ref":"RemoteObject"}]},{"id":"CallArgument","description":"Represents function call argument. Either remote object id `objectId`, primitive `value`,\\nunserializable primitive value or neither of (for undefined) them should be specified.","type":"object","properties":[{"name":"value","description":"Primitive value or serializable javascript object.","optional":true,"type":"any"},{"name":"unserializableValue","description":"Primitive value which can not be JSON-stringified.","optional":true,"$ref":"UnserializableValue"},{"name":"objectId","description":"Remote object handle.","optional":true,"$ref":"RemoteObjectId"}]},{"id":"ExecutionContextId","description":"Id of an execution context.","type":"integer"},{"id":"ExecutionContextDescription","description":"Description of an isolated world.","type":"object","properties":[{"name":"id","description":"Unique id of the execution context. It can be used to specify in which execution context\\nscript evaluation should be performed.","$ref":"ExecutionContextId"},{"name":"origin","description":"Execution context origin.","type":"string"},{"name":"name","description":"Human readable name describing given context.","type":"string"},{"name":"uniqueId","description":"A system-unique execution context identifier. Unlike the id, this is unique across\\nmultiple processes, so can be reliably used to identify specific context while backend\\nperforms a cross-process navigation.","experimental":true,"type":"string"},{"name":"auxData","description":"Embedder-specific auxiliary data likely matching {isDefault: boolean, type: \'default\'|\'isolated\'|\'worker\', frameId: string}","optional":true,"type":"object"}]},{"id":"ExceptionDetails","description":"Detailed information about exception (or error) that was thrown during script compilation or\\nexecution.","type":"object","properties":[{"name":"exceptionId","description":"Exception id.","type":"integer"},{"name":"text","description":"Exception text, which should be used together with exception object when available.","type":"string"},{"name":"lineNumber","description":"Line number of the exception location (0-based).","type":"integer"},{"name":"columnNumber","description":"Column number of the exception location (0-based).","type":"integer"},{"name":"scriptId","description":"Script ID of the exception location.","optional":true,"$ref":"ScriptId"},{"name":"url","description":"URL of the exception location, to be used when the script was not reported.","optional":true,"type":"string"},{"name":"stackTrace","description":"JavaScript stack trace if available.","optional":true,"$ref":"StackTrace"},{"name":"exception","description":"Exception object if available.","optional":true,"$ref":"RemoteObject"},{"name":"executionContextId","description":"Identifier of the context where exception happened.","optional":true,"$ref":"ExecutionContextId"},{"name":"exceptionMetaData","description":"Dictionary with entries of meta data that the client associated\\nwith this exception, such as information about associated network\\nrequests, etc.","experimental":true,"optional":true,"type":"object"}]},{"id":"Timestamp","description":"Number of milliseconds since epoch.","type":"number"},{"id":"TimeDelta","description":"Number of milliseconds.","type":"number"},{"id":"CallFrame","description":"Stack entry for runtime errors and assertions.","type":"object","properties":[{"name":"functionName","description":"JavaScript function name.","type":"string"},{"name":"scriptId","description":"JavaScript script id.","$ref":"ScriptId"},{"name":"url","description":"JavaScript script name or url.","type":"string"},{"name":"lineNumber","description":"JavaScript script line number (0-based).","type":"integer"},{"name":"columnNumber","description":"JavaScript script column number (0-based).","type":"integer"}]},{"id":"StackTrace","description":"Call frames for assertions or error messages.","type":"object","properties":[{"name":"description","description":"String label of this stack trace. For async traces this may be a name of the function that\\ninitiated the async call.","optional":true,"type":"string"},{"name":"callFrames","description":"JavaScript function name.","type":"array","items":{"$ref":"CallFrame"}},{"name":"parent","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","optional":true,"$ref":"StackTrace"},{"name":"parentId","description":"Asynchronous JavaScript stack trace that preceded this stack, if available.","experimental":true,"optional":true,"$ref":"StackTraceId"}]},{"id":"UniqueDebuggerId","description":"Unique identifier of current debugger.","experimental":true,"type":"string"},{"id":"StackTraceId","description":"If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.","experimental":true,"type":"object","properties":[{"name":"id","type":"string"},{"name":"debuggerId","optional":true,"$ref":"UniqueDebuggerId"}]}],"commands":[{"name":"awaitPromise","description":"Add handler to promise with given promise object id.","parameters":[{"name":"promiseObjectId","description":"Identifier of the promise.","$ref":"RemoteObjectId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Promise result. Will contain rejected value if promise was rejected.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details if stack strace is available.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"callFunctionOn","description":"Calls function with given declaration on the given object. Object group of the result is\\ninherited from the target object.","parameters":[{"name":"functionDeclaration","description":"Declaration of the function to call.","type":"string"},{"name":"objectId","description":"Identifier of the object to call function on. Either objectId or executionContextId should\\nbe specified.","optional":true,"$ref":"RemoteObjectId"},{"name":"arguments","description":"Call arguments. All call arguments must belong to the same JavaScript world as the target\\nobject.","optional":true,"type":"array","items":{"$ref":"CallArgument"}},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.\\nCan be overriden by `serializationOptions`.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"executionContextId","description":"Specifies execution context which global object will be used to call function on. Either\\nexecutionContextId or objectId should be specified.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects. If objectGroup is not\\nspecified and objectId is, objectGroup will be inherited from object.","optional":true,"type":"string"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.","experimental":true,"optional":true,"type":"boolean"},{"name":"uniqueContextId","description":"An alternative way to specify the execution context to call function on.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental function call\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `executionContextId`.","experimental":true,"optional":true,"type":"string"},{"name":"generateWebDriverValue","description":"Deprecated. Use `serializationOptions: {serialization:\\"deep\\"}` instead.\\nWhether the result should contain `webDriverValue`, serialized according to\\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\\nresulting `objectId` is still provided.","deprecated":true,"optional":true,"type":"boolean"},{"name":"serializationOptions","description":"Specifies the result serialization. If provided, overrides\\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.","experimental":true,"optional":true,"$ref":"SerializationOptions"}],"returns":[{"name":"result","description":"Call result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"compileScript","description":"Compiles expression.","parameters":[{"name":"expression","description":"Expression to compile.","type":"string"},{"name":"sourceURL","description":"Source url to be set for the script.","type":"string"},{"name":"persistScript","description":"Specifies whether the compiled script should be persisted.","type":"boolean"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"scriptId","description":"Id of the script.","optional":true,"$ref":"ScriptId"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"disable","description":"Disables reporting of execution contexts creation."},{"name":"discardConsoleEntries","description":"Discards collected exceptions and console API calls."},{"name":"enable","description":"Enables reporting of execution contexts creation by means of `executionContextCreated` event.\\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\\ncontext."},{"name":"evaluate","description":"Evaluates expression on global object.","parameters":[{"name":"expression","description":"Expression to evaluate.","type":"string"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"contextId","description":"Specifies in which execution context to perform evaluation. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.\\nThis is mutually exclusive with `uniqueContextId`, which offers an\\nalternative way to identify the execution context that is more reliable\\nin a multi-process environment.","optional":true,"$ref":"ExecutionContextId"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object that should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","experimental":true,"optional":true,"type":"boolean"},{"name":"userGesture","description":"Whether execution should be treated as initiated by user in the UI.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"},{"name":"throwOnSideEffect","description":"Whether to throw an exception if side effect cannot be ruled out during evaluation.\\nThis implies `disableBreaks` below.","experimental":true,"optional":true,"type":"boolean"},{"name":"timeout","description":"Terminate execution after timing out (number of milliseconds).","experimental":true,"optional":true,"$ref":"TimeDelta"},{"name":"disableBreaks","description":"Disable breakpoints during execution.","experimental":true,"optional":true,"type":"boolean"},{"name":"replMode","description":"Setting this flag to true enables `let` re-declaration and top-level `await`.\\nNote that `let` variables can only be re-declared if they originate from\\n`replMode` themselves.","experimental":true,"optional":true,"type":"boolean"},{"name":"allowUnsafeEvalBlockedByCSP","description":"The Content Security Policy (CSP) for the target might block \'unsafe-eval\'\\nwhich includes eval(), Function(), setTimeout() and setInterval()\\nwhen called with non-callable arguments. This flag bypasses CSP for this\\nevaluation and allows unsafe-eval. Defaults to true.","experimental":true,"optional":true,"type":"boolean"},{"name":"uniqueContextId","description":"An alternative way to specify the execution context to evaluate in.\\nCompared to contextId that may be reused across processes, this is guaranteed to be\\nsystem-unique, so it can be used to prevent accidental evaluation of the expression\\nin context different than intended (e.g. as a result of navigation across process\\nboundaries).\\nThis is mutually exclusive with `contextId`.","experimental":true,"optional":true,"type":"string"},{"name":"generateWebDriverValue","description":"Deprecated. Use `serializationOptions: {serialization:\\"deep\\"}` instead.\\nWhether the result should contain `webDriverValue`, serialized\\naccording to\\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\\nresulting `objectId` is still provided.","deprecated":true,"optional":true,"type":"boolean"},{"name":"serializationOptions","description":"Specifies the result serialization. If provided, overrides\\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.","experimental":true,"optional":true,"$ref":"SerializationOptions"}],"returns":[{"name":"result","description":"Evaluation result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"getIsolateId","description":"Returns the isolate id.","experimental":true,"returns":[{"name":"id","description":"The isolate id.","type":"string"}]},{"name":"getHeapUsage","description":"Returns the JavaScript heap usage.\\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.","experimental":true,"returns":[{"name":"usedSize","description":"Used heap size in bytes.","type":"number"},{"name":"totalSize","description":"Allocated heap size in bytes.","type":"number"}]},{"name":"getProperties","description":"Returns properties of a given object. Object group of the result is inherited from the target\\nobject.","parameters":[{"name":"objectId","description":"Identifier of the object to return properties for.","$ref":"RemoteObjectId"},{"name":"ownProperties","description":"If true, returns properties belonging only to the element itself, not to its prototype\\nchain.","optional":true,"type":"boolean"},{"name":"accessorPropertiesOnly","description":"If true, returns accessor properties (with getter/setter) only; internal properties are not\\nreturned either.","experimental":true,"optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the results.","experimental":true,"optional":true,"type":"boolean"},{"name":"nonIndexedPropertiesOnly","description":"If true, returns non-indexed properties only.","experimental":true,"optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Object properties.","type":"array","items":{"$ref":"PropertyDescriptor"}},{"name":"internalProperties","description":"Internal object properties (only of the element itself).","optional":true,"type":"array","items":{"$ref":"InternalPropertyDescriptor"}},{"name":"privateProperties","description":"Object private properties.","experimental":true,"optional":true,"type":"array","items":{"$ref":"PrivatePropertyDescriptor"}},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"globalLexicalScopeNames","description":"Returns all let, const and class variables from global scope.","parameters":[{"name":"executionContextId","description":"Specifies in which execution context to lookup global scope variables.","optional":true,"$ref":"ExecutionContextId"}],"returns":[{"name":"names","type":"array","items":{"type":"string"}}]},{"name":"queryObjects","parameters":[{"name":"prototypeObjectId","description":"Identifier of the prototype to return objects for.","$ref":"RemoteObjectId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release the results.","optional":true,"type":"string"}],"returns":[{"name":"objects","description":"Array with objects.","$ref":"RemoteObject"}]},{"name":"releaseObject","description":"Releases remote object with given id.","parameters":[{"name":"objectId","description":"Identifier of the object to release.","$ref":"RemoteObjectId"}]},{"name":"releaseObjectGroup","description":"Releases all remote objects that belong to a given group.","parameters":[{"name":"objectGroup","description":"Symbolic object group name.","type":"string"}]},{"name":"runIfWaitingForDebugger","description":"Tells inspected instance to run if it was waiting for debugger to attach."},{"name":"runScript","description":"Runs script with given id in a given context.","parameters":[{"name":"scriptId","description":"Id of the script to run.","$ref":"ScriptId"},{"name":"executionContextId","description":"Specifies in which execution context to perform script run. If the parameter is omitted the\\nevaluation will be performed in the context of the inspected page.","optional":true,"$ref":"ExecutionContextId"},{"name":"objectGroup","description":"Symbolic group name that can be used to release multiple objects.","optional":true,"type":"string"},{"name":"silent","description":"In silent mode exceptions thrown during evaluation are not reported and do not pause\\nexecution. Overrides `setPauseOnException` state.","optional":true,"type":"boolean"},{"name":"includeCommandLineAPI","description":"Determines whether Command Line API should be available during the evaluation.","optional":true,"type":"boolean"},{"name":"returnByValue","description":"Whether the result is expected to be a JSON object which should be sent by value.","optional":true,"type":"boolean"},{"name":"generatePreview","description":"Whether preview should be generated for the result.","optional":true,"type":"boolean"},{"name":"awaitPromise","description":"Whether execution should `await` for resulting value and return once awaited promise is\\nresolved.","optional":true,"type":"boolean"}],"returns":[{"name":"result","description":"Run result.","$ref":"RemoteObject"},{"name":"exceptionDetails","description":"Exception details.","optional":true,"$ref":"ExceptionDetails"}]},{"name":"setAsyncCallStackDepth","description":"Enables or disables async call stacks tracking.","redirect":"Debugger","parameters":[{"name":"maxDepth","description":"Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\\ncall stacks (default).","type":"integer"}]},{"name":"setCustomObjectFormatterEnabled","experimental":true,"parameters":[{"name":"enabled","type":"boolean"}]},{"name":"setMaxCallStackSizeToCapture","experimental":true,"parameters":[{"name":"size","type":"integer"}]},{"name":"terminateExecution","description":"Terminate current or next JavaScript execution.\\nWill cancel the termination when the outer-most script execution ends.","experimental":true},{"name":"addBinding","description":"If executionContextId is empty, adds binding with the given name on the\\nglobal objects of all inspected contexts, including those created later,\\nbindings survive reloads.\\nBinding function takes exactly one argument, this argument should be string,\\nin case of any other input, function throws an exception.\\nEach binding function call produces Runtime.bindingCalled notification.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"executionContextId","description":"If specified, the binding would only be exposed to the specified\\nexecution context. If omitted and `executionContextName` is not set,\\nthe binding is exposed to all execution contexts of the target.\\nThis parameter is mutually exclusive with `executionContextName`.\\nDeprecated in favor of `executionContextName` due to an unclear use case\\nand bugs in implementation (crbug.com/1169639). `executionContextId` will be\\nremoved in the future.","deprecated":true,"optional":true,"$ref":"ExecutionContextId"},{"name":"executionContextName","description":"If specified, the binding is exposed to the executionContext with\\nmatching name, even for contexts created after the binding is added.\\nSee also `ExecutionContext.name` and `worldName` parameter to\\n`Page.addScriptToEvaluateOnNewDocument`.\\nThis parameter is mutually exclusive with `executionContextId`.","experimental":true,"optional":true,"type":"string"}]},{"name":"removeBinding","description":"This method does not remove binding function from global object but\\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.","experimental":true,"parameters":[{"name":"name","type":"string"}]},{"name":"getExceptionDetails","description":"This method tries to lookup and populate exception details for a\\nJavaScript Error object.\\nNote that the stackTrace portion of the resulting exceptionDetails will\\nonly be populated if the Runtime domain was enabled at the time when the\\nError was thrown.","experimental":true,"parameters":[{"name":"errorObjectId","description":"The error object for which to resolve the exception details.","$ref":"RemoteObjectId"}],"returns":[{"name":"exceptionDetails","optional":true,"$ref":"ExceptionDetails"}]}],"events":[{"name":"bindingCalled","description":"Notification is issued every time when binding is called.","experimental":true,"parameters":[{"name":"name","type":"string"},{"name":"payload","type":"string"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"}]},{"name":"consoleAPICalled","description":"Issued when console API was called.","parameters":[{"name":"type","description":"Type of the call.","type":"string","enum":["log","debug","info","error","warning","dir","dirxml","table","trace","clear","startGroup","startGroupCollapsed","endGroup","assert","profile","profileEnd","count","timeEnd"]},{"name":"args","description":"Call arguments.","type":"array","items":{"$ref":"RemoteObject"}},{"name":"executionContextId","description":"Identifier of the context where the call was made.","$ref":"ExecutionContextId"},{"name":"timestamp","description":"Call timestamp.","$ref":"Timestamp"},{"name":"stackTrace","description":"Stack trace captured when the call was made. The async stack chain is automatically reported for\\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.","optional":true,"$ref":"StackTrace"},{"name":"context","description":"Console context descriptor for calls on non-default console context (not console.*):\\n\'anonymous#unique-logger-id\' for call on unnamed context, \'name#unique-logger-id\' for call\\non named context.","experimental":true,"optional":true,"type":"string"}]},{"name":"exceptionRevoked","description":"Issued when unhandled exception was revoked.","parameters":[{"name":"reason","description":"Reason describing why exception was revoked.","type":"string"},{"name":"exceptionId","description":"The id of revoked exception, as reported in `exceptionThrown`.","type":"integer"}]},{"name":"exceptionThrown","description":"Issued when exception was thrown and unhandled.","parameters":[{"name":"timestamp","description":"Timestamp of the exception.","$ref":"Timestamp"},{"name":"exceptionDetails","$ref":"ExceptionDetails"}]},{"name":"executionContextCreated","description":"Issued when new execution context is created.","parameters":[{"name":"context","description":"A newly created execution context.","$ref":"ExecutionContextDescription"}]},{"name":"executionContextDestroyed","description":"Issued when execution context is destroyed.","parameters":[{"name":"executionContextId","description":"Id of the destroyed context","deprecated":true,"$ref":"ExecutionContextId"},{"name":"executionContextUniqueId","description":"Unique Id of the destroyed context","experimental":true,"type":"string"}]},{"name":"executionContextsCleared","description":"Issued when all executionContexts were cleared in browser"},{"name":"inspectRequested","description":"Issued when object should be inspected (for example, as a result of inspect() command line API\\ncall).","parameters":[{"name":"object","$ref":"RemoteObject"},{"name":"hints","type":"object"},{"name":"executionContextId","description":"Identifier of the context where the call was made.","experimental":true,"optional":true,"$ref":"ExecutionContextId"}]}]},{"domain":"Schema","description":"This domain is deprecated.","deprecated":true,"types":[{"id":"Domain","description":"Description of the protocol domain.","type":"object","properties":[{"name":"name","description":"Domain name.","type":"string"},{"name":"version","description":"Domain version.","type":"string"}]}],"commands":[{"name":"getDomains","description":"Returns supported domains.","returns":[{"name":"domains","description":"List of supported domains.","type":"array","items":{"$ref":"Domain"}}]}]}]}')}},t={};function r(n){var i=t[n];if(void 0!==i)return i.exports;var o=t[n]={id:n,loaded:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.loaded=!0,o.exports}r.g=function(){if("object"==typeof globalThis)return globalThis;try{return this||new Function("return this")()}catch(e){if("object"==typeof window)return window}}(),r.nmd=e=>(e.paths=[],e.children||(e.children=[]),e),r(6124);var n=r(6010);module.exports.CDP=n})(); \ No newline at end of file
diff --git a/node_modules/chrome-remote-interface/index.js b/node_modules/chrome-remote-interface/index.js
new file mode 100644
index 0000000..3a32c89
--- /dev/null
+++ b/node_modules/chrome-remote-interface/index.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const EventEmitter = require('events');
+const dns = require('dns');
+
+const devtools = require('./lib/devtools.js');
+const Chrome = require('./lib/chrome.js');
+
+// XXX reset the default that has been changed in
+// (https://github.com/nodejs/node/pull/39987) to prefer IPv4. since
+// implementations alway bind on 127.0.0.1 this solution should be fairly safe
+// (see #467)
+if (dns.setDefaultResultOrder) {
+ dns.setDefaultResultOrder('ipv4first');
+}
+
+function CDP(options, callback) {
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ const notifier = new EventEmitter();
+ if (typeof callback === 'function') {
+ // allow to register the error callback later
+ process.nextTick(() => {
+ new Chrome(options, notifier);
+ });
+ return notifier.once('connect', callback);
+ } else {
+ return new Promise((fulfill, reject) => {
+ notifier.once('connect', fulfill);
+ notifier.once('error', reject);
+ new Chrome(options, notifier);
+ });
+ }
+}
+
+module.exports = CDP;
+module.exports.Protocol = devtools.Protocol;
+module.exports.List = devtools.List;
+module.exports.New = devtools.New;
+module.exports.Activate = devtools.Activate;
+module.exports.Close = devtools.Close;
+module.exports.Version = devtools.Version;
diff --git a/node_modules/chrome-remote-interface/lib/api.js b/node_modules/chrome-remote-interface/lib/api.js
new file mode 100644
index 0000000..03fc75d
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/api.js
@@ -0,0 +1,92 @@
+'use strict';
+
+function arrayToObject(parameters) {
+ const keyValue = {};
+ parameters.forEach((parameter) =>{
+ const name = parameter.name;
+ delete parameter.name;
+ keyValue[name] = parameter;
+ });
+ return keyValue;
+}
+
+function decorate(to, category, object) {
+ to.category = category;
+ Object.keys(object).forEach((field) => {
+ // skip the 'name' field as it is part of the function prototype
+ if (field === 'name') {
+ return;
+ }
+ // commands and events have parameters whereas types have properties
+ if (category === 'type' && field === 'properties' ||
+ field === 'parameters') {
+ to[field] = arrayToObject(object[field]);
+ } else {
+ to[field] = object[field];
+ }
+ });
+}
+
+function addCommand(chrome, domainName, command) {
+ const commandName = `${domainName}.${command.name}`;
+ const handler = (params, sessionId, callback) => {
+ return chrome.send(commandName, params, sessionId, callback);
+ };
+ decorate(handler, 'command', command);
+ chrome[commandName] = chrome[domainName][command.name] = handler;
+}
+
+function addEvent(chrome, domainName, event) {
+ const eventName = `${domainName}.${event.name}`;
+ const handler = (sessionId, handler) => {
+ if (typeof sessionId === 'function') {
+ handler = sessionId;
+ sessionId = undefined;
+ }
+ const rawEventName = sessionId ? `${eventName}.${sessionId}` : eventName;
+ if (typeof handler === 'function') {
+ chrome.on(rawEventName, handler);
+ return () => chrome.removeListener(rawEventName, handler);
+ } else {
+ return new Promise((fulfill, reject) => {
+ chrome.once(rawEventName, fulfill);
+ });
+ }
+ };
+ decorate(handler, 'event', event);
+ chrome[eventName] = chrome[domainName][event.name] = handler;
+}
+
+function addType(chrome, domainName, type) {
+ const typeName = `${domainName}.${type.id}`;
+ const help = {};
+ decorate(help, 'type', type);
+ chrome[typeName] = chrome[domainName][type.id] = help;
+}
+
+function prepare(object, protocol) {
+ // assign the protocol and generate the shorthands
+ object.protocol = protocol;
+ protocol.domains.forEach((domain) => {
+ const domainName = domain.domain;
+ object[domainName] = {};
+ // add commands
+ (domain.commands || []).forEach((command) => {
+ addCommand(object, domainName, command);
+ });
+ // add events
+ (domain.events || []).forEach((event) => {
+ addEvent(object, domainName, event);
+ });
+ // add types
+ (domain.types || []).forEach((type) => {
+ addType(object, domainName, type);
+ });
+ // add utility listener for each domain
+ object[domainName].on = (eventName, handler) => {
+ return object[domainName][eventName](handler);
+ };
+ });
+}
+
+module.exports.prepare = prepare;
diff --git a/node_modules/chrome-remote-interface/lib/chrome.js b/node_modules/chrome-remote-interface/lib/chrome.js
new file mode 100644
index 0000000..b093d1e
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/chrome.js
@@ -0,0 +1,314 @@
+'use strict';
+
+const EventEmitter = require('events');
+const util = require('util');
+const formatUrl = require('url').format;
+const parseUrl = require('url').parse;
+
+const WebSocket = require('ws');
+
+const api = require('./api.js');
+const defaults = require('./defaults.js');
+const devtools = require('./devtools.js');
+
+class ProtocolError extends Error {
+ constructor(request, response) {
+ let {message} = response;
+ if (response.data) {
+ message += ` (${response.data})`;
+ }
+ super(message);
+ // attach the original response as well
+ this.request = request;
+ this.response = response;
+ }
+}
+
+class Chrome extends EventEmitter {
+ constructor(options, notifier) {
+ super();
+ // options
+ const defaultTarget = (targets) => {
+ // prefer type = 'page' inspectable targets as they represents
+ // browser tabs (fall back to the first inspectable target
+ // otherwise)
+ let backup;
+ let target = targets.find((target) => {
+ if (target.webSocketDebuggerUrl) {
+ backup = backup || target;
+ return target.type === 'page';
+ } else {
+ return false;
+ }
+ });
+ target = target || backup;
+ if (target) {
+ return target;
+ } else {
+ throw new Error('No inspectable targets');
+ }
+ };
+ options = options || {};
+ this.host = options.host || defaults.HOST;
+ this.port = options.port || defaults.PORT;
+ this.secure = !!(options.secure);
+ this.useHostName = !!(options.useHostName);
+ this.alterPath = options.alterPath || ((path) => path);
+ this.protocol = options.protocol;
+ this.local = !!(options.local);
+ this.target = options.target || defaultTarget;
+ // locals
+ this._notifier = notifier;
+ this._callbacks = {};
+ this._nextCommandId = 1;
+ // properties
+ this.webSocketUrl = undefined;
+ // operations
+ this._start();
+ }
+
+ // avoid misinterpreting protocol's members as custom util.inspect functions
+ inspect(depth, options) {
+ options.customInspect = false;
+ return util.inspect(this, options);
+ }
+
+ send(method, params, sessionId, callback) {
+ // handle optional arguments
+ const optionals = Array.from(arguments).slice(1);
+ params = optionals.find(x => typeof x === 'object');
+ sessionId = optionals.find(x => typeof x === 'string');
+ callback = optionals.find(x => typeof x === 'function');
+ // return a promise when a callback is not provided
+ if (typeof callback === 'function') {
+ this._enqueueCommand(method, params, sessionId, callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ this._enqueueCommand(method, params, sessionId, (error, response) => {
+ if (error) {
+ const request = {method, params, sessionId};
+ reject(
+ error instanceof Error
+ ? error // low-level WebSocket error
+ : new ProtocolError(request, response)
+ );
+ } else {
+ fulfill(response);
+ }
+ });
+ });
+ }
+ }
+
+ close(callback) {
+ const closeWebSocket = (callback) => {
+ // don't close if it's already closed
+ if (this._ws.readyState === 3) {
+ callback();
+ } else {
+ // don't notify on user-initiated shutdown ('disconnect' event)
+ this._ws.removeAllListeners('close');
+ this._ws.once('close', () => {
+ this._ws.removeAllListeners();
+ this._handleConnectionClose();
+ callback();
+ });
+ this._ws.close();
+ }
+ };
+ if (typeof callback === 'function') {
+ closeWebSocket(callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ closeWebSocket(fulfill);
+ });
+ }
+ }
+
+ // initiate the connection process
+ async _start() {
+ const options = {
+ host: this.host,
+ port: this.port,
+ secure: this.secure,
+ useHostName: this.useHostName,
+ alterPath: this.alterPath
+ };
+ try {
+ // fetch the WebSocket debugger URL
+ const url = await this._fetchDebuggerURL(options);
+ // allow the user to alter the URL
+ const urlObject = parseUrl(url);
+ urlObject.pathname = options.alterPath(urlObject.pathname);
+ this.webSocketUrl = formatUrl(urlObject);
+ // update the connection parameters using the debugging URL
+ options.host = urlObject.hostname;
+ options.port = urlObject.port || options.port;
+ // fetch the protocol and prepare the API
+ const protocol = await this._fetchProtocol(options);
+ api.prepare(this, protocol);
+ // finally connect to the WebSocket
+ await this._connectToWebSocket();
+ // since the handler is executed synchronously, the emit() must be
+ // performed in the next tick so that uncaught errors in the client code
+ // are not intercepted by the Promise mechanism and therefore reported
+ // via the 'error' event
+ process.nextTick(() => {
+ this._notifier.emit('connect', this);
+ });
+ } catch (err) {
+ this._notifier.emit('error', err);
+ }
+ }
+
+ // fetch the WebSocket URL according to 'target'
+ async _fetchDebuggerURL(options) {
+ const userTarget = this.target;
+ switch (typeof userTarget) {
+ case 'string': {
+ let idOrUrl = userTarget;
+ // use default host and port if omitted (and a relative URL is specified)
+ if (idOrUrl.startsWith('/')) {
+ idOrUrl = `ws://${this.host}:${this.port}${idOrUrl}`;
+ }
+ // a WebSocket URL is specified by the user (e.g., node-inspector)
+ if (idOrUrl.match(/^wss?:/i)) {
+ return idOrUrl; // done!
+ }
+ // a target id is specified by the user
+ else {
+ const targets = await devtools.List(options);
+ const object = targets.find((target) => target.id === idOrUrl);
+ return object.webSocketDebuggerUrl;
+ }
+ }
+ case 'object': {
+ const object = userTarget;
+ return object.webSocketDebuggerUrl;
+ }
+ case 'function': {
+ const func = userTarget;
+ const targets = await devtools.List(options);
+ const result = func(targets);
+ const object = typeof result === 'number' ? targets[result] : result;
+ return object.webSocketDebuggerUrl;
+ }
+ default:
+ throw new Error(`Invalid target argument "${this.target}"`);
+ }
+ }
+
+ // fetch the protocol according to 'protocol' and 'local'
+ async _fetchProtocol(options) {
+ // if a protocol has been provided then use it
+ if (this.protocol) {
+ return this.protocol;
+ }
+ // otherwise user either the local or the remote version
+ else {
+ options.local = this.local;
+ return await devtools.Protocol(options);
+ }
+ }
+
+ // establish the WebSocket connection and start processing user commands
+ _connectToWebSocket() {
+ return new Promise((fulfill, reject) => {
+ // create the WebSocket
+ try {
+ if (this.secure) {
+ this.webSocketUrl = this.webSocketUrl.replace(/^ws:/i, 'wss:');
+ }
+ this._ws = new WebSocket(this.webSocketUrl, [], {
+ maxPayload: 256 * 1024 * 1024,
+ perMessageDeflate: false,
+ followRedirects: true,
+ });
+ } catch (err) {
+ // handles bad URLs
+ reject(err);
+ return;
+ }
+ // set up event handlers
+ this._ws.on('open', () => {
+ fulfill();
+ });
+ this._ws.on('message', (data) => {
+ const message = JSON.parse(data);
+ this._handleMessage(message);
+ });
+ this._ws.on('close', (code) => {
+ this._handleConnectionClose();
+ this.emit('disconnect');
+ });
+ this._ws.on('error', (err) => {
+ reject(err);
+ });
+ });
+ }
+
+ _handleConnectionClose() {
+ // make sure to complete all the unresolved callbacks
+ const err = new Error('WebSocket connection closed');
+ for (const callback of Object.values(this._callbacks)) {
+ callback(err);
+ }
+ this._callbacks = {};
+ }
+
+ // handle the messages read from the WebSocket
+ _handleMessage(message) {
+ // command response
+ if (message.id) {
+ const callback = this._callbacks[message.id];
+ if (!callback) {
+ return;
+ }
+ // interpret the lack of both 'error' and 'result' as success
+ // (this may happen with node-inspector)
+ if (message.error) {
+ callback(true, message.error);
+ } else {
+ callback(false, message.result || {});
+ }
+ // unregister command response callback
+ delete this._callbacks[message.id];
+ // notify when there are no more pending commands
+ if (Object.keys(this._callbacks).length === 0) {
+ this.emit('ready');
+ }
+ }
+ // event
+ else if (message.method) {
+ const {method, params, sessionId} = message;
+ this.emit('event', message);
+ this.emit(method, params, sessionId);
+ this.emit(`${method}.${sessionId}`, params, sessionId);
+ }
+ }
+
+ // send a command to the remote endpoint and register a callback for the reply
+ _enqueueCommand(method, params, sessionId, callback) {
+ const id = this._nextCommandId++;
+ const message = {
+ id,
+ method,
+ sessionId,
+ params: params || {}
+ };
+ this._ws.send(JSON.stringify(message), (err) => {
+ if (err) {
+ // handle low-level WebSocket errors
+ if (typeof callback === 'function') {
+ callback(err);
+ }
+ } else {
+ this._callbacks[id] = callback;
+ }
+ });
+ }
+}
+
+module.exports = Chrome;
diff --git a/node_modules/chrome-remote-interface/lib/defaults.js b/node_modules/chrome-remote-interface/lib/defaults.js
new file mode 100644
index 0000000..18778f0
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/defaults.js
@@ -0,0 +1,4 @@
+'use strict';
+
+module.exports.HOST = 'localhost';
+module.exports.PORT = 9222;
diff --git a/node_modules/chrome-remote-interface/lib/devtools.js b/node_modules/chrome-remote-interface/lib/devtools.js
new file mode 100644
index 0000000..3975a00
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/devtools.js
@@ -0,0 +1,127 @@
+'use strict';
+
+const http = require('http');
+const https = require('https');
+
+const defaults = require('./defaults.js');
+const externalRequest = require('./external-request.js');
+
+// options.path must be specified; callback(err, data)
+function devToolsInterface(path, options, callback) {
+ const transport = options.secure ? https : http;
+ const requestOptions = {
+ method: options.method,
+ host: options.host || defaults.HOST,
+ port: options.port || defaults.PORT,
+ useHostName: options.useHostName,
+ path: (options.alterPath ? options.alterPath(path) : path)
+ };
+ externalRequest(transport, requestOptions, callback);
+}
+
+// wrapper that allows to return a promise if the callback is omitted, it works
+// for DevTools methods
+function promisesWrapper(func) {
+ return (options, callback) => {
+ // options is an optional argument
+ if (typeof options === 'function') {
+ callback = options;
+ options = undefined;
+ }
+ options = options || {};
+ // just call the function otherwise wrap a promise around its execution
+ if (typeof callback === 'function') {
+ func(options, callback);
+ return undefined;
+ } else {
+ return new Promise((fulfill, reject) => {
+ func(options, (err, result) => {
+ if (err) {
+ reject(err);
+ } else {
+ fulfill(result);
+ }
+ });
+ });
+ }
+ };
+}
+
+function Protocol(options, callback) {
+ // if the local protocol is requested
+ if (options.local) {
+ const localDescriptor = require('./protocol.json');
+ callback(null, localDescriptor);
+ return;
+ }
+ // try to fetch the protocol remotely
+ devToolsInterface('/json/protocol', options, (err, descriptor) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(descriptor));
+ }
+ });
+}
+
+function List(options, callback) {
+ devToolsInterface('/json/list', options, (err, tabs) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(tabs));
+ }
+ });
+}
+
+function New(options, callback) {
+ let path = '/json/new';
+ if (Object.prototype.hasOwnProperty.call(options, 'url')) {
+ path += `?${options.url}`;
+ }
+ options.method = options.method || 'PUT'; // see #497
+ devToolsInterface(path, options, (err, tab) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(tab));
+ }
+ });
+}
+
+function Activate(options, callback) {
+ devToolsInterface('/json/activate/' + options.id, options, (err) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null);
+ }
+ });
+}
+
+function Close(options, callback) {
+ devToolsInterface('/json/close/' + options.id, options, (err) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null);
+ }
+ });
+}
+
+function Version(options, callback) {
+ devToolsInterface('/json/version', options, (err, versionInfo) => {
+ if (err) {
+ callback(err);
+ } else {
+ callback(null, JSON.parse(versionInfo));
+ }
+ });
+}
+
+module.exports.Protocol = promisesWrapper(Protocol);
+module.exports.List = promisesWrapper(List);
+module.exports.New = promisesWrapper(New);
+module.exports.Activate = promisesWrapper(Activate);
+module.exports.Close = promisesWrapper(Close);
+module.exports.Version = promisesWrapper(Version);
diff --git a/node_modules/chrome-remote-interface/lib/external-request.js b/node_modules/chrome-remote-interface/lib/external-request.js
new file mode 100644
index 0000000..656446e
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/external-request.js
@@ -0,0 +1,44 @@
+'use strict';
+
+const dns = require('dns');
+const util = require('util');
+
+const REQUEST_TIMEOUT = 10000;
+
+// callback(err, data)
+async function externalRequest(transport, options, callback) {
+ // perform the DNS lookup manually so that the HTTP host header generated by
+ // http.get will contain the IP address, this is needed because since Chrome
+ // 66 the host header cannot contain an host name different than localhost
+ // (see https://github.com/cyrus-and/chrome-remote-interface/issues/340)
+ if (!options.useHostName) {
+ try {
+ const {address} = await util.promisify(dns.lookup)(options.host);
+ options.host = address;
+ } catch (err) {
+ callback(err);
+ return;
+ }
+ }
+ // perform the actual request
+ const request = transport.request(options, (response) => {
+ let data = '';
+ response.on('data', (chunk) => {
+ data += chunk;
+ });
+ response.on('end', () => {
+ if (response.statusCode === 200) {
+ callback(null, data);
+ } else {
+ callback(new Error(data));
+ }
+ });
+ });
+ request.setTimeout(REQUEST_TIMEOUT, () => {
+ request.abort();
+ });
+ request.on('error', callback);
+ request.end();
+}
+
+module.exports = externalRequest;
diff --git a/node_modules/chrome-remote-interface/lib/protocol.json b/node_modules/chrome-remote-interface/lib/protocol.json
new file mode 100644
index 0000000..896a371
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/protocol.json
@@ -0,0 +1,27862 @@
+{
+ "version": {
+ "major": "1",
+ "minor": "3"
+ },
+ "domains": [
+ {
+ "domain": "Accessibility",
+ "experimental": true,
+ "dependencies": [
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "AXNodeId",
+ "description": "Unique accessibility node identifier.",
+ "type": "string"
+ },
+ {
+ "id": "AXValueType",
+ "description": "Enum of possible property types.",
+ "type": "string",
+ "enum": [
+ "boolean",
+ "tristate",
+ "booleanOrUndefined",
+ "idref",
+ "idrefList",
+ "integer",
+ "node",
+ "nodeList",
+ "number",
+ "string",
+ "computedString",
+ "token",
+ "tokenList",
+ "domRelation",
+ "role",
+ "internalRole",
+ "valueUndefined"
+ ]
+ },
+ {
+ "id": "AXValueSourceType",
+ "description": "Enum of possible property sources.",
+ "type": "string",
+ "enum": [
+ "attribute",
+ "implicit",
+ "style",
+ "contents",
+ "placeholder",
+ "relatedElement"
+ ]
+ },
+ {
+ "id": "AXValueNativeSourceType",
+ "description": "Enum of possible native property sources (as a subtype of a particular AXValueSourceType).",
+ "type": "string",
+ "enum": [
+ "description",
+ "figcaption",
+ "label",
+ "labelfor",
+ "labelwrapped",
+ "legend",
+ "rubyannotation",
+ "tablecaption",
+ "title",
+ "other"
+ ]
+ },
+ {
+ "id": "AXValueSource",
+ "description": "A single source for a computed AX property.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "What type of source this is.",
+ "$ref": "AXValueSourceType"
+ },
+ {
+ "name": "value",
+ "description": "The value of this property source.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "attribute",
+ "description": "The name of the relevant attribute, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "attributeValue",
+ "description": "The value of the relevant attribute, if any.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "superseded",
+ "description": "Whether this source is superseded by a higher priority source.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "nativeSource",
+ "description": "The native markup source for this value, e.g. a `<label>` element.",
+ "optional": true,
+ "$ref": "AXValueNativeSourceType"
+ },
+ {
+ "name": "nativeSourceValue",
+ "description": "The value, such as a node or node list, of the native source.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "invalid",
+ "description": "Whether the value for this property is invalid.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "invalidReason",
+ "description": "Reason for the value being invalid, if it is.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AXRelatedNode",
+ "type": "object",
+ "properties": [
+ {
+ "name": "backendDOMNodeId",
+ "description": "The BackendNodeId of the related DOM node.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "idref",
+ "description": "The IDRef value provided, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "text",
+ "description": "The text alternative of this node in the current context.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AXProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The name of this property.",
+ "$ref": "AXPropertyName"
+ },
+ {
+ "name": "value",
+ "description": "The value of this property.",
+ "$ref": "AXValue"
+ }
+ ]
+ },
+ {
+ "id": "AXValue",
+ "description": "A single computed AX property.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "The type of this value.",
+ "$ref": "AXValueType"
+ },
+ {
+ "name": "value",
+ "description": "The computed value of this property.",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "relatedNodes",
+ "description": "One or more related nodes, if applicable.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXRelatedNode"
+ }
+ },
+ {
+ "name": "sources",
+ "description": "The sources which contributed to the computation of this property.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXValueSource"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AXPropertyName",
+ "description": "Values of AXProperty name:\n- from 'busy' to 'roledescription': states which apply to every AX node\n- from 'live' to 'root': attributes which apply to nodes in live regions\n- from 'autocomplete' to 'valuetext': attributes which apply to widgets\n- from 'checked' to 'selected': states which apply to widgets\n- from 'activedescendant' to 'owns' - relationships between elements other than parent/child/sibling.",
+ "type": "string",
+ "enum": [
+ "busy",
+ "disabled",
+ "editable",
+ "focusable",
+ "focused",
+ "hidden",
+ "hiddenRoot",
+ "invalid",
+ "keyshortcuts",
+ "settable",
+ "roledescription",
+ "live",
+ "atomic",
+ "relevant",
+ "root",
+ "autocomplete",
+ "hasPopup",
+ "level",
+ "multiselectable",
+ "orientation",
+ "multiline",
+ "readonly",
+ "required",
+ "valuemin",
+ "valuemax",
+ "valuetext",
+ "checked",
+ "expanded",
+ "modal",
+ "pressed",
+ "selected",
+ "activedescendant",
+ "controls",
+ "describedby",
+ "details",
+ "errormessage",
+ "flowto",
+ "labelledby",
+ "owns"
+ ]
+ },
+ {
+ "id": "AXNode",
+ "description": "A node in the accessibility tree.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "description": "Unique identifier for this node.",
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "ignored",
+ "description": "Whether this node is ignored for accessibility",
+ "type": "boolean"
+ },
+ {
+ "name": "ignoredReasons",
+ "description": "Collection of reasons why this node is hidden.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXProperty"
+ }
+ },
+ {
+ "name": "role",
+ "description": "This `Node`'s role, whether explicit or implicit.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "chromeRole",
+ "description": "This `Node`'s Chrome raw role.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "name",
+ "description": "The accessible name for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "description",
+ "description": "The accessible description for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "value",
+ "description": "The value for this `Node`.",
+ "optional": true,
+ "$ref": "AXValue"
+ },
+ {
+ "name": "properties",
+ "description": "All other properties",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXProperty"
+ }
+ },
+ {
+ "name": "parentId",
+ "description": "ID for this node's parent.",
+ "optional": true,
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "childIds",
+ "description": "IDs for each of this node's child nodes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AXNodeId"
+ }
+ },
+ {
+ "name": "backendDOMNodeId",
+ "description": "The backend ID for the associated DOM node, if any.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame ID for the frame associated with this nodes document.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables the accessibility domain."
+ },
+ {
+ "name": "enable",
+ "description": "Enables the accessibility domain which causes `AXNodeId`s to remain consistent between method calls.\nThis turns on accessibility for the page, which can impact performance until accessibility is disabled."
+ },
+ {
+ "name": "getPartialAXTree",
+ "description": "Fetches the accessibility node and partial accessibility tree for this DOM node, if it exists.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper to get the partial accessibility tree for.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "fetchRelatives",
+ "description": "Whether to fetch this node's ancestors, siblings and children. Defaults to true.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "The `Accessibility.AXNode` for this DOM node, if it exists, plus its ancestors, siblings and\nchildren, if requested.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getFullAXTree",
+ "description": "Fetches the entire accessibility tree for the root Document",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which descendants of the root node should be retrieved.\nIf omitted, the full tree is returned.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame for whose document the AX tree should be retrieved.\nIf omited, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getRootAXNode",
+ "description": "Fetches the root node.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "The frame in whose document the node resides.\nIf omitted, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "node",
+ "$ref": "AXNode"
+ }
+ ]
+ },
+ {
+ "name": "getAXNodeAndAncestors",
+ "description": "Fetches a node and all ancestors up to and including the root.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to get.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to get.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper to get.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getChildAXNodes",
+ "description": "Fetches a particular accessibility node by AXNodeId.\nRequires `enable()` to have been called previously.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "AXNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame in whose document the node resides.\nIf omitted, the root frame is used.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "queryAXTree",
+ "description": "Query a DOM node's accessibility subtree for accessible name and role.\nThis command computes the name and role for all nodes in the subtree, including those that are\nignored for accessibility, and returns those that mactch the specified name and role. If no DOM\nnode is specified, or the DOM node does not exist, the command returns an error. If neither\n`accessibleName` or `role` is specified, it returns all the accessibility nodes in the subtree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node for the root to query.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node for the root to query.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper for the root to query.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "accessibleName",
+ "description": "Find nodes with this computed name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "role",
+ "description": "Find nodes with this computed role.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "A list of `Accessibility.AXNode` matching the specified attributes,\nincluding nodes that are ignored for accessibility.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "loadComplete",
+ "description": "The loadComplete event mirrors the load complete event sent by the browser to assistive\ntechnology when the web page has finished loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "root",
+ "description": "New document root node.",
+ "$ref": "AXNode"
+ }
+ ]
+ },
+ {
+ "name": "nodesUpdated",
+ "description": "The nodesUpdated event is sent every time a previously requested node has changed the in tree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodes",
+ "description": "Updated node data.",
+ "type": "array",
+ "items": {
+ "$ref": "AXNode"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Animation",
+ "experimental": true,
+ "dependencies": [
+ "Runtime",
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "Animation",
+ "description": "Animation instance.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "`Animation`'s id.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "`Animation`'s name.",
+ "type": "string"
+ },
+ {
+ "name": "pausedState",
+ "description": "`Animation`'s internal paused state.",
+ "type": "boolean"
+ },
+ {
+ "name": "playState",
+ "description": "`Animation`'s play state.",
+ "type": "string"
+ },
+ {
+ "name": "playbackRate",
+ "description": "`Animation`'s playback rate.",
+ "type": "number"
+ },
+ {
+ "name": "startTime",
+ "description": "`Animation`'s start time.",
+ "type": "number"
+ },
+ {
+ "name": "currentTime",
+ "description": "`Animation`'s current time.",
+ "type": "number"
+ },
+ {
+ "name": "type",
+ "description": "Animation type of `Animation`.",
+ "type": "string",
+ "enum": [
+ "CSSTransition",
+ "CSSAnimation",
+ "WebAnimation"
+ ]
+ },
+ {
+ "name": "source",
+ "description": "`Animation`'s source animation node.",
+ "optional": true,
+ "$ref": "AnimationEffect"
+ },
+ {
+ "name": "cssId",
+ "description": "A unique ID for `Animation` representing the sources that triggered this CSS\nanimation/transition.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AnimationEffect",
+ "description": "AnimationEffect instance",
+ "type": "object",
+ "properties": [
+ {
+ "name": "delay",
+ "description": "`AnimationEffect`'s delay.",
+ "type": "number"
+ },
+ {
+ "name": "endDelay",
+ "description": "`AnimationEffect`'s end delay.",
+ "type": "number"
+ },
+ {
+ "name": "iterationStart",
+ "description": "`AnimationEffect`'s iteration start.",
+ "type": "number"
+ },
+ {
+ "name": "iterations",
+ "description": "`AnimationEffect`'s iterations.",
+ "type": "number"
+ },
+ {
+ "name": "duration",
+ "description": "`AnimationEffect`'s iteration duration.",
+ "type": "number"
+ },
+ {
+ "name": "direction",
+ "description": "`AnimationEffect`'s playback direction.",
+ "type": "string"
+ },
+ {
+ "name": "fill",
+ "description": "`AnimationEffect`'s fill mode.",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`AnimationEffect`'s target node.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "keyframesRule",
+ "description": "`AnimationEffect`'s keyframes.",
+ "optional": true,
+ "$ref": "KeyframesRule"
+ },
+ {
+ "name": "easing",
+ "description": "`AnimationEffect`'s timing function.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "KeyframesRule",
+ "description": "Keyframes Rule",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "CSS keyframed animation's name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "keyframes",
+ "description": "List of animation keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "KeyframeStyle"
+ }
+ }
+ ]
+ },
+ {
+ "id": "KeyframeStyle",
+ "description": "Keyframe Style",
+ "type": "object",
+ "properties": [
+ {
+ "name": "offset",
+ "description": "Keyframe's time offset.",
+ "type": "string"
+ },
+ {
+ "name": "easing",
+ "description": "`AnimationEffect`'s timing function.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables animation domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables animation domain notifications."
+ },
+ {
+ "name": "getCurrentTime",
+ "description": "Returns the current time of the an animation.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of animation.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "currentTime",
+ "description": "Current time of the page.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "getPlaybackRate",
+ "description": "Gets the playback rate of the document timeline.",
+ "returns": [
+ {
+ "name": "playbackRate",
+ "description": "Playback rate for animations on page.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "releaseAnimations",
+ "description": "Releases a set of animations to no longer be manipulated.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "List of animation ids to seek.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "resolveAnimation",
+ "description": "Gets the remote object of the Animation.",
+ "parameters": [
+ {
+ "name": "animationId",
+ "description": "Animation id.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "remoteObject",
+ "description": "Corresponding remote object.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "seekAnimations",
+ "description": "Seek a set of animations to a particular time within each animation.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "List of animation ids to seek.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "currentTime",
+ "description": "Set the current time of each animation.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setPaused",
+ "description": "Sets the paused state of a set of animations.",
+ "parameters": [
+ {
+ "name": "animations",
+ "description": "Animations to set the pause state of.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "paused",
+ "description": "Paused state to set to.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPlaybackRate",
+ "description": "Sets the playback rate of the document timeline.",
+ "parameters": [
+ {
+ "name": "playbackRate",
+ "description": "Playback rate for animations on page",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setTiming",
+ "description": "Sets the timing of an animation node.",
+ "parameters": [
+ {
+ "name": "animationId",
+ "description": "Animation id.",
+ "type": "string"
+ },
+ {
+ "name": "duration",
+ "description": "Duration of the animation.",
+ "type": "number"
+ },
+ {
+ "name": "delay",
+ "description": "Delay of the animation.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "animationCanceled",
+ "description": "Event for when an animation has been cancelled.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of the animation that was cancelled.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "animationCreated",
+ "description": "Event for each animation that has been created.",
+ "parameters": [
+ {
+ "name": "id",
+ "description": "Id of the animation that was created.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "animationStarted",
+ "description": "Event for animation that has been started.",
+ "parameters": [
+ {
+ "name": "animation",
+ "description": "Animation that was started.",
+ "$ref": "Animation"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Audits",
+ "description": "Audits domain allows investigation of page violations and possible improvements.",
+ "experimental": true,
+ "dependencies": [
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "AffectedCookie",
+ "description": "Information about a cookie that is affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The following three properties uniquely identify a cookie",
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AffectedRequest",
+ "description": "Information about a request that is affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestId",
+ "description": "The unique request id.",
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "url",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AffectedFrame",
+ "description": "Information about the frame affected by an inspector issue.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ]
+ },
+ {
+ "id": "CookieExclusionReason",
+ "type": "string",
+ "enum": [
+ "ExcludeSameSiteUnspecifiedTreatedAsLax",
+ "ExcludeSameSiteNoneInsecure",
+ "ExcludeSameSiteLax",
+ "ExcludeSameSiteStrict",
+ "ExcludeInvalidSameParty",
+ "ExcludeSamePartyCrossPartyContext",
+ "ExcludeDomainNonASCII",
+ "ExcludeThirdPartyCookieBlockedInFirstPartySet"
+ ]
+ },
+ {
+ "id": "CookieWarningReason",
+ "type": "string",
+ "enum": [
+ "WarnSameSiteUnspecifiedCrossSiteContext",
+ "WarnSameSiteNoneInsecure",
+ "WarnSameSiteUnspecifiedLaxAllowUnsafe",
+ "WarnSameSiteStrictLaxDowngradeStrict",
+ "WarnSameSiteStrictCrossDowngradeStrict",
+ "WarnSameSiteStrictCrossDowngradeLax",
+ "WarnSameSiteLaxCrossDowngradeStrict",
+ "WarnSameSiteLaxCrossDowngradeLax",
+ "WarnAttributeValueExceedsMaxSize",
+ "WarnDomainNonASCII",
+ "WarnThirdPartyPhaseout"
+ ]
+ },
+ {
+ "id": "CookieOperation",
+ "type": "string",
+ "enum": [
+ "SetCookie",
+ "ReadCookie"
+ ]
+ },
+ {
+ "id": "CookieIssueDetails",
+ "description": "This information is currently necessary, as the front-end has a difficult\ntime finding a specific cookie. With this, we can convey specific error\ninformation without the cookie.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cookie",
+ "description": "If AffectedCookie is not set then rawCookieLine contains the raw\nSet-Cookie header string. This hints at a problem where the\ncookie line is syntactically or semantically malformed in a way\nthat no valid cookie could be created.",
+ "optional": true,
+ "$ref": "AffectedCookie"
+ },
+ {
+ "name": "rawCookieLine",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookieWarningReasons",
+ "type": "array",
+ "items": {
+ "$ref": "CookieWarningReason"
+ }
+ },
+ {
+ "name": "cookieExclusionReasons",
+ "type": "array",
+ "items": {
+ "$ref": "CookieExclusionReason"
+ }
+ },
+ {
+ "name": "operation",
+ "description": "Optionally identifies the site-for-cookies and the cookie url, which\nmay be used by the front-end as additional context.",
+ "$ref": "CookieOperation"
+ },
+ {
+ "name": "siteForCookies",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookieUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ }
+ ]
+ },
+ {
+ "id": "MixedContentResolutionStatus",
+ "type": "string",
+ "enum": [
+ "MixedContentBlocked",
+ "MixedContentAutomaticallyUpgraded",
+ "MixedContentWarning"
+ ]
+ },
+ {
+ "id": "MixedContentResourceType",
+ "type": "string",
+ "enum": [
+ "AttributionSrc",
+ "Audio",
+ "Beacon",
+ "CSPReport",
+ "Download",
+ "EventSource",
+ "Favicon",
+ "Font",
+ "Form",
+ "Frame",
+ "Image",
+ "Import",
+ "Manifest",
+ "Ping",
+ "PluginData",
+ "PluginResource",
+ "Prefetch",
+ "Resource",
+ "Script",
+ "ServiceWorker",
+ "SharedWorker",
+ "Stylesheet",
+ "Track",
+ "Video",
+ "Worker",
+ "XMLHttpRequest",
+ "XSLT"
+ ]
+ },
+ {
+ "id": "MixedContentIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resourceType",
+ "description": "The type of resource causing the mixed content issue (css, js, iframe,\nform,...). Marked as optional because it is mapped to from\nblink::mojom::RequestContextType, which will be replaced\nby network::mojom::RequestDestination",
+ "optional": true,
+ "$ref": "MixedContentResourceType"
+ },
+ {
+ "name": "resolutionStatus",
+ "description": "The way the mixed content issue is being resolved.",
+ "$ref": "MixedContentResolutionStatus"
+ },
+ {
+ "name": "insecureURL",
+ "description": "The unsafe http url causing the mixed content issue.",
+ "type": "string"
+ },
+ {
+ "name": "mainResourceURL",
+ "description": "The url responsible for the call to an unsafe url.",
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "description": "The mixed content request.\nDoes not always exist (e.g. for unsafe form submission urls).",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "frame",
+ "description": "Optional because not every mixed content issue is necessarily linked to a frame.",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ }
+ ]
+ },
+ {
+ "id": "BlockedByResponseReason",
+ "description": "Enum indicating the reason a response has been blocked. These reasons are\nrefinements of the net error BLOCKED_BY_RESPONSE.",
+ "type": "string",
+ "enum": [
+ "CoepFrameResourceNeedsCoepHeader",
+ "CoopSandboxedIFrameCannotNavigateToCoopPage",
+ "CorpNotSameOrigin",
+ "CorpNotSameOriginAfterDefaultedToSameOriginByCoep",
+ "CorpNotSameSite"
+ ]
+ },
+ {
+ "id": "BlockedByResponseIssueDetails",
+ "description": "Details for a request that has been blocked with the BLOCKED_BY_RESPONSE\ncode. Currently only used for COEP/COOP, but may be extended to include\nsome CSP errors in the future.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "request",
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "parentFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "blockedFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "reason",
+ "$ref": "BlockedByResponseReason"
+ }
+ ]
+ },
+ {
+ "id": "HeavyAdResolutionStatus",
+ "type": "string",
+ "enum": [
+ "HeavyAdBlocked",
+ "HeavyAdWarning"
+ ]
+ },
+ {
+ "id": "HeavyAdReason",
+ "type": "string",
+ "enum": [
+ "NetworkTotalLimit",
+ "CpuTotalLimit",
+ "CpuPeakLimit"
+ ]
+ },
+ {
+ "id": "HeavyAdIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resolution",
+ "description": "The resolution status, either blocking the content or warning.",
+ "$ref": "HeavyAdResolutionStatus"
+ },
+ {
+ "name": "reason",
+ "description": "The reason the ad was blocked, total network or cpu or peak cpu.",
+ "$ref": "HeavyAdReason"
+ },
+ {
+ "name": "frame",
+ "description": "The frame that was blocked.",
+ "$ref": "AffectedFrame"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyViolationType",
+ "type": "string",
+ "enum": [
+ "kInlineViolation",
+ "kEvalViolation",
+ "kURLViolation",
+ "kTrustedTypesSinkViolation",
+ "kTrustedTypesPolicyViolation",
+ "kWasmEvalViolation"
+ ]
+ },
+ {
+ "id": "SourceCodeLocation",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "optional": true,
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedURL",
+ "description": "The url not included in allowed sources.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "violatedDirective",
+ "description": "Specific directive that is violated, causing the CSP issue.",
+ "type": "string"
+ },
+ {
+ "name": "isReportOnly",
+ "type": "boolean"
+ },
+ {
+ "name": "contentSecurityPolicyViolationType",
+ "$ref": "ContentSecurityPolicyViolationType"
+ },
+ {
+ "name": "frameAncestor",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "sourceCodeLocation",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "SharedArrayBufferIssueType",
+ "type": "string",
+ "enum": [
+ "TransferIssue",
+ "CreationIssue"
+ ]
+ },
+ {
+ "id": "SharedArrayBufferIssueDetails",
+ "description": "Details for a issue arising from an SAB being instantiated in, or\ntransferred to a context that is not cross-origin isolated.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "isWarning",
+ "type": "boolean"
+ },
+ {
+ "name": "type",
+ "$ref": "SharedArrayBufferIssueType"
+ }
+ ]
+ },
+ {
+ "id": "LowTextContrastIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "violatingNodeId",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "violatingNodeSelector",
+ "type": "string"
+ },
+ {
+ "name": "contrastRatio",
+ "type": "number"
+ },
+ {
+ "name": "thresholdAA",
+ "type": "number"
+ },
+ {
+ "name": "thresholdAAA",
+ "type": "number"
+ },
+ {
+ "name": "fontSize",
+ "type": "string"
+ },
+ {
+ "name": "fontWeight",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CorsIssueDetails",
+ "description": "Details for a CORS related issue, e.g. a warning or error related to\nCORS RFC1918 enforcement.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "corsErrorStatus",
+ "$ref": "Network.CorsErrorStatus"
+ },
+ {
+ "name": "isWarning",
+ "type": "boolean"
+ },
+ {
+ "name": "request",
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "location",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "initiatorOrigin",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceIPAddressSpace",
+ "optional": true,
+ "$ref": "Network.IPAddressSpace"
+ },
+ {
+ "name": "clientSecurityState",
+ "optional": true,
+ "$ref": "Network.ClientSecurityState"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingIssueType",
+ "type": "string",
+ "enum": [
+ "PermissionPolicyDisabled",
+ "UntrustworthyReportingOrigin",
+ "InsecureContext",
+ "InvalidHeader",
+ "InvalidRegisterTriggerHeader",
+ "SourceAndTriggerHeaders",
+ "SourceIgnored",
+ "TriggerIgnored",
+ "OsSourceIgnored",
+ "OsTriggerIgnored",
+ "InvalidRegisterOsSourceHeader",
+ "InvalidRegisterOsTriggerHeader",
+ "WebAndOsHeaders",
+ "NoWebOrOsSupport",
+ "NavigationRegistrationWithoutTransientUserActivation"
+ ]
+ },
+ {
+ "id": "AttributionReportingIssueDetails",
+ "description": "Details for issues around \"Attribution Reporting API\" usage.\nExplainer: https://github.com/WICG/attribution-reporting-api",
+ "type": "object",
+ "properties": [
+ {
+ "name": "violationType",
+ "$ref": "AttributionReportingIssueType"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "invalidParameter",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "QuirksModeIssueDetails",
+ "description": "Details for issues about documents in Quirks Mode\nor Limited Quirks Mode that affects page layouting.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "isLimitedQuirksMode",
+ "description": "If false, it means the document's mode is \"quirks\"\ninstead of \"limited-quirks\".",
+ "type": "boolean"
+ },
+ {
+ "name": "documentNodeId",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ }
+ ]
+ },
+ {
+ "id": "NavigatorUserAgentIssueDetails",
+ "deprecated": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "optional": true,
+ "$ref": "SourceCodeLocation"
+ }
+ ]
+ },
+ {
+ "id": "GenericIssueErrorType",
+ "type": "string",
+ "enum": [
+ "CrossOriginPortalPostMessageError",
+ "FormLabelForNameError",
+ "FormDuplicateIdForInputError",
+ "FormInputWithNoLabelError",
+ "FormAutocompleteAttributeEmptyError",
+ "FormEmptyIdAndNameAttributesForInputError",
+ "FormAriaLabelledByToNonExistingId",
+ "FormInputAssignedAutocompleteValueToIdOrNameAttributeError",
+ "FormLabelHasNeitherForNorNestedInput",
+ "FormLabelForMatchesNonExistingIdError",
+ "FormInputHasWrongButWellIntendedAutocompleteValueError",
+ "ResponseWasBlockedByORB"
+ ]
+ },
+ {
+ "id": "GenericIssueDetails",
+ "description": "Depending on the concrete errorType, different properties are set.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorType",
+ "description": "Issues with the same errorType are aggregated in the frontend.",
+ "$ref": "GenericIssueErrorType"
+ },
+ {
+ "name": "frameId",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "violatingNodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "violatingNodeAttribute",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "optional": true,
+ "$ref": "AffectedRequest"
+ }
+ ]
+ },
+ {
+ "id": "DeprecationIssueDetails",
+ "description": "This issue tracks information needed to print a deprecation message.\nhttps://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/frame/third_party/blink/renderer/core/frame/deprecation/README.md",
+ "type": "object",
+ "properties": [
+ {
+ "name": "affectedFrame",
+ "optional": true,
+ "$ref": "AffectedFrame"
+ },
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "type",
+ "description": "One of the deprecation names from third_party/blink/renderer/core/frame/deprecation/deprecation.json5",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BounceTrackingIssueDetails",
+ "description": "This issue warns about sites in the redirect chain of a finished navigation\nthat may be flagged as trackers and have their state cleared if they don't\nreceive a user interaction. Note that in this context 'site' means eTLD+1.\nFor example, if the URL `https://example.test:80/bounce` was in the\nredirect chain, the site reported would be `example.test`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "trackingSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ClientHintIssueReason",
+ "type": "string",
+ "enum": [
+ "MetaTagAllowListInvalidOrigin",
+ "MetaTagModifiedHTML"
+ ]
+ },
+ {
+ "id": "FederatedAuthRequestIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "federatedAuthRequestIssueReason",
+ "$ref": "FederatedAuthRequestIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FederatedAuthRequestIssueReason",
+ "description": "Represents the failure reason when a federated authentication reason fails.\nShould be updated alongside RequestIdTokenStatus in\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom to include\nall cases except for success.",
+ "type": "string",
+ "enum": [
+ "ShouldEmbargo",
+ "TooManyRequests",
+ "WellKnownHttpNotFound",
+ "WellKnownNoResponse",
+ "WellKnownInvalidResponse",
+ "WellKnownListEmpty",
+ "WellKnownInvalidContentType",
+ "ConfigNotInWellKnown",
+ "WellKnownTooBig",
+ "ConfigHttpNotFound",
+ "ConfigNoResponse",
+ "ConfigInvalidResponse",
+ "ConfigInvalidContentType",
+ "ClientMetadataHttpNotFound",
+ "ClientMetadataNoResponse",
+ "ClientMetadataInvalidResponse",
+ "ClientMetadataInvalidContentType",
+ "DisabledInSettings",
+ "ErrorFetchingSignin",
+ "InvalidSigninResponse",
+ "AccountsHttpNotFound",
+ "AccountsNoResponse",
+ "AccountsInvalidResponse",
+ "AccountsListEmpty",
+ "AccountsInvalidContentType",
+ "IdTokenHttpNotFound",
+ "IdTokenNoResponse",
+ "IdTokenInvalidResponse",
+ "IdTokenInvalidRequest",
+ "IdTokenInvalidContentType",
+ "ErrorIdToken",
+ "Canceled",
+ "RpPageNotVisible",
+ "SilentMediationFailure",
+ "ThirdPartyCookiesBlocked"
+ ]
+ },
+ {
+ "id": "FederatedAuthUserInfoRequestIssueDetails",
+ "type": "object",
+ "properties": [
+ {
+ "name": "federatedAuthUserInfoRequestIssueReason",
+ "$ref": "FederatedAuthUserInfoRequestIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FederatedAuthUserInfoRequestIssueReason",
+ "description": "Represents the failure reason when a getUserInfo() call fails.\nShould be updated alongside FederatedAuthUserInfoRequestResult in\nthird_party/blink/public/mojom/devtools/inspector_issue.mojom.",
+ "type": "string",
+ "enum": [
+ "NotSameOrigin",
+ "NotIframe",
+ "NotPotentiallyTrustworthy",
+ "NoApiPermission",
+ "NotSignedInWithIdp",
+ "NoAccountSharingPermission",
+ "InvalidConfigOrWellKnown",
+ "InvalidAccountsResponse",
+ "NoReturningUserFromFetchedAccounts"
+ ]
+ },
+ {
+ "id": "ClientHintIssueDetails",
+ "description": "This issue tracks client hints related issues. It's used to deprecate old\nfeatures, encourage the use of new ones, and provide general guidance.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "clientHintIssueReason",
+ "$ref": "ClientHintIssueReason"
+ }
+ ]
+ },
+ {
+ "id": "FailedRequestInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL that failed to load.",
+ "type": "string"
+ },
+ {
+ "name": "failureMessage",
+ "description": "The failure message for the failed request.",
+ "type": "string"
+ },
+ {
+ "name": "requestId",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ }
+ ]
+ },
+ {
+ "id": "StyleSheetLoadingIssueReason",
+ "type": "string",
+ "enum": [
+ "LateImportRule",
+ "RequestFailed"
+ ]
+ },
+ {
+ "id": "StylesheetLoadingIssueDetails",
+ "description": "This issue warns when a referenced stylesheet couldn't be loaded.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "sourceCodeLocation",
+ "description": "Source code position that referenced the failing stylesheet.",
+ "$ref": "SourceCodeLocation"
+ },
+ {
+ "name": "styleSheetLoadingIssueReason",
+ "description": "Reason why the stylesheet couldn't be loaded.",
+ "$ref": "StyleSheetLoadingIssueReason"
+ },
+ {
+ "name": "failedRequestInfo",
+ "description": "Contains additional info when the failure was due to a request.",
+ "optional": true,
+ "$ref": "FailedRequestInfo"
+ }
+ ]
+ },
+ {
+ "id": "InspectorIssueCode",
+ "description": "A unique identifier for the type of issue. Each type may use one of the\noptional fields in InspectorIssueDetails to convey more specific\ninformation about the kind of issue.",
+ "type": "string",
+ "enum": [
+ "CookieIssue",
+ "MixedContentIssue",
+ "BlockedByResponseIssue",
+ "HeavyAdIssue",
+ "ContentSecurityPolicyIssue",
+ "SharedArrayBufferIssue",
+ "LowTextContrastIssue",
+ "CorsIssue",
+ "AttributionReportingIssue",
+ "QuirksModeIssue",
+ "NavigatorUserAgentIssue",
+ "GenericIssue",
+ "DeprecationIssue",
+ "ClientHintIssue",
+ "FederatedAuthRequestIssue",
+ "BounceTrackingIssue",
+ "StylesheetLoadingIssue",
+ "FederatedAuthUserInfoRequestIssue"
+ ]
+ },
+ {
+ "id": "InspectorIssueDetails",
+ "description": "This struct holds a list of optional fields with additional information\nspecific to the kind of issue. When adding a new issue code, please also\nadd a new optional field to this type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cookieIssueDetails",
+ "optional": true,
+ "$ref": "CookieIssueDetails"
+ },
+ {
+ "name": "mixedContentIssueDetails",
+ "optional": true,
+ "$ref": "MixedContentIssueDetails"
+ },
+ {
+ "name": "blockedByResponseIssueDetails",
+ "optional": true,
+ "$ref": "BlockedByResponseIssueDetails"
+ },
+ {
+ "name": "heavyAdIssueDetails",
+ "optional": true,
+ "$ref": "HeavyAdIssueDetails"
+ },
+ {
+ "name": "contentSecurityPolicyIssueDetails",
+ "optional": true,
+ "$ref": "ContentSecurityPolicyIssueDetails"
+ },
+ {
+ "name": "sharedArrayBufferIssueDetails",
+ "optional": true,
+ "$ref": "SharedArrayBufferIssueDetails"
+ },
+ {
+ "name": "lowTextContrastIssueDetails",
+ "optional": true,
+ "$ref": "LowTextContrastIssueDetails"
+ },
+ {
+ "name": "corsIssueDetails",
+ "optional": true,
+ "$ref": "CorsIssueDetails"
+ },
+ {
+ "name": "attributionReportingIssueDetails",
+ "optional": true,
+ "$ref": "AttributionReportingIssueDetails"
+ },
+ {
+ "name": "quirksModeIssueDetails",
+ "optional": true,
+ "$ref": "QuirksModeIssueDetails"
+ },
+ {
+ "name": "navigatorUserAgentIssueDetails",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "NavigatorUserAgentIssueDetails"
+ },
+ {
+ "name": "genericIssueDetails",
+ "optional": true,
+ "$ref": "GenericIssueDetails"
+ },
+ {
+ "name": "deprecationIssueDetails",
+ "optional": true,
+ "$ref": "DeprecationIssueDetails"
+ },
+ {
+ "name": "clientHintIssueDetails",
+ "optional": true,
+ "$ref": "ClientHintIssueDetails"
+ },
+ {
+ "name": "federatedAuthRequestIssueDetails",
+ "optional": true,
+ "$ref": "FederatedAuthRequestIssueDetails"
+ },
+ {
+ "name": "bounceTrackingIssueDetails",
+ "optional": true,
+ "$ref": "BounceTrackingIssueDetails"
+ },
+ {
+ "name": "stylesheetLoadingIssueDetails",
+ "optional": true,
+ "$ref": "StylesheetLoadingIssueDetails"
+ },
+ {
+ "name": "federatedAuthUserInfoRequestIssueDetails",
+ "optional": true,
+ "$ref": "FederatedAuthUserInfoRequestIssueDetails"
+ }
+ ]
+ },
+ {
+ "id": "IssueId",
+ "description": "A unique id for a DevTools inspector issue. Allows other entities (e.g.\nexceptions, CDP message, console messages, etc.) to reference an issue.",
+ "type": "string"
+ },
+ {
+ "id": "InspectorIssue",
+ "description": "An inspector issue reported from the back-end.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "code",
+ "$ref": "InspectorIssueCode"
+ },
+ {
+ "name": "details",
+ "$ref": "InspectorIssueDetails"
+ },
+ {
+ "name": "issueId",
+ "description": "A unique id for this issue. May be omitted if no other entity (e.g.\nexception, CDP message, etc.) is referencing this issue.",
+ "optional": true,
+ "$ref": "IssueId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getEncodedResponse",
+ "description": "Returns the response body and size if it were re-encoded with the specified settings. Only\napplies to images.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "encoding",
+ "description": "The encoding to use.",
+ "type": "string",
+ "enum": [
+ "webp",
+ "jpeg",
+ "png"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "The quality of the encoding (0-1). (defaults to 1)",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "sizeOnly",
+ "description": "Whether to only return the size information (defaults to false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "The encoded body as a base64 string. Omitted if sizeOnly is true. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originalSize",
+ "description": "Size before re-encoding.",
+ "type": "integer"
+ },
+ {
+ "name": "encodedSize",
+ "description": "Size after re-encoding.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables issues domain, prevents further issues from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables issues domain, sends the issues collected so far to the client by means of the\n`issueAdded` event."
+ },
+ {
+ "name": "checkContrast",
+ "description": "Runs the contrast check for the target page. Found issues are reported\nusing Audits.issueAdded event.",
+ "parameters": [
+ {
+ "name": "reportAAA",
+ "description": "Whether to report WCAG AAA level issues. Default is false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "checkFormsIssues",
+ "description": "Runs the form issues check for the target page. Found issues are reported\nusing Audits.issueAdded event.",
+ "returns": [
+ {
+ "name": "formIssues",
+ "type": "array",
+ "items": {
+ "$ref": "GenericIssueDetails"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "issueAdded",
+ "parameters": [
+ {
+ "name": "issue",
+ "$ref": "InspectorIssue"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Autofill",
+ "description": "Defines commands and events for Autofill.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "CreditCard",
+ "type": "object",
+ "properties": [
+ {
+ "name": "number",
+ "description": "16-digit credit card number.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Name of the credit card owner.",
+ "type": "string"
+ },
+ {
+ "name": "expiryMonth",
+ "description": "2-digit expiry month.",
+ "type": "string"
+ },
+ {
+ "name": "expiryYear",
+ "description": "4-digit expiry year.",
+ "type": "string"
+ },
+ {
+ "name": "cvc",
+ "description": "3-digit card verification code.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AddressField",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "address field name, for example GIVEN_NAME.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "address field name, for example Jon Doe.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Address",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fields",
+ "description": "fields and values defining a test address.",
+ "type": "array",
+ "items": {
+ "$ref": "AddressField"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "trigger",
+ "description": "Trigger autofill on a form identified by the fieldId.\nIf the field and related form cannot be autofilled, returns an error.",
+ "parameters": [
+ {
+ "name": "fieldId",
+ "description": "Identifies a field that serves as an anchor for autofill.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "Identifies the frame that field belongs to.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "card",
+ "description": "Credit card information to fill out the form. Credit card data is not saved.",
+ "$ref": "CreditCard"
+ }
+ ]
+ },
+ {
+ "name": "setAddresses",
+ "description": "Set addresses so that developers can verify their forms implementation.",
+ "parameters": [
+ {
+ "name": "addresses",
+ "type": "array",
+ "items": {
+ "$ref": "Address"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "BackgroundService",
+ "description": "Defines events for background web platform features.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "ServiceName",
+ "description": "The Background Service that will be associated with the commands/events.\nEvery Background Service operates independently, but they share the same\nAPI.",
+ "type": "string",
+ "enum": [
+ "backgroundFetch",
+ "backgroundSync",
+ "pushMessaging",
+ "notifications",
+ "paymentHandler",
+ "periodicBackgroundSync"
+ ]
+ },
+ {
+ "id": "EventMetadata",
+ "description": "A key-value pair for additional event information to pass along.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BackgroundServiceEvent",
+ "type": "object",
+ "properties": [
+ {
+ "name": "timestamp",
+ "description": "Timestamp of the event (in seconds).",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "origin",
+ "description": "The origin this event belongs to.",
+ "type": "string"
+ },
+ {
+ "name": "serviceWorkerRegistrationId",
+ "description": "The Service Worker ID that initiated the event.",
+ "$ref": "ServiceWorker.RegistrationID"
+ },
+ {
+ "name": "service",
+ "description": "The Background Service this event belongs to.",
+ "$ref": "ServiceName"
+ },
+ {
+ "name": "eventName",
+ "description": "A description of the event.",
+ "type": "string"
+ },
+ {
+ "name": "instanceId",
+ "description": "An identifier that groups related events together.",
+ "type": "string"
+ },
+ {
+ "name": "eventMetadata",
+ "description": "A list of event-specific information.",
+ "type": "array",
+ "items": {
+ "$ref": "EventMetadata"
+ }
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key this event belongs to.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "startObserving",
+ "description": "Enables event updates for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "stopObserving",
+ "description": "Disables event updates for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "setRecording",
+ "description": "Set the recording state for the service.",
+ "parameters": [
+ {
+ "name": "shouldRecord",
+ "type": "boolean"
+ },
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "clearEvents",
+ "description": "Clears all stored data for the service.",
+ "parameters": [
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "recordingStateChanged",
+ "description": "Called when the recording state for the service has been updated.",
+ "parameters": [
+ {
+ "name": "isRecording",
+ "type": "boolean"
+ },
+ {
+ "name": "service",
+ "$ref": "ServiceName"
+ }
+ ]
+ },
+ {
+ "name": "backgroundServiceEventReceived",
+ "description": "Called with all existing backgroundServiceEvents when enabled, and all new\nevents afterwards if enabled and recording.",
+ "parameters": [
+ {
+ "name": "backgroundServiceEvent",
+ "$ref": "BackgroundServiceEvent"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Browser",
+ "description": "The Browser domain defines methods and events for browser managing.",
+ "types": [
+ {
+ "id": "BrowserContextID",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "WindowID",
+ "experimental": true,
+ "type": "integer"
+ },
+ {
+ "id": "WindowState",
+ "description": "The state of the browser window.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "normal",
+ "minimized",
+ "maximized",
+ "fullscreen"
+ ]
+ },
+ {
+ "id": "Bounds",
+ "description": "Browser window bounds information",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "left",
+ "description": "The offset from the left edge of the screen to the window in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "top",
+ "description": "The offset from the top edge of the screen to the window in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "width",
+ "description": "The window width in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "The window height in pixels.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "windowState",
+ "description": "The window state. Default to normal.",
+ "optional": true,
+ "$ref": "WindowState"
+ }
+ ]
+ },
+ {
+ "id": "PermissionType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "accessibilityEvents",
+ "audioCapture",
+ "backgroundSync",
+ "backgroundFetch",
+ "clipboardReadWrite",
+ "clipboardSanitizedWrite",
+ "displayCapture",
+ "durableStorage",
+ "flash",
+ "geolocation",
+ "idleDetection",
+ "localFonts",
+ "midi",
+ "midiSysex",
+ "nfc",
+ "notifications",
+ "paymentHandler",
+ "periodicBackgroundSync",
+ "protectedMediaIdentifier",
+ "sensors",
+ "storageAccess",
+ "topLevelStorageAccess",
+ "videoCapture",
+ "videoCapturePanTiltZoom",
+ "wakeLockScreen",
+ "wakeLockSystem",
+ "windowManagement"
+ ]
+ },
+ {
+ "id": "PermissionSetting",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "granted",
+ "denied",
+ "prompt"
+ ]
+ },
+ {
+ "id": "PermissionDescriptor",
+ "description": "Definition of PermissionDescriptor defined in the Permissions API:\nhttps://w3c.github.io/permissions/#dictdef-permissiondescriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name of permission.\nSee https://cs.chromium.org/chromium/src/third_party/blink/renderer/modules/permissions/permission_descriptor.idl for valid permission names.",
+ "type": "string"
+ },
+ {
+ "name": "sysex",
+ "description": "For \"midi\" permission, may also specify sysex control.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userVisibleOnly",
+ "description": "For \"push\" permission, may specify userVisibleOnly.\nNote that userVisibleOnly = true is the only currently supported type.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowWithoutSanitization",
+ "description": "For \"clipboard\" permission, may specify allowWithoutSanitization.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "panTiltZoom",
+ "description": "For \"camera\" permission, may specify panTiltZoom.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "BrowserCommandId",
+ "description": "Browser command ids used by executeBrowserCommand.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "openTabSearch",
+ "closeTabSearch"
+ ]
+ },
+ {
+ "id": "Bucket",
+ "description": "Chrome histogram bucket.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "low",
+ "description": "Minimum value (inclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "high",
+ "description": "Maximum value (exclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Number of samples.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "Histogram",
+ "description": "Chrome histogram.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name.",
+ "type": "string"
+ },
+ {
+ "name": "sum",
+ "description": "Sum of sample values.",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Total number of samples.",
+ "type": "integer"
+ },
+ {
+ "name": "buckets",
+ "description": "Buckets.",
+ "type": "array",
+ "items": {
+ "$ref": "Bucket"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "setPermission",
+ "description": "Set permission settings for given origin.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "permission",
+ "description": "Descriptor of permission to override.",
+ "$ref": "PermissionDescriptor"
+ },
+ {
+ "name": "setting",
+ "description": "Setting of the permission.",
+ "$ref": "PermissionSetting"
+ },
+ {
+ "name": "origin",
+ "description": "Origin the permission applies to, all origins if not specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "Context to override. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "grantPermissions",
+ "description": "Grant specific permissions to the given origin and reject all others.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "permissions",
+ "type": "array",
+ "items": {
+ "$ref": "PermissionType"
+ }
+ },
+ {
+ "name": "origin",
+ "description": "Origin the permission applies to, all origins if not specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to override permissions. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "resetPermissions",
+ "description": "Reset all permission management for all origins.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to reset permissions. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "setDownloadBehavior",
+ "description": "Set the behavior when downloading a file.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "behavior",
+ "description": "Whether to allow all or deny all download requests, or use default Chrome behavior if\navailable (otherwise deny). |allowAndName| allows download and names files according to\ntheir dowmload guids.",
+ "type": "string",
+ "enum": [
+ "deny",
+ "allow",
+ "allowAndName",
+ "default"
+ ]
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to set download behavior. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ },
+ {
+ "name": "downloadPath",
+ "description": "The default path to save downloaded files to. This is required if behavior is set to 'allow'\nor 'allowAndName'.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "eventsEnabled",
+ "description": "Whether to emit download events (defaults to false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "cancelDownload",
+ "description": "Cancel a download if in progress",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "browserContextId",
+ "description": "BrowserContext to perform the action in. When omitted, default browser context is used.",
+ "optional": true,
+ "$ref": "BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "close",
+ "description": "Close browser gracefully."
+ },
+ {
+ "name": "crash",
+ "description": "Crashes browser on the main thread.",
+ "experimental": true
+ },
+ {
+ "name": "crashGpuProcess",
+ "description": "Crashes GPU process.",
+ "experimental": true
+ },
+ {
+ "name": "getVersion",
+ "description": "Returns version information.",
+ "returns": [
+ {
+ "name": "protocolVersion",
+ "description": "Protocol version.",
+ "type": "string"
+ },
+ {
+ "name": "product",
+ "description": "Product name.",
+ "type": "string"
+ },
+ {
+ "name": "revision",
+ "description": "Product revision.",
+ "type": "string"
+ },
+ {
+ "name": "userAgent",
+ "description": "User-Agent.",
+ "type": "string"
+ },
+ {
+ "name": "jsVersion",
+ "description": "V8 version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserCommandLine",
+ "description": "Returns the command line switches for the browser process if, and only if\n--enable-automation is on the commandline.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "arguments",
+ "description": "Commandline parameters",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getHistograms",
+ "description": "Get Chrome histograms.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "query",
+ "description": "Requested substring in name. Only histograms which have query as a\nsubstring in their name are extracted. An empty or absent query returns\nall histograms.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "delta",
+ "description": "If true, retrieve delta since last delta call.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "histograms",
+ "description": "Histograms.",
+ "type": "array",
+ "items": {
+ "$ref": "Histogram"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getHistogram",
+ "description": "Get a Chrome histogram by name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Requested histogram name.",
+ "type": "string"
+ },
+ {
+ "name": "delta",
+ "description": "If true, retrieve delta since last delta call.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "histogram",
+ "description": "Histogram.",
+ "$ref": "Histogram"
+ }
+ ]
+ },
+ {
+ "name": "getWindowBounds",
+ "description": "Get position and size of the browser window.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "bounds",
+ "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "getWindowForTarget",
+ "description": "Get the browser window that contains the devtools target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "description": "Devtools agent host id. If called as a part of the session, associated targetId is used.",
+ "optional": true,
+ "$ref": "Target.TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ },
+ {
+ "name": "bounds",
+ "description": "Bounds information of the window. When window state is 'minimized', the restored window\nposition and size are returned.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "setWindowBounds",
+ "description": "Set position and/or size of the browser window.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "windowId",
+ "description": "Browser window id.",
+ "$ref": "WindowID"
+ },
+ {
+ "name": "bounds",
+ "description": "New window bounds. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined\nwith 'left', 'top', 'width' or 'height'. Leaves unspecified fields unchanged.",
+ "$ref": "Bounds"
+ }
+ ]
+ },
+ {
+ "name": "setDockTile",
+ "description": "Set dock tile details, platform-specific.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "badgeLabel",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "image",
+ "description": "Png encoded image. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "executeBrowserCommand",
+ "description": "Invoke custom browser commands used by telemetry.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "commandId",
+ "$ref": "BrowserCommandId"
+ }
+ ]
+ },
+ {
+ "name": "addPrivacySandboxEnrollmentOverride",
+ "description": "Allows a site to use privacy sandbox features that require enrollment\nwithout the site actually being enrolled. Only supported on page targets.",
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "downloadWillBegin",
+ "description": "Fired when page is about to start a download.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that caused the download to begin.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource being downloaded.",
+ "type": "string"
+ },
+ {
+ "name": "suggestedFilename",
+ "description": "Suggested file name of the resource (the actual name of the file saved on disk may differ).",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "downloadProgress",
+ "description": "Fired when download makes progress. Last call has |done| == true.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "totalBytes",
+ "description": "Total expected bytes to download.",
+ "type": "number"
+ },
+ {
+ "name": "receivedBytes",
+ "description": "Total bytes received.",
+ "type": "number"
+ },
+ {
+ "name": "state",
+ "description": "Download status.",
+ "type": "string",
+ "enum": [
+ "inProgress",
+ "completed",
+ "canceled"
+ ]
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "CSS",
+ "description": "This domain exposes CSS read/write operations. All CSS objects (stylesheets, rules, and styles)\nhave an associated `id` used in subsequent operations on the related object. Each object type has\na specific `id` structure, and those are not interchangeable between objects of different kinds.\nCSS objects can be loaded using the `get*ForNode()` calls (which accept a DOM node id). A client\ncan also keep track of stylesheets via the `styleSheetAdded`/`styleSheetRemoved` events and\nsubsequently load the required stylesheet contents using the `getStyleSheet[Text]()` methods.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "StyleSheetId",
+ "type": "string"
+ },
+ {
+ "id": "StyleSheetOrigin",
+ "description": "Stylesheet type: \"injected\" for stylesheets injected via extension, \"user-agent\" for user-agent\nstylesheets, \"inspector\" for stylesheets created by the inspector (i.e. those holding the \"via\ninspector\" rules), \"regular\" for regular stylesheets.",
+ "type": "string",
+ "enum": [
+ "injected",
+ "user-agent",
+ "inspector",
+ "regular"
+ ]
+ },
+ {
+ "id": "PseudoElementMatches",
+ "description": "CSS rule collection for a single pseudo style.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pseudoType",
+ "description": "Pseudo element type.",
+ "$ref": "DOM.PseudoType"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element custom ident.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "matches",
+ "description": "Matches of CSS rules applicable to the pseudo style.",
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InheritedStyleEntry",
+ "description": "Inherited CSS rule collection from ancestor node.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "inlineStyle",
+ "description": "The ancestor node's inline style, if any, in the style inheritance chain.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "matchedCSSRules",
+ "description": "Matches of CSS rules matching the ancestor node in the style inheritance chain.",
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InheritedPseudoElementMatches",
+ "description": "Inherited pseudo element matches from pseudos of an ancestor node.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pseudoElements",
+ "description": "Matches of pseudo styles from the pseudos of an ancestor node.",
+ "type": "array",
+ "items": {
+ "$ref": "PseudoElementMatches"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RuleMatch",
+ "description": "Match data for a CSS rule.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rule",
+ "description": "CSS rule in the match.",
+ "$ref": "CSSRule"
+ },
+ {
+ "name": "matchingSelectors",
+ "description": "Matching selector indices in the rule's selectorList selectors (0-based).",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Value",
+ "description": "Data for a simple selector (these are delimited by commas in a selector list).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Value text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "Value range in the underlying resource (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "specificity",
+ "description": "Specificity of the selector.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Specificity"
+ }
+ ]
+ },
+ {
+ "id": "Specificity",
+ "description": "Specificity:\nhttps://drafts.csswg.org/selectors/#specificity-rules",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "a",
+ "description": "The a component, which represents the number of ID selectors.",
+ "type": "integer"
+ },
+ {
+ "name": "b",
+ "description": "The b component, which represents the number of class selectors, attributes selectors, and\npseudo-classes.",
+ "type": "integer"
+ },
+ {
+ "name": "c",
+ "description": "The c component, which represents the number of type selectors and pseudo-elements.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "SelectorList",
+ "description": "Selector list data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "selectors",
+ "description": "Selectors in the list.",
+ "type": "array",
+ "items": {
+ "$ref": "Value"
+ }
+ },
+ {
+ "name": "text",
+ "description": "Rule selector text.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CSSStyleSheetHeader",
+ "description": "CSS stylesheet metainformation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The stylesheet identifier.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "frameId",
+ "description": "Owner frame identifier.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "sourceURL",
+ "description": "Stylesheet resource URL. Empty if this is a constructed stylesheet created using\nnew CSSStyleSheet() (but non-empty if this is a constructed sylesheet imported\nas a CSS module script).",
+ "type": "string"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with the stylesheet (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "origin",
+ "description": "Stylesheet origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "title",
+ "description": "Stylesheet title.",
+ "type": "string"
+ },
+ {
+ "name": "ownerNode",
+ "description": "The backend id for the owner node of the stylesheet.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "disabled",
+ "description": "Denotes whether the stylesheet is disabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "Whether the sourceURL field value comes from the sourceURL comment.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isInline",
+ "description": "Whether this stylesheet is created for STYLE tag by parser. This flag is not set for\ndocument.written STYLE tags.",
+ "type": "boolean"
+ },
+ {
+ "name": "isMutable",
+ "description": "Whether this stylesheet is mutable. Inline stylesheets become mutable\nafter they have been modified via CSSOM API.\n`<link>` element's stylesheets become mutable only if DevTools modifies them.\nConstructed stylesheets (new CSSStyleSheet()) are mutable immediately after creation.",
+ "type": "boolean"
+ },
+ {
+ "name": "isConstructed",
+ "description": "True if this stylesheet is created through new CSSStyleSheet() or imported as a\nCSS module script.",
+ "type": "boolean"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "length",
+ "description": "Size of the content (in characters).",
+ "type": "number"
+ },
+ {
+ "name": "endLine",
+ "description": "Line offset of the end of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "endColumn",
+ "description": "Column offset of the end of the stylesheet within the resource (zero based).",
+ "type": "number"
+ },
+ {
+ "name": "loadingFailed",
+ "description": "If the style sheet was loaded from a network resource, this indicates when the resource failed to load",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CSSRule",
+ "description": "CSS rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "selectorList",
+ "description": "Rule selector data.",
+ "$ref": "SelectorList"
+ },
+ {
+ "name": "nestingSelectors",
+ "description": "Array of selectors from ancestor style rules, sorted by distance from the current rule.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "media",
+ "description": "Media list array (for rules involving media queries). The array enumerates media queries\nstarting with the innermost one, going outwards.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSMedia"
+ }
+ },
+ {
+ "name": "containerQueries",
+ "description": "Container query list array (for rules involving container queries).\nThe array enumerates container queries starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSContainerQuery"
+ }
+ },
+ {
+ "name": "supports",
+ "description": "@supports CSS at-rule array.\nThe array enumerates @supports at-rules starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSSupports"
+ }
+ },
+ {
+ "name": "layers",
+ "description": "Cascade layer array. Contains the layer hierarchy that this rule belongs to starting\nwith the innermost layer and going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSLayer"
+ }
+ },
+ {
+ "name": "scopes",
+ "description": "@scope CSS at-rule array.\nThe array enumerates @scope at-rules starting with the innermost one, going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSScope"
+ }
+ },
+ {
+ "name": "ruleTypes",
+ "description": "The array keeps the types of ancestor CSSRules from the innermost going outwards.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSRuleType"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSRuleType",
+ "description": "Enum indicating the type of a CSS rule, used to represent the order of a style rule's ancestors.\nThis list only contains rule types that are collected during the ancestor rule collection.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "MediaRule",
+ "SupportsRule",
+ "ContainerRule",
+ "LayerRule",
+ "ScopeRule",
+ "StyleRule"
+ ]
+ },
+ {
+ "id": "RuleUsage",
+ "description": "CSS coverage information.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "startOffset",
+ "description": "Offset of the start of the rule (including selector) from the beginning of the stylesheet.",
+ "type": "number"
+ },
+ {
+ "name": "endOffset",
+ "description": "Offset of the end of the rule body from the beginning of the stylesheet.",
+ "type": "number"
+ },
+ {
+ "name": "used",
+ "description": "Indicates whether the rule was actually used by some element in the page.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SourceRange",
+ "description": "Text range within a resource. All numbers are zero-based.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "startLine",
+ "description": "Start line of range.",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Start column of range (inclusive).",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "End line of range",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "End column of range (exclusive).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ShorthandEntry",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Shorthand name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Shorthand value.",
+ "type": "string"
+ },
+ {
+ "name": "important",
+ "description": "Whether the property has \"!important\" annotation (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CSSComputedStyleProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Computed style property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Computed style property value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CSSStyle",
+ "description": "CSS style representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "cssProperties",
+ "description": "CSS properties in the style.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSProperty"
+ }
+ },
+ {
+ "name": "shorthandEntries",
+ "description": "Computed values for all shorthands found in the style.",
+ "type": "array",
+ "items": {
+ "$ref": "ShorthandEntry"
+ }
+ },
+ {
+ "name": "cssText",
+ "description": "Style declaration text (if available).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "Style declaration range in the enclosing stylesheet (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ }
+ ]
+ },
+ {
+ "id": "CSSProperty",
+ "description": "CSS property declaration data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "The property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The property value.",
+ "type": "string"
+ },
+ {
+ "name": "important",
+ "description": "Whether the property has \"!important\" annotation (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "implicit",
+ "description": "Whether the property is implicit (implies `false` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "text",
+ "description": "The full property text as specified in the style.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "parsedOk",
+ "description": "Whether the property is understood by the browser (implies `true` if absent).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "disabled",
+ "description": "Whether the property is disabled by the user (present for source-based properties only).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "range",
+ "description": "The entire property range in the enclosing style declaration (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "longhandProperties",
+ "description": "Parsed longhand components of this property if it is a shorthand.\nThis field will be empty if the given property is not a shorthand.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSProperty"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSMedia",
+ "description": "CSS media rule descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Media query text.",
+ "type": "string"
+ },
+ {
+ "name": "source",
+ "description": "Source of the media query: \"mediaRule\" if specified by a @media rule, \"importRule\" if\nspecified by an @import rule, \"linkedSheet\" if specified by a \"media\" attribute in a linked\nstylesheet's LINK tag, \"inlineSheet\" if specified by a \"media\" attribute in an inline\nstylesheet's STYLE tag.",
+ "type": "string",
+ "enum": [
+ "mediaRule",
+ "importRule",
+ "linkedSheet",
+ "inlineSheet"
+ ]
+ },
+ {
+ "name": "sourceURL",
+ "description": "URL of the document containing the media query description.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule (@media or @import) header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "mediaList",
+ "description": "Array of media queries.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "MediaQuery"
+ }
+ }
+ ]
+ },
+ {
+ "id": "MediaQuery",
+ "description": "Media query descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "expressions",
+ "description": "Array of media query expressions.",
+ "type": "array",
+ "items": {
+ "$ref": "MediaQueryExpression"
+ }
+ },
+ {
+ "name": "active",
+ "description": "Whether the media query condition is satisfied.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "MediaQueryExpression",
+ "description": "Media query expression descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Media query expression value.",
+ "type": "number"
+ },
+ {
+ "name": "unit",
+ "description": "Media query expression units.",
+ "type": "string"
+ },
+ {
+ "name": "feature",
+ "description": "Media query expression feature.",
+ "type": "string"
+ },
+ {
+ "name": "valueRange",
+ "description": "The associated range of the value text in the enclosing stylesheet (if available).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "computedLength",
+ "description": "Computed length of media query expression (if applicable).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "CSSContainerQuery",
+ "description": "CSS container query rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Container query text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "name",
+ "description": "Optional name for the container.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "physicalAxes",
+ "description": "Optional physical axes queried for the container.",
+ "optional": true,
+ "$ref": "DOM.PhysicalAxes"
+ },
+ {
+ "name": "logicalAxes",
+ "description": "Optional logical axes queried for the container.",
+ "optional": true,
+ "$ref": "DOM.LogicalAxes"
+ }
+ ]
+ },
+ {
+ "id": "CSSSupports",
+ "description": "CSS Supports at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Supports rule text.",
+ "type": "string"
+ },
+ {
+ "name": "active",
+ "description": "Whether the supports condition is satisfied.",
+ "type": "boolean"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSScope",
+ "description": "CSS Scope at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Scope rule text.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSLayer",
+ "description": "CSS Layer at-rule descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "text",
+ "description": "Layer name.",
+ "type": "string"
+ },
+ {
+ "name": "range",
+ "description": "The associated rule header range in the enclosing stylesheet (if\navailable).",
+ "optional": true,
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the stylesheet containing this object (if exists).",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "id": "CSSLayerData",
+ "description": "CSS Layer data.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Layer name.",
+ "type": "string"
+ },
+ {
+ "name": "subLayers",
+ "description": "Direct sub-layers",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSLayerData"
+ }
+ },
+ {
+ "name": "order",
+ "description": "Layer order. The order determines the order of the layer in the cascade order.\nA higher number has higher priority in the cascade order.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "PlatformFontUsage",
+ "description": "Information about amount of glyphs that were rendered with given font.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "familyName",
+ "description": "Font's family name reported by platform.",
+ "type": "string"
+ },
+ {
+ "name": "isCustomFont",
+ "description": "Indicates if the font was downloaded or resolved locally.",
+ "type": "boolean"
+ },
+ {
+ "name": "glyphCount",
+ "description": "Amount of glyphs that were rendered with this font.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontVariationAxis",
+ "description": "Information about font variation axes for variable fonts",
+ "type": "object",
+ "properties": [
+ {
+ "name": "tag",
+ "description": "The font-variation-setting tag (a.k.a. \"axis tag\").",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Human-readable variation name in the default language (normally, \"en\").",
+ "type": "string"
+ },
+ {
+ "name": "minValue",
+ "description": "The minimum value (inclusive) the font supports for this tag.",
+ "type": "number"
+ },
+ {
+ "name": "maxValue",
+ "description": "The maximum value (inclusive) the font supports for this tag.",
+ "type": "number"
+ },
+ {
+ "name": "defaultValue",
+ "description": "The default value.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontFace",
+ "description": "Properties of a web font: https://www.w3.org/TR/2008/REC-CSS2-20080411/fonts.html#font-descriptions\nand additional information such as platformFontFamily and fontVariationAxes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fontFamily",
+ "description": "The font-family.",
+ "type": "string"
+ },
+ {
+ "name": "fontStyle",
+ "description": "The font-style.",
+ "type": "string"
+ },
+ {
+ "name": "fontVariant",
+ "description": "The font-variant.",
+ "type": "string"
+ },
+ {
+ "name": "fontWeight",
+ "description": "The font-weight.",
+ "type": "string"
+ },
+ {
+ "name": "fontStretch",
+ "description": "The font-stretch.",
+ "type": "string"
+ },
+ {
+ "name": "fontDisplay",
+ "description": "The font-display.",
+ "type": "string"
+ },
+ {
+ "name": "unicodeRange",
+ "description": "The unicode-range.",
+ "type": "string"
+ },
+ {
+ "name": "src",
+ "description": "The src.",
+ "type": "string"
+ },
+ {
+ "name": "platformFontFamily",
+ "description": "The resolved platform font family",
+ "type": "string"
+ },
+ {
+ "name": "fontVariationAxes",
+ "description": "Available variation settings (a.k.a. \"axes\").",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FontVariationAxis"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSTryRule",
+ "description": "CSS try rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "id": "CSSPositionFallbackRule",
+ "description": "CSS position-fallback rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "$ref": "Value"
+ },
+ {
+ "name": "tryRules",
+ "description": "List of keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSTryRule"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSKeyframesRule",
+ "description": "CSS keyframes rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "animationName",
+ "description": "Animation name.",
+ "$ref": "Value"
+ },
+ {
+ "name": "keyframes",
+ "description": "List of keyframes.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSKeyframeRule"
+ }
+ }
+ ]
+ },
+ {
+ "id": "CSSKeyframeRule",
+ "description": "CSS keyframe rule representation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier (absent for user agent stylesheet and user-specified\nstylesheet rules) this rule came from.",
+ "optional": true,
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "origin",
+ "description": "Parent stylesheet's origin.",
+ "$ref": "StyleSheetOrigin"
+ },
+ {
+ "name": "keyText",
+ "description": "Associated key text.",
+ "$ref": "Value"
+ },
+ {
+ "name": "style",
+ "description": "Associated style declaration.",
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "id": "StyleDeclarationEdit",
+ "description": "A descriptor of operation to mutate style declaration text.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "description": "The range of the style text in the enclosing stylesheet.",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "description": "New style text.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addRule",
+ "description": "Inserts a new rule with the given `ruleText` in a stylesheet with given `styleSheetId`, at the\nposition specified by `location`.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "description": "The css style sheet identifier where a new rule should be inserted.",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "ruleText",
+ "description": "The text of a new rule.",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Text position of a new rule in the target style sheet.",
+ "$ref": "SourceRange"
+ }
+ ],
+ "returns": [
+ {
+ "name": "rule",
+ "description": "The newly created rule.",
+ "$ref": "CSSRule"
+ }
+ ]
+ },
+ {
+ "name": "collectClassNames",
+ "description": "Returns all class names from specified stylesheet.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "classNames",
+ "description": "Class name list.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "createStyleSheet",
+ "description": "Creates a new special \"via-inspector\" stylesheet in the frame with given `frameId`.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Identifier of the frame where \"via-inspector\" stylesheet should be created.",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the created \"via-inspector\" stylesheet.",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables the CSS agent for the given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables the CSS agent for the given page. Clients should not assume that the CSS agent has been\nenabled until the result of this command is received."
+ },
+ {
+ "name": "forcePseudoState",
+ "description": "Ensures that the given node will have specified pseudo-classes whenever its style is computed by\nthe browser.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "The element id for which to force the pseudo state.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "forcedPseudoClasses",
+ "description": "Element pseudo classes to force when computing the element's style.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getBackgroundColors",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get background colors for.",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backgroundColors",
+ "description": "The range of background colors behind this element, if it contains any visible text. If no\nvisible text is present, this will be undefined. In the case of a flat background color,\nthis will consist of simply that color. In the case of a gradient, this will consist of each\nof the color stops. For anything more complicated, this will be an empty array. Images will\nbe ignored (as if the image had failed to load).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "computedFontSize",
+ "description": "The computed font size for this node, as a CSS computed value string (e.g. '12px').",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "computedFontWeight",
+ "description": "The computed font weight for this node, as a CSS computed value string (e.g. 'normal' or\n'100').",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getComputedStyleForNode",
+ "description": "Returns the computed style for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "computedStyle",
+ "description": "Computed style for the specified DOM node.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getInlineStylesForNode",
+ "description": "Returns the styles defined inline (explicitly in the \"style\" attribute and implicitly, using DOM\nattributes) for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "inlineStyle",
+ "description": "Inline style for the specified DOM node.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "attributesStyle",
+ "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").",
+ "optional": true,
+ "$ref": "CSSStyle"
+ }
+ ]
+ },
+ {
+ "name": "getMatchedStylesForNode",
+ "description": "Returns requested styles for a DOM node identified by `nodeId`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "inlineStyle",
+ "description": "Inline style for the specified DOM node.",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "attributesStyle",
+ "description": "Attribute-defined element style (e.g. resulting from \"width=20 height=100%\").",
+ "optional": true,
+ "$ref": "CSSStyle"
+ },
+ {
+ "name": "matchedCSSRules",
+ "description": "CSS rules matching this node, from all applicable stylesheets.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "RuleMatch"
+ }
+ },
+ {
+ "name": "pseudoElements",
+ "description": "Pseudo style matches for this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PseudoElementMatches"
+ }
+ },
+ {
+ "name": "inherited",
+ "description": "A chain of inherited styles (from the immediate node parent up to the DOM tree root).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InheritedStyleEntry"
+ }
+ },
+ {
+ "name": "inheritedPseudoElements",
+ "description": "A chain of inherited pseudo element styles (from the immediate node parent up to the DOM tree root).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InheritedPseudoElementMatches"
+ }
+ },
+ {
+ "name": "cssKeyframesRules",
+ "description": "A list of CSS keyframed animations matching this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSKeyframesRule"
+ }
+ },
+ {
+ "name": "cssPositionFallbackRules",
+ "description": "A list of CSS position fallbacks matching this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CSSPositionFallbackRule"
+ }
+ },
+ {
+ "name": "parentLayoutNodeId",
+ "description": "Id of the first parent element that does not have display: contents.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getMediaQueries",
+ "description": "Returns all media queries parsed by the rendering engine.",
+ "returns": [
+ {
+ "name": "medias",
+ "type": "array",
+ "items": {
+ "$ref": "CSSMedia"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getPlatformFontsForNode",
+ "description": "Requests information about platform fonts which we used to render child TextNodes in the given\nnode.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "fonts",
+ "description": "Usage statistics for every employed platform font.",
+ "type": "array",
+ "items": {
+ "$ref": "PlatformFontUsage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getStyleSheetText",
+ "description": "Returns the current textual content for a stylesheet.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "text",
+ "description": "The stylesheet text.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getLayersForNode",
+ "description": "Returns all layers parsed by the rendering engine for the tree scope of a node.\nGiven a DOM element identified by nodeId, getLayersForNode returns the root\nlayer for the nearest ancestor document or shadow root. The layer root contains\nthe full layer tree for the tree scope and their ordering.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "rootLayer",
+ "$ref": "CSSLayerData"
+ }
+ ]
+ },
+ {
+ "name": "trackComputedStyleUpdates",
+ "description": "Starts tracking the given computed styles for updates. The specified array of properties\nreplaces the one previously specified. Pass empty array to disable tracking.\nUse takeComputedStyleUpdates to retrieve the list of nodes that had properties modified.\nThe changes to computed style properties are only tracked for nodes pushed to the front-end\nby the DOM agent. If no changes to the tracked properties occur after the node has been pushed\nto the front-end, no updates will be issued for the node.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "propertiesToTrack",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "takeComputedStyleUpdates",
+ "description": "Polls the next batch of computed style updates.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "The list of node Ids that have their tracked computed styles updated.",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setEffectivePropertyValueForNode",
+ "description": "Find a rule with the given active property for the given node and set the new value for this\nproperty",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "The element id for which to set property.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "propertyName",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setKeyframeKey",
+ "description": "Modifies the keyframe rule key text.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "keyText",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "keyText",
+ "description": "The resulting key text after modification.",
+ "$ref": "Value"
+ }
+ ]
+ },
+ {
+ "name": "setMediaText",
+ "description": "Modifies the rule selector.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "media",
+ "description": "The resulting CSS media rule after modification.",
+ "$ref": "CSSMedia"
+ }
+ ]
+ },
+ {
+ "name": "setContainerQueryText",
+ "description": "Modifies the expression of a container query.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "containerQuery",
+ "description": "The resulting CSS container query rule after modification.",
+ "$ref": "CSSContainerQuery"
+ }
+ ]
+ },
+ {
+ "name": "setSupportsText",
+ "description": "Modifies the expression of a supports at-rule.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "supports",
+ "description": "The resulting CSS Supports rule after modification.",
+ "$ref": "CSSSupports"
+ }
+ ]
+ },
+ {
+ "name": "setScopeText",
+ "description": "Modifies the expression of a scope at-rule.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scope",
+ "description": "The resulting CSS Scope rule after modification.",
+ "$ref": "CSSScope"
+ }
+ ]
+ },
+ {
+ "name": "setRuleSelector",
+ "description": "Modifies the rule selector.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "range",
+ "$ref": "SourceRange"
+ },
+ {
+ "name": "selector",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "selectorList",
+ "description": "The resulting selector list after modification.",
+ "$ref": "SelectorList"
+ }
+ ]
+ },
+ {
+ "name": "setStyleSheetText",
+ "description": "Sets the new stylesheet text.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ },
+ {
+ "name": "text",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setStyleTexts",
+ "description": "Applies specified style edits one after another in the given order.",
+ "parameters": [
+ {
+ "name": "edits",
+ "type": "array",
+ "items": {
+ "$ref": "StyleDeclarationEdit"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "styles",
+ "description": "The resulting styles after modification.",
+ "type": "array",
+ "items": {
+ "$ref": "CSSStyle"
+ }
+ }
+ ]
+ },
+ {
+ "name": "startRuleUsageTracking",
+ "description": "Enables the selector recording."
+ },
+ {
+ "name": "stopRuleUsageTracking",
+ "description": "Stop tracking rule usage and return the list of rules that were used since last call to\n`takeCoverageDelta` (or since start of coverage instrumentation).",
+ "returns": [
+ {
+ "name": "ruleUsage",
+ "type": "array",
+ "items": {
+ "$ref": "RuleUsage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "takeCoverageDelta",
+ "description": "Obtain list of rules that became used since last call to this method (or since start of coverage\ninstrumentation).",
+ "returns": [
+ {
+ "name": "coverage",
+ "type": "array",
+ "items": {
+ "$ref": "RuleUsage"
+ }
+ },
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time, in seconds.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLocalFontsEnabled",
+ "description": "Enables/disables rendering of local CSS fonts (enabled by default).",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether rendering of local fonts is enabled.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "fontsUpdated",
+ "description": "Fires whenever a web font is updated. A non-empty font parameter indicates a successfully loaded\nweb font.",
+ "parameters": [
+ {
+ "name": "font",
+ "description": "The web font that has loaded.",
+ "optional": true,
+ "$ref": "FontFace"
+ }
+ ]
+ },
+ {
+ "name": "mediaQueryResultChanged",
+ "description": "Fires whenever a MediaQuery result changes (for example, after a browser window has been\nresized.) The current implementation considers only viewport-dependent media features."
+ },
+ {
+ "name": "styleSheetAdded",
+ "description": "Fired whenever an active document stylesheet is added.",
+ "parameters": [
+ {
+ "name": "header",
+ "description": "Added stylesheet metainfo.",
+ "$ref": "CSSStyleSheetHeader"
+ }
+ ]
+ },
+ {
+ "name": "styleSheetChanged",
+ "description": "Fired whenever a stylesheet is changed as a result of the client operation.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ },
+ {
+ "name": "styleSheetRemoved",
+ "description": "Fired whenever an active document stylesheet is removed.",
+ "parameters": [
+ {
+ "name": "styleSheetId",
+ "description": "Identifier of the removed stylesheet.",
+ "$ref": "StyleSheetId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "CacheStorage",
+ "experimental": true,
+ "dependencies": [
+ "Storage"
+ ],
+ "types": [
+ {
+ "id": "CacheId",
+ "description": "Unique identifier of the Cache object.",
+ "type": "string"
+ },
+ {
+ "id": "CachedResponseType",
+ "description": "type of HTTP response cached",
+ "type": "string",
+ "enum": [
+ "basic",
+ "cors",
+ "default",
+ "error",
+ "opaqueResponse",
+ "opaqueRedirect"
+ ]
+ },
+ {
+ "id": "DataEntry",
+ "description": "Data entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestURL",
+ "description": "Request URL.",
+ "type": "string"
+ },
+ {
+ "name": "requestMethod",
+ "description": "Request method.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "Request headers",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ },
+ {
+ "name": "responseTime",
+ "description": "Number of seconds since epoch.",
+ "type": "number"
+ },
+ {
+ "name": "responseStatus",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseStatusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "responseType",
+ "description": "HTTP response type",
+ "$ref": "CachedResponseType"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Cache",
+ "description": "Cache identifier.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "cacheId",
+ "description": "An opaque unique id of the cache.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "securityOrigin",
+ "description": "Security origin of the cache.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key of the cache.",
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket of the cache.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "cacheName",
+ "description": "The name of the cache.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Header",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CachedResponse",
+ "description": "Cached response",
+ "type": "object",
+ "properties": [
+ {
+ "name": "body",
+ "description": "Entry content, base64-encoded. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "deleteCache",
+ "description": "Deletes a cache.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache for deletion.",
+ "$ref": "CacheId"
+ }
+ ]
+ },
+ {
+ "name": "deleteEntry",
+ "description": "Deletes a cache entry.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache where the entry will be deleted.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "request",
+ "description": "URL spec of the request.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "requestCacheNames",
+ "description": "Requests cache names.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ }
+ ],
+ "returns": [
+ {
+ "name": "caches",
+ "description": "Caches for the security origin.",
+ "type": "array",
+ "items": {
+ "$ref": "Cache"
+ }
+ }
+ ]
+ },
+ {
+ "name": "requestCachedResponse",
+ "description": "Fetches cache entry.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "Id of cache that contains the entry.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "requestURL",
+ "description": "URL spec of the request.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "headers of the request.",
+ "type": "array",
+ "items": {
+ "$ref": "Header"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "response",
+ "description": "Response read from the cache.",
+ "$ref": "CachedResponse"
+ }
+ ]
+ },
+ {
+ "name": "requestEntries",
+ "description": "Requests data from cache.",
+ "parameters": [
+ {
+ "name": "cacheId",
+ "description": "ID of cache to get entries from.",
+ "$ref": "CacheId"
+ },
+ {
+ "name": "skipCount",
+ "description": "Number of records to skip.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pageSize",
+ "description": "Number of records to fetch.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pathFilter",
+ "description": "If present, only return the entries containing this substring in the path",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "cacheDataEntries",
+ "description": "Array of object store data entries.",
+ "type": "array",
+ "items": {
+ "$ref": "DataEntry"
+ }
+ },
+ {
+ "name": "returnCount",
+ "description": "Count of returned entries from this storage. If pathFilter is empty, it\nis the count of all entries from this storage.",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Cast",
+ "description": "A domain for interacting with Cast, Presentation API, and Remote Playback API\nfunctionalities.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "Sink",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "session",
+ "description": "Text describing the current session. Present only if there is an active\nsession on the sink.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Starts observing for sinks that can be used for tab mirroring, and if set,\nsinks compatible with |presentationUrl| as well. When sinks are found, a\n|sinksUpdated| event is fired.\nAlso starts observing for issue messages. When an issue is added or removed,\nan |issueUpdated| event is fired.",
+ "parameters": [
+ {
+ "name": "presentationUrl",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Stops observing for sinks and issues."
+ },
+ {
+ "name": "setSinkToUse",
+ "description": "Sets a sink to be used when the web page requests the browser to choose a\nsink via Presentation API, Remote Playback API, or Cast SDK.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startDesktopMirroring",
+ "description": "Starts mirroring the desktop to the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startTabMirroring",
+ "description": "Starts mirroring the tab to the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "stopCasting",
+ "description": "Stops the active Cast session on the sink.",
+ "parameters": [
+ {
+ "name": "sinkName",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "sinksUpdated",
+ "description": "This is fired whenever the list of available sinks changes. A sink is a\ndevice or a software surface that you can cast to.",
+ "parameters": [
+ {
+ "name": "sinks",
+ "type": "array",
+ "items": {
+ "$ref": "Sink"
+ }
+ }
+ ]
+ },
+ {
+ "name": "issueUpdated",
+ "description": "This is fired whenever the outstanding issue/error message changes.\n|issueMessage| is empty if there is no issue.",
+ "parameters": [
+ {
+ "name": "issueMessage",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOM",
+ "description": "This domain exposes DOM read/write operations. Each DOM Node is represented with its mirror object\nthat has an `id`. This `id` can be used to get additional information on the Node, resolve it into\nthe JavaScript object wrapper, etc. It is important that client receives DOM events only for the\nnodes that are known to the client. Backend keeps track of the nodes that were sent to the client\nand never sends the same node twice. It is client's responsibility to collect information about\nthe nodes that were sent to the client. Note that `iframe` owner elements will return\ncorresponding document elements as their child nodes.",
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "NodeId",
+ "description": "Unique DOM node identifier.",
+ "type": "integer"
+ },
+ {
+ "id": "BackendNodeId",
+ "description": "Unique DOM node identifier used to reference a node that may not have been pushed to the\nfront-end.",
+ "type": "integer"
+ },
+ {
+ "id": "BackendNode",
+ "description": "Backend node with a friendly name.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "$ref": "BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "PseudoType",
+ "description": "Pseudo element type.",
+ "type": "string",
+ "enum": [
+ "first-line",
+ "first-letter",
+ "before",
+ "after",
+ "marker",
+ "backdrop",
+ "selection",
+ "target-text",
+ "spelling-error",
+ "grammar-error",
+ "highlight",
+ "first-line-inherited",
+ "scrollbar",
+ "scrollbar-thumb",
+ "scrollbar-button",
+ "scrollbar-track",
+ "scrollbar-track-piece",
+ "scrollbar-corner",
+ "resizer",
+ "input-list-button",
+ "view-transition",
+ "view-transition-group",
+ "view-transition-image-pair",
+ "view-transition-old",
+ "view-transition-new"
+ ]
+ },
+ {
+ "id": "ShadowRootType",
+ "description": "Shadow root type.",
+ "type": "string",
+ "enum": [
+ "user-agent",
+ "open",
+ "closed"
+ ]
+ },
+ {
+ "id": "CompatibilityMode",
+ "description": "Document compatibility mode.",
+ "type": "string",
+ "enum": [
+ "QuirksMode",
+ "LimitedQuirksMode",
+ "NoQuirksMode"
+ ]
+ },
+ {
+ "id": "PhysicalAxes",
+ "description": "ContainerSelector physical axes",
+ "type": "string",
+ "enum": [
+ "Horizontal",
+ "Vertical",
+ "Both"
+ ]
+ },
+ {
+ "id": "LogicalAxes",
+ "description": "ContainerSelector logical axes",
+ "type": "string",
+ "enum": [
+ "Inline",
+ "Block",
+ "Both"
+ ]
+ },
+ {
+ "id": "Node",
+ "description": "DOM interaction is implemented in terms of mirror objects that represent the actual DOM nodes.\nDOMNode is a base node mirror type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "description": "Node identifier that is passed into the rest of the DOM messages as the `nodeId`. Backend\nwill only push node with given `id` once. It is aware of all requested nodes and will only\nfire DOM events for nodes known to the client.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "parentId",
+ "description": "The id of the parent node if any.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "The BackendNodeId for this node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "localName",
+ "description": "`Node`'s localName.",
+ "type": "string"
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "type": "string"
+ },
+ {
+ "name": "childNodeCount",
+ "description": "Child count for `Container` nodes.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child nodes of this node when requested with children.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of the `Element` node in the form of flat array `[name1, value1, name2, value2]`.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType`'s publicId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType`'s systemId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "internalSubset",
+ "description": "`DocumentType`'s internalSubset.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "xmlVersion",
+ "description": "`Document`'s XML version in case of XML documents.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "`Attr`'s name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "`Attr`'s value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Pseudo element type for this node.",
+ "optional": true,
+ "$ref": "PseudoType"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element identifier for this node. Only present if there is a\nvalid pseudoType.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Shadow root type.",
+ "optional": true,
+ "$ref": "ShadowRootType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentDocument",
+ "description": "Content document for frame owner elements.",
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "shadowRoots",
+ "description": "Shadow root list for given element host.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "templateContent",
+ "description": "Content document fragment for template elements.",
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "pseudoElements",
+ "description": "Pseudo elements associated with this node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ },
+ {
+ "name": "importedDocument",
+ "description": "Deprecated, as the HTML Imports API has been removed (crbug.com/937746).\nThis property used to return the imported document for the HTMLImport links.\nThe property is always undefined now.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Node"
+ },
+ {
+ "name": "distributedNodes",
+ "description": "Distributed nodes for given insertion point.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "BackendNode"
+ }
+ },
+ {
+ "name": "isSVG",
+ "description": "Whether the node is SVG.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "compatibilityMode",
+ "optional": true,
+ "$ref": "CompatibilityMode"
+ },
+ {
+ "name": "assignedSlot",
+ "optional": true,
+ "$ref": "BackendNode"
+ }
+ ]
+ },
+ {
+ "id": "RGBA",
+ "description": "A structure holding an RGBA color.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "r",
+ "description": "The red component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "g",
+ "description": "The green component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "b",
+ "description": "The blue component, in the [0-255] range.",
+ "type": "integer"
+ },
+ {
+ "name": "a",
+ "description": "The alpha component, in the [0-1] range (default: 1).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Quad",
+ "description": "An array of quad vertices, x immediately followed by y for each point, points clock-wise.",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "id": "BoxModel",
+ "description": "Box model.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "content",
+ "description": "Content box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "padding",
+ "description": "Padding box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "border",
+ "description": "Border box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "margin",
+ "description": "Margin box",
+ "$ref": "Quad"
+ },
+ {
+ "name": "width",
+ "description": "Node width",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Node height",
+ "type": "integer"
+ },
+ {
+ "name": "shapeOutside",
+ "description": "Shape outside coordinates",
+ "optional": true,
+ "$ref": "ShapeOutsideInfo"
+ }
+ ]
+ },
+ {
+ "id": "ShapeOutsideInfo",
+ "description": "CSS Shape Outside details.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bounds",
+ "description": "Shape bounds",
+ "$ref": "Quad"
+ },
+ {
+ "name": "shape",
+ "description": "Shape coordinate details",
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ },
+ {
+ "name": "marginShape",
+ "description": "Margin shape bounds",
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Rect",
+ "description": "Rectangle.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X coordinate",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "CSSComputedStyleProperty",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Computed style property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Computed style property value.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "collectClassNamesFromSubtree",
+ "description": "Collects class names for the node with given id and all of it's child nodes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to collect class names.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "classNames",
+ "description": "Class name list.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "copyTo",
+ "description": "Creates a deep copy of the specified node and places it into the target container before the\ngiven anchor.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to copy.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "targetNodeId",
+ "description": "Id of the element to drop the copy into.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "insertBeforeNodeId",
+ "description": "Drop the copy before this node (if absent, the copy becomes the last child of\n`targetNodeId`).",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node clone.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "describeNode",
+ "description": "Describes node given its id, does not require domain to be enabled. Does not start tracking any\nobjects, can be used for automation.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "node",
+ "description": "Node description.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "scrollIntoViewIfNeeded",
+ "description": "Scrolls the specified rect of the given node into view if not already visible.\nNote: exactly one between nodeId, backendNodeId and objectId should be passed\nto identify the node.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "rect",
+ "description": "The rect to be scrolled into view, relative to the node's border box, in CSS pixels.\nWhen omitted, center of the node will be used, similar to Element.scrollIntoView.",
+ "optional": true,
+ "$ref": "Rect"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables DOM agent for the given page."
+ },
+ {
+ "name": "discardSearchResults",
+ "description": "Discards search results from the session with the given id. `getSearchResults` should no longer\nbe called for that search.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "enable",
+ "description": "Enables DOM agent for the given page.",
+ "parameters": [
+ {
+ "name": "includeWhitespace",
+ "description": "Whether to include whitespaces in the children array of returned Nodes.",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "all"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "focus",
+ "description": "Focuses the given element.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "getAttributes",
+ "description": "Returns attributes for the specified node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to retrieve attibutes for.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "attributes",
+ "description": "An interleaved array of node attribute names and values.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getBoxModel",
+ "description": "Returns boxes for the given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "model",
+ "description": "Box model for the node.",
+ "$ref": "BoxModel"
+ }
+ ]
+ },
+ {
+ "name": "getContentQuads",
+ "description": "Returns quads that describe node position on the page. This method\nmight return multiple quads for inline nodes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "quads",
+ "description": "Quads that describe node layout relative to viewport.",
+ "type": "array",
+ "items": {
+ "$ref": "Quad"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getDocument",
+ "description": "Returns the root DOM node (and optionally the subtree) to the caller.\nImplicitly enables the DOM domain events for the current target.",
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "root",
+ "description": "Resulting node.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "getFlattenedDocument",
+ "description": "Returns the root DOM node (and optionally the subtree) to the caller.\nDeprecated, as it is not designed to work well with the rest of the DOM agent.\nUse DOMSnapshot.captureSnapshot instead.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodes",
+ "description": "Resulting node.",
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getNodesForSubtreeByStyle",
+ "description": "Finds nodes with a given computed style in a subtree.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Node ID pointing to the root of a subtree.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "computedStyles",
+ "description": "The style to filter nodes by (includes nodes if any of properties matches).",
+ "type": "array",
+ "items": {
+ "$ref": "CSSComputedStyleProperty"
+ }
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots in the same target should be traversed when returning the\nresults (default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Resulting nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getNodeForLocation",
+ "description": "Returns node id at given location. Depending on whether DOM domain is enabled, nodeId is\neither returned or not.",
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate.",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate.",
+ "type": "integer"
+ },
+ {
+ "name": "includeUserAgentShadowDOM",
+ "description": "False to skip to the nearest non-UA shadow root ancestor (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "ignorePointerEventsNone",
+ "description": "Whether to ignore pointer-events: none on elements and hit test them.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backendNodeId",
+ "description": "Resulting node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame this node belongs to.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node at given coordinates, only when enabled and requested document.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getOuterHTML",
+ "description": "Returns node's HTML markup.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "outerHTML",
+ "description": "Outer HTML markup.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getRelayoutBoundary",
+ "description": "Returns the id of the nearest ancestor that is a relayout boundary.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Relayout boundary node id for the given node.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getSearchResults",
+ "description": "Returns search results from given `fromIndex` to given `toIndex` from the search with the given\nidentifier.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ },
+ {
+ "name": "fromIndex",
+ "description": "Start index of the search result to be returned.",
+ "type": "integer"
+ },
+ {
+ "name": "toIndex",
+ "description": "End index of the search result to be returned.",
+ "type": "integer"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the search result nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "hideHighlight",
+ "description": "Hides any highlight.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "highlightNode",
+ "description": "Highlights DOM node.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "highlightRect",
+ "description": "Highlights given rectangle.",
+ "redirect": "Overlay"
+ },
+ {
+ "name": "markUndoableState",
+ "description": "Marks last undoable state.",
+ "experimental": true
+ },
+ {
+ "name": "moveTo",
+ "description": "Moves node into the new container, places it before the given anchor.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to move.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "targetNodeId",
+ "description": "Id of the element to drop the moved node into.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "insertBeforeNodeId",
+ "description": "Drop node before this one (if absent, the moved node becomes the last child of\n`targetNodeId`).",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "New id of the moved node.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "performSearch",
+ "description": "Searches for a given string in the DOM tree. Use `getSearchResults` to access search results or\n`cancelSearch` to end this search session.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "query",
+ "description": "Plain text or query selector or XPath search query.",
+ "type": "string"
+ },
+ {
+ "name": "includeUserAgentShadowDOM",
+ "description": "True to search in user agent shadow DOM.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "searchId",
+ "description": "Unique search session identifier.",
+ "type": "string"
+ },
+ {
+ "name": "resultCount",
+ "description": "Number of search results.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "pushNodeByPathToFrontend",
+ "description": "Requests that the node is sent to the caller given its path. // FIXME, use XPath",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "path",
+ "description": "Path to node in the proprietary format.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node for given path.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "pushNodesByBackendIdsToFrontend",
+ "description": "Requests that a batch of nodes is sent to the caller given their backend node ids.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "backendNodeIds",
+ "description": "The array of backend node ids.",
+ "type": "array",
+ "items": {
+ "$ref": "BackendNodeId"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "The array of ids of pushed nodes that correspond to the backend ids specified in\nbackendNodeIds.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "querySelector",
+ "description": "Executes `querySelector` on a given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to query upon.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "selector",
+ "description": "Selector string.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Query selector result.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "querySelectorAll",
+ "description": "Executes `querySelectorAll` on a given node.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to query upon.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "selector",
+ "description": "Selector string.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Query selector result.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getTopLayerElements",
+ "description": "Returns NodeIds of current top layer elements.\nTop layer is rendered closest to the user within a viewport, therefore its elements always\nappear on top of all other content.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "NodeIds of top layer elements",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "redo",
+ "description": "Re-does the last undone action.",
+ "experimental": true
+ },
+ {
+ "name": "removeAttribute",
+ "description": "Removes attribute with given name from an element with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to remove attribute from.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Name of the attribute to remove.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeNode",
+ "description": "Removes node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to remove.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "requestChildNodes",
+ "description": "Requests that children of the node with given id are returned to the caller in form of\n`setChildNodes` events where not only immediate children are retrieved, but all children down to\nthe specified depth.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get children for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the sub-tree\n(default is false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "requestNode",
+ "description": "Requests that the node is sent to the caller given the JavaScript node object reference. All\nnodes that form the path from the node to the root are also sent to the client as a series of\n`setChildNodes` notifications.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "JavaScript object id to convert into node.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "Node id for given object.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "resolveNode",
+ "description": "Resolves the JavaScript node object for a given NodeId or BackendNodeId.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to resolve.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Backend identifier of the node to resolve.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Execution context in which to resolve the node.",
+ "optional": true,
+ "$ref": "Runtime.ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "object",
+ "description": "JavaScript object wrapper for given node.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "setAttributeValue",
+ "description": "Sets attribute for an element with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to set attribute for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setAttributesAsText",
+ "description": "Sets attributes on element with given id. This method is useful when user edits some existing\nattribute value and types in several attribute name/value pairs.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the element to set attributes for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "text",
+ "description": "Text with a number of attributes. Will parse this text using HTML parser.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name to replace with new attributes derived from text in case text parsed\nsuccessfully.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setFileInputFiles",
+ "description": "Sets files for the given file input element.",
+ "parameters": [
+ {
+ "name": "files",
+ "description": "Array of file paths to set.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node.",
+ "optional": true,
+ "$ref": "NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node.",
+ "optional": true,
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeStackTracesEnabled",
+ "description": "Sets if stack traces should be captured for Nodes. See `Node.getNodeStackTraces`. Default is disabled.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "description": "Enable or disable.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getNodeStackTraces",
+ "description": "Gets stack traces associated with a Node. As of now, only provides stack trace for Node creation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get stack traces for.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "creation",
+ "description": "Creation stack trace, if available.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "getFileInfo",
+ "description": "Returns file information for the given\nFile wrapper.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node wrapper.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "path",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setInspectedNode",
+ "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions).",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "DOM node id to be accessible by means of $x command line API.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeName",
+ "description": "Sets node name for a node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set name for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "New node's name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "New node's id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setNodeValue",
+ "description": "Sets node value for a node with given id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set value for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "value",
+ "description": "New node's value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setOuterHTML",
+ "description": "Sets node HTML markup, returns new node id.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to set markup for.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "outerHTML",
+ "description": "Outer HTML markup to set.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "undo",
+ "description": "Undoes the last performed action.",
+ "experimental": true
+ },
+ {
+ "name": "getFrameOwner",
+ "description": "Returns iframe node that owns iframe with the given domain.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "backendNodeId",
+ "description": "Resulting node.",
+ "$ref": "BackendNodeId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node at given coordinates, only when enabled and requested document.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getContainerForNode",
+ "description": "Returns the query container of the given node based on container query\nconditions: containerName, physical, and logical axes. If no axes are\nprovided, the style container is returned, which is the direct parent or the\nclosest element with a matching container-name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "containerName",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "physicalAxes",
+ "optional": true,
+ "$ref": "PhysicalAxes"
+ },
+ {
+ "name": "logicalAxes",
+ "optional": true,
+ "$ref": "LogicalAxes"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeId",
+ "description": "The container node for the given node, or null if not found.",
+ "optional": true,
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "getQueryingDescendantsForContainer",
+ "description": "Returns the descendants of a container query container that have\ncontainer queries against this container.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the container node to find querying descendants from.",
+ "$ref": "NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "nodeIds",
+ "description": "Descendant nodes with container queries against the given container.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "attributeModified",
+ "description": "Fired when `Element`'s attribute is modified.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "Attribute name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "attributeRemoved",
+ "description": "Fired when `Element`'s attribute is removed.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "name",
+ "description": "A ttribute name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "characterDataModified",
+ "description": "Mirrors `DOMCharacterDataModified` event.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "characterData",
+ "description": "New text value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "childNodeCountUpdated",
+ "description": "Fired when `Container`'s child node count has changed.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "childNodeCount",
+ "description": "New node count.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "childNodeInserted",
+ "description": "Mirrors `DOMNodeInserted` event.",
+ "parameters": [
+ {
+ "name": "parentNodeId",
+ "description": "Id of the node that has changed.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "previousNodeId",
+ "description": "Id of the previous sibling.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "node",
+ "description": "Inserted node data.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "childNodeRemoved",
+ "description": "Mirrors `DOMNodeRemoved` event.",
+ "parameters": [
+ {
+ "name": "parentNodeId",
+ "description": "Parent id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the node that has been removed.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "distributedNodesUpdated",
+ "description": "Called when distribution is changed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "insertionPointId",
+ "description": "Insertion point where distributed nodes were updated.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "distributedNodes",
+ "description": "Distributed nodes for given insertion point.",
+ "type": "array",
+ "items": {
+ "$ref": "BackendNode"
+ }
+ }
+ ]
+ },
+ {
+ "name": "documentUpdated",
+ "description": "Fired when `Document` has been totally updated. Node ids are no longer valid."
+ },
+ {
+ "name": "inlineStyleInvalidated",
+ "description": "Fired when `Element`'s inline style is modified via a CSS property modification.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the nodes for which the inline styles have been invalidated.",
+ "type": "array",
+ "items": {
+ "$ref": "NodeId"
+ }
+ }
+ ]
+ },
+ {
+ "name": "pseudoElementAdded",
+ "description": "Called when a pseudo element is added to an element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Pseudo element's parent element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "pseudoElement",
+ "description": "The added pseudo element.",
+ "$ref": "Node"
+ }
+ ]
+ },
+ {
+ "name": "topLayerElementsUpdated",
+ "description": "Called when top layer elements are changed.",
+ "experimental": true
+ },
+ {
+ "name": "pseudoElementRemoved",
+ "description": "Called when a pseudo element is removed from an element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Pseudo element's parent element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "pseudoElementId",
+ "description": "The removed pseudo element id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "setChildNodes",
+ "description": "Fired when backend wants to provide client with the missing DOM structure. This happens upon\nmost of the calls requesting node ids.",
+ "parameters": [
+ {
+ "name": "parentId",
+ "description": "Parent node id to populate with children.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "nodes",
+ "description": "Child nodes array.",
+ "type": "array",
+ "items": {
+ "$ref": "Node"
+ }
+ }
+ ]
+ },
+ {
+ "name": "shadowRootPopped",
+ "description": "Called when shadow root is popped from the element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hostId",
+ "description": "Host element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "rootId",
+ "description": "Shadow root id.",
+ "$ref": "NodeId"
+ }
+ ]
+ },
+ {
+ "name": "shadowRootPushed",
+ "description": "Called when shadow root is pushed into the element.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hostId",
+ "description": "Host element id.",
+ "$ref": "NodeId"
+ },
+ {
+ "name": "root",
+ "description": "Shadow root.",
+ "$ref": "Node"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMDebugger",
+ "description": "DOM debugging allows setting breakpoints on particular DOM operations and events. JavaScript\nexecution will stop on these operations as if there was a regular breakpoint set.",
+ "dependencies": [
+ "DOM",
+ "Debugger",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "DOMBreakpointType",
+ "description": "DOM breakpoint type.",
+ "type": "string",
+ "enum": [
+ "subtree-modified",
+ "attribute-modified",
+ "node-removed"
+ ]
+ },
+ {
+ "id": "CSPViolationType",
+ "description": "CSP Violation type.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "trustedtype-sink-violation",
+ "trustedtype-policy-violation"
+ ]
+ },
+ {
+ "id": "EventListener",
+ "description": "Object event listener.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "`EventListener`'s type.",
+ "type": "string"
+ },
+ {
+ "name": "useCapture",
+ "description": "`EventListener`'s useCapture.",
+ "type": "boolean"
+ },
+ {
+ "name": "passive",
+ "description": "`EventListener`'s passive flag.",
+ "type": "boolean"
+ },
+ {
+ "name": "once",
+ "description": "`EventListener`'s once flag.",
+ "type": "boolean"
+ },
+ {
+ "name": "scriptId",
+ "description": "Script id of the handler code.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "handler",
+ "description": "Event handler function value.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "originalHandler",
+ "description": "Event original handler function value.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Node the listener is added to (if any).",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getEventListeners",
+ "description": "Returns event listeners of the given object.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to return listeners for.",
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "depth",
+ "description": "The maximum depth at which Node children should be retrieved, defaults to 1. Use -1 for the\nentire subtree or provide an integer larger than 0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pierce",
+ "description": "Whether or not iframes and shadow roots should be traversed when returning the subtree\n(default is false). Reports listeners for all contexts if pierce is enabled.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "listeners",
+ "description": "Array of relevant listeners.",
+ "type": "array",
+ "items": {
+ "$ref": "EventListener"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeDOMBreakpoint",
+ "description": "Removes DOM breakpoint that was set using `setDOMBreakpoint`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to remove breakpoint from.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "type",
+ "description": "Type of the breakpoint to remove.",
+ "$ref": "DOMBreakpointType"
+ }
+ ]
+ },
+ {
+ "name": "removeEventListenerBreakpoint",
+ "description": "Removes breakpoint on particular DOM event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Event name.",
+ "type": "string"
+ },
+ {
+ "name": "targetName",
+ "description": "EventTarget interface name.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeInstrumentationBreakpoint",
+ "description": "Removes breakpoint on particular native event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeXHRBreakpoint",
+ "description": "Removes breakpoint from XMLHttpRequest.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Resource URL substring.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setBreakOnCSPViolation",
+ "description": "Sets breakpoint on particular CSP violations.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "violationTypes",
+ "description": "CSP Violations to stop upon.",
+ "type": "array",
+ "items": {
+ "$ref": "CSPViolationType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setDOMBreakpoint",
+ "description": "Sets breakpoint on particular operation with DOM.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to set breakpoint on.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "type",
+ "description": "Type of the operation to stop upon.",
+ "$ref": "DOMBreakpointType"
+ }
+ ]
+ },
+ {
+ "name": "setEventListenerBreakpoint",
+ "description": "Sets breakpoint on particular DOM event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "DOM Event name to stop on (any DOM event will do).",
+ "type": "string"
+ },
+ {
+ "name": "targetName",
+ "description": "EventTarget interface name to stop on. If equal to `\"*\"` or not provided, will stop on any\nEventTarget.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets breakpoint on particular native event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setXHRBreakpoint",
+ "description": "Sets breakpoint on XMLHttpRequest.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Resource URL substring. All XHRs having this substring in the URL will get stopped upon.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "EventBreakpoints",
+ "description": "EventBreakpoints permits setting breakpoints on particular operations and\nevents in targets that run JavaScript but do not have a DOM.\nJavaScript execution will stop on these operations as if there was a regular\nbreakpoint set.",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets breakpoint on particular native event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeInstrumentationBreakpoint",
+ "description": "Removes breakpoint on particular native event.",
+ "parameters": [
+ {
+ "name": "eventName",
+ "description": "Instrumentation name to stop on.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMSnapshot",
+ "description": "This domain facilitates obtaining document snapshots with DOM, layout, and style information.",
+ "experimental": true,
+ "dependencies": [
+ "CSS",
+ "DOM",
+ "DOMDebugger",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "DOMNode",
+ "description": "A Node in the DOM tree.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "type": "integer"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "type": "string"
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "type": "string"
+ },
+ {
+ "name": "textValue",
+ "description": "Only set for textarea elements, contains the text value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inputValue",
+ "description": "Only set for input elements, contains the input's associated text value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inputChecked",
+ "description": "Only set for radio and checkbox input elements, indicates if the element has been checked",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "optionSelected",
+ "description": "Only set for option elements, indicates if the element has been selected",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.",
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "childNodeIndexes",
+ "description": "The indexes of the node's child nodes in the `domNodes` array returned by `getSnapshot`, if\nany.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of an `Element` node.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "NameValue"
+ }
+ },
+ {
+ "name": "pseudoElementIndexes",
+ "description": "Indexes of pseudo elements associated with this node in the `domNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "layoutNodeIndex",
+ "description": "The index of the node's related layout tree node in the `layoutTreeNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "contentLanguage",
+ "description": "Only set for documents, contains the document's content language.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "documentEncoding",
+ "description": "Only set for documents, contains the document's character set encoding.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType` node's publicId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType` node's systemId.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements and also for the document node.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentDocumentIndex",
+ "description": "The index of a frame owner element's content document in the `domNodes` array returned by\n`getSnapshot`, if any.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Type of a pseudo element node.",
+ "optional": true,
+ "$ref": "DOM.PseudoType"
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Shadow root type.",
+ "optional": true,
+ "$ref": "DOM.ShadowRootType"
+ },
+ {
+ "name": "isClickable",
+ "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "eventListeners",
+ "description": "Details of the node's event listeners, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "DOMDebugger.EventListener"
+ }
+ },
+ {
+ "name": "currentSourceURL",
+ "description": "The selected url for nodes with a srcset attribute.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originURL",
+ "description": "The url of the script (if any) that generates this node.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Scroll offsets, set when this node is a Document.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "InlineTextBox",
+ "description": "Details of post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "boundingBox",
+ "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "startCharacterIndex",
+ "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.",
+ "type": "integer"
+ },
+ {
+ "name": "numCharacters",
+ "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "LayoutTreeNode",
+ "description": "Details of an element in the DOM tree with a LayoutObject.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "domNodeIndex",
+ "description": "The index of the related DOM node in the `domNodes` array returned by `getSnapshot`.",
+ "type": "integer"
+ },
+ {
+ "name": "boundingBox",
+ "description": "The bounding box in document coordinates. Note that scroll offset of the document is ignored.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "layoutText",
+ "description": "Contents of the LayoutText, if any.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "inlineTextNodes",
+ "description": "The post-layout inline text nodes, if any.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InlineTextBox"
+ }
+ },
+ {
+ "name": "styleIndex",
+ "description": "Index into the `computedStyles` array returned by `getSnapshot`.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "paintOrder",
+ "description": "Global paint order index, which is determined by the stacking order of the nodes. Nodes\nthat are painted together will have the same index. Only provided if includePaintOrder in\ngetSnapshot was true.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "isStackingContext",
+ "description": "Set to true to indicate the element begins a new stacking context.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ComputedStyle",
+ "description": "A subset of the full ComputedStyle as defined by the request whitelist.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "properties",
+ "description": "Name/value pairs of computed style properties.",
+ "type": "array",
+ "items": {
+ "$ref": "NameValue"
+ }
+ }
+ ]
+ },
+ {
+ "id": "NameValue",
+ "description": "A name/value pair.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Attribute/property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Attribute/property value.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "StringIndex",
+ "description": "Index of the string in the strings table.",
+ "type": "integer"
+ },
+ {
+ "id": "ArrayOfStrings",
+ "description": "Index of the string in the strings table.",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "id": "RareStringData",
+ "description": "Data that is only present on rare nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RareBooleanData",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "RareIntegerData",
+ "type": "object",
+ "properties": [
+ {
+ "name": "index",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Rectangle",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "id": "DocumentSnapshot",
+ "description": "Document snapshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "documentURL",
+ "description": "Document URL that `Document` or `FrameOwner` node points to.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "title",
+ "description": "Document title.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "baseURL",
+ "description": "Base URL that `Document` or `FrameOwner` node uses for URL completion.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "contentLanguage",
+ "description": "Contains the document's content language.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "encodingName",
+ "description": "Contains the document's character set encoding.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "publicId",
+ "description": "`DocumentType` node's publicId.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "systemId",
+ "description": "`DocumentType` node's systemId.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame ID for frame owner elements and also for the document node.",
+ "$ref": "StringIndex"
+ },
+ {
+ "name": "nodes",
+ "description": "A table with dom nodes.",
+ "$ref": "NodeTreeSnapshot"
+ },
+ {
+ "name": "layout",
+ "description": "The nodes in the layout tree.",
+ "$ref": "LayoutTreeSnapshot"
+ },
+ {
+ "name": "textBoxes",
+ "description": "The post-layout inline text nodes.",
+ "$ref": "TextBoxSnapshot"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Horizontal scroll offset.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "description": "Vertical scroll offset.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "contentWidth",
+ "description": "Document content width.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "contentHeight",
+ "description": "Document content height.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "NodeTreeSnapshot",
+ "description": "Table containing nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "parentIndex",
+ "description": "Parent node index.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "nodeType",
+ "description": "`Node`'s nodeType.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "shadowRootType",
+ "description": "Type of the shadow root the `Node` is in. String values are equal to the `ShadowRootType` enum.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "nodeName",
+ "description": "`Node`'s nodeName.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "nodeValue",
+ "description": "`Node`'s nodeValue.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "backendNodeId",
+ "description": "`Node`'s id, corresponds to DOM.Node.backendNodeId.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "DOM.BackendNodeId"
+ }
+ },
+ {
+ "name": "attributes",
+ "description": "Attributes of an `Element` node. Flatten name, value pairs.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ArrayOfStrings"
+ }
+ },
+ {
+ "name": "textValue",
+ "description": "Only set for textarea elements, contains the text value.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "inputValue",
+ "description": "Only set for input elements, contains the input's associated text value.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "inputChecked",
+ "description": "Only set for radio and checkbox input elements, indicates if the element has been checked",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "optionSelected",
+ "description": "Only set for option elements, indicates if the element has been selected",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "contentDocumentIndex",
+ "description": "The index of the document in the list of the snapshot documents.",
+ "optional": true,
+ "$ref": "RareIntegerData"
+ },
+ {
+ "name": "pseudoType",
+ "description": "Type of a pseudo element node.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "pseudoIdentifier",
+ "description": "Pseudo element identifier for this node. Only present if there is a\nvalid pseudoType.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "isClickable",
+ "description": "Whether this DOM node responds to mouse clicks. This includes nodes that have had click\nevent listeners attached via JavaScript as well as anchor tags that naturally navigate when\nclicked.",
+ "optional": true,
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "currentSourceURL",
+ "description": "The selected url for nodes with a srcset attribute.",
+ "optional": true,
+ "$ref": "RareStringData"
+ },
+ {
+ "name": "originURL",
+ "description": "The url of the script (if any) that generates this node.",
+ "optional": true,
+ "$ref": "RareStringData"
+ }
+ ]
+ },
+ {
+ "id": "LayoutTreeSnapshot",
+ "description": "Table of details of an element in the DOM tree with a LayoutObject.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeIndex",
+ "description": "Index of the corresponding node in the `NodeTreeSnapshot` array returned by `captureSnapshot`.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "styles",
+ "description": "Array of indexes specifying computed style strings, filtered according to the `computedStyles` parameter passed to `captureSnapshot`.",
+ "type": "array",
+ "items": {
+ "$ref": "ArrayOfStrings"
+ }
+ },
+ {
+ "name": "bounds",
+ "description": "The absolute position bounding box.",
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "text",
+ "description": "Contents of the LayoutText, if any.",
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "stackingContexts",
+ "description": "Stacking context information.",
+ "$ref": "RareBooleanData"
+ },
+ {
+ "name": "paintOrders",
+ "description": "Global paint order index, which is determined by the stacking order of the nodes. Nodes\nthat are painted together will have the same index. Only provided if includePaintOrder in\ncaptureSnapshot was true.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "offsetRects",
+ "description": "The offset rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "scrollRects",
+ "description": "The scroll rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "clientRects",
+ "description": "The client rect of nodes. Only available when includeDOMRects is set to true",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "blendedBackgroundColors",
+ "description": "The list of background colors that are blended with colors of overlapping elements.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "StringIndex"
+ }
+ },
+ {
+ "name": "textColorOpacities",
+ "description": "The list of computed text opacities.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TextBoxSnapshot",
+ "description": "Table of details of the post layout rendered text positions. The exact layout should not be regarded as\nstable and may change between versions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "layoutIndex",
+ "description": "Index of the layout tree node that owns this box collection.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "bounds",
+ "description": "The absolute position bounding box.",
+ "type": "array",
+ "items": {
+ "$ref": "Rectangle"
+ }
+ },
+ {
+ "name": "start",
+ "description": "The starting index in characters, for this post layout textbox substring. Characters that\nwould be represented as a surrogate pair in UTF-16 have length 2.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "length",
+ "description": "The number of characters in this post layout textbox substring. Characters that would be\nrepresented as a surrogate pair in UTF-16 have length 2.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables DOM snapshot agent for the given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables DOM snapshot agent for the given page."
+ },
+ {
+ "name": "getSnapshot",
+ "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "computedStyleWhitelist",
+ "description": "Whitelist of computed styles to return.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "includeEventListeners",
+ "description": "Whether or not to retrieve details of DOM listeners (default false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includePaintOrder",
+ "description": "Whether to determine and include the paint order index of LayoutTreeNodes (default false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeUserAgentShadowTree",
+ "description": "Whether to include UA shadow tree in the snapshot (default false).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "domNodes",
+ "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.",
+ "type": "array",
+ "items": {
+ "$ref": "DOMNode"
+ }
+ },
+ {
+ "name": "layoutTreeNodes",
+ "description": "The nodes in the layout tree.",
+ "type": "array",
+ "items": {
+ "$ref": "LayoutTreeNode"
+ }
+ },
+ {
+ "name": "computedStyles",
+ "description": "Whitelisted ComputedStyle properties for each node in the layout tree.",
+ "type": "array",
+ "items": {
+ "$ref": "ComputedStyle"
+ }
+ }
+ ]
+ },
+ {
+ "name": "captureSnapshot",
+ "description": "Returns a document snapshot, including the full DOM tree of the root node (including iframes,\ntemplate contents, and imported documents) in a flattened array, as well as layout and\nwhite-listed computed style information for the nodes. Shadow DOM in the returned DOM tree is\nflattened.",
+ "parameters": [
+ {
+ "name": "computedStyles",
+ "description": "Whitelist of computed styles to return.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "includePaintOrder",
+ "description": "Whether to include layout object paint orders into the snapshot.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeDOMRects",
+ "description": "Whether to include DOM rectangles (offsetRects, clientRects, scrollRects) into the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeBlendedBackgroundColors",
+ "description": "Whether to include blended background colors in the snapshot (default: false).\nBlended background color is achieved by blending background colors of all elements\nthat overlap with the current element.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeTextColorOpacities",
+ "description": "Whether to include text color opacity in the snapshot (default: false).\nAn element might have the opacity property set that affects the text color of the element.\nThe final text color opacity is computed based on the opacity of all overlapping elements.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "documents",
+ "description": "The nodes in the DOM tree. The DOMNode at index 0 corresponds to the root document.",
+ "type": "array",
+ "items": {
+ "$ref": "DocumentSnapshot"
+ }
+ },
+ {
+ "name": "strings",
+ "description": "Shared string table that all string properties refer to with indexes.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DOMStorage",
+ "description": "Query and modify DOM storage.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "SerializedStorageKey",
+ "type": "string"
+ },
+ {
+ "id": "StorageId",
+ "description": "DOM Storage identifier.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityOrigin",
+ "description": "Security origin for the storage.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Represents a key by which DOM Storage keys its CachedStorageAreas",
+ "optional": true,
+ "$ref": "SerializedStorageKey"
+ },
+ {
+ "name": "isLocalStorage",
+ "description": "Whether the storage is local storage (not session storage).",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Item",
+ "description": "DOM Storage item.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "commands": [
+ {
+ "name": "clear",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables storage tracking, prevents storage events from being sent to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables storage tracking, storage events will now be delivered to the client."
+ },
+ {
+ "name": "getDOMStorageItems",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entries",
+ "type": "array",
+ "items": {
+ "$ref": "Item"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeDOMStorageItem",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setDOMStorageItem",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "domStorageItemAdded",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemRemoved",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemUpdated",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "oldValue",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "domStorageItemsCleared",
+ "parameters": [
+ {
+ "name": "storageId",
+ "$ref": "StorageId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Database",
+ "experimental": true,
+ "types": [
+ {
+ "id": "DatabaseId",
+ "description": "Unique identifier of Database object.",
+ "type": "string"
+ },
+ {
+ "id": "Database",
+ "description": "Database object.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Database ID.",
+ "$ref": "DatabaseId"
+ },
+ {
+ "name": "domain",
+ "description": "Database domain.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Database version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Error",
+ "description": "Database error.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Error code.",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables database tracking, prevents database events from being sent to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables database tracking, database events will now be delivered to the client."
+ },
+ {
+ "name": "executeSQL",
+ "parameters": [
+ {
+ "name": "databaseId",
+ "$ref": "DatabaseId"
+ },
+ {
+ "name": "query",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "columnNames",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "values",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "any"
+ }
+ },
+ {
+ "name": "sqlError",
+ "optional": true,
+ "$ref": "Error"
+ }
+ ]
+ },
+ {
+ "name": "getDatabaseTableNames",
+ "parameters": [
+ {
+ "name": "databaseId",
+ "$ref": "DatabaseId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "tableNames",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "addDatabase",
+ "parameters": [
+ {
+ "name": "database",
+ "$ref": "Database"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "DeviceOrientation",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "clearDeviceOrientationOverride",
+ "description": "Clears the overridden Device Orientation."
+ },
+ {
+ "name": "setDeviceOrientationOverride",
+ "description": "Overrides the Device Orientation.",
+ "parameters": [
+ {
+ "name": "alpha",
+ "description": "Mock alpha",
+ "type": "number"
+ },
+ {
+ "name": "beta",
+ "description": "Mock beta",
+ "type": "number"
+ },
+ {
+ "name": "gamma",
+ "description": "Mock gamma",
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Emulation",
+ "description": "This domain emulates different environments for the page.",
+ "dependencies": [
+ "DOM",
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ScreenOrientation",
+ "description": "Screen orientation.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Orientation type.",
+ "type": "string",
+ "enum": [
+ "portraitPrimary",
+ "portraitSecondary",
+ "landscapePrimary",
+ "landscapeSecondary"
+ ]
+ },
+ {
+ "name": "angle",
+ "description": "Orientation angle.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "DisplayFeature",
+ "type": "object",
+ "properties": [
+ {
+ "name": "orientation",
+ "description": "Orientation of a display feature in relation to screen",
+ "type": "string",
+ "enum": [
+ "vertical",
+ "horizontal"
+ ]
+ },
+ {
+ "name": "offset",
+ "description": "The offset from the screen origin in either the x (for vertical\norientation) or y (for horizontal orientation) direction.",
+ "type": "integer"
+ },
+ {
+ "name": "maskLength",
+ "description": "A display feature may mask content such that it is not physically\ndisplayed - this length along with the offset describes this area.\nA display feature that only splits content will have a 0 mask_length.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "MediaFeature",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "VirtualTimePolicy",
+ "description": "advance: If the scheduler runs out of immediate work, the virtual time base may fast forward to\nallow the next delayed task (if any) to run; pause: The virtual time base may not advance;\npauseIfNetworkFetchesPending: The virtual time base may not advance if there are any pending\nresource fetches.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "advance",
+ "pause",
+ "pauseIfNetworkFetchesPending"
+ ]
+ },
+ {
+ "id": "UserAgentBrandVersion",
+ "description": "Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "brand",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "UserAgentMetadata",
+ "description": "Used to specify User Agent Cient Hints to emulate. See https://wicg.github.io/ua-client-hints\nMissing optional values will be filled in by the target with what it would normally use.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "brands",
+ "description": "Brands appearing in Sec-CH-UA.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "UserAgentBrandVersion"
+ }
+ },
+ {
+ "name": "fullVersionList",
+ "description": "Brands appearing in Sec-CH-UA-Full-Version-List.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "UserAgentBrandVersion"
+ }
+ },
+ {
+ "name": "fullVersion",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "type": "string"
+ },
+ {
+ "name": "platformVersion",
+ "type": "string"
+ },
+ {
+ "name": "architecture",
+ "type": "string"
+ },
+ {
+ "name": "model",
+ "type": "string"
+ },
+ {
+ "name": "mobile",
+ "type": "boolean"
+ },
+ {
+ "name": "bitness",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "wow64",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "DisabledImageType",
+ "description": "Enum of image types that can be disabled.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "avif",
+ "webp"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "canEmulate",
+ "description": "Tells whether emulation is supported.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if emulation is supported.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearDeviceMetricsOverride",
+ "description": "Clears the overridden device metrics."
+ },
+ {
+ "name": "clearGeolocationOverride",
+ "description": "Clears the overridden Geolocation Position and Error."
+ },
+ {
+ "name": "resetPageScaleFactor",
+ "description": "Requests that page scale factor is reset to initial values.",
+ "experimental": true
+ },
+ {
+ "name": "setFocusEmulationEnabled",
+ "description": "Enables or disables simulating a focused and active page.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to enable to disable focus emulation.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAutoDarkModeOverride",
+ "description": "Automatically render all web contents using a dark theme.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to enable or disable automatic dark mode.\nIf not specified, any existing override will be cleared.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCPUThrottlingRate",
+ "description": "Enables CPU throttling to emulate slow CPUs.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "rate",
+ "description": "Throttling rate as a slowdown factor (1 is no throttle, 2 is 2x slowdown, etc).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setDefaultBackgroundColorOverride",
+ "description": "Sets or clears an override of the default background color of the frame. This override is used\nif the content does not specify one.",
+ "parameters": [
+ {
+ "name": "color",
+ "description": "RGBA of the default background color. If not specified, any existing override will be\ncleared.",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "setDeviceMetricsOverride",
+ "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).",
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "deviceScaleFactor",
+ "description": "Overriding device scale factor value. 0 disables the override.",
+ "type": "number"
+ },
+ {
+ "name": "mobile",
+ "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.",
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale to apply to resulting view image.",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "screenWidth",
+ "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "screenHeight",
+ "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionX",
+ "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionY",
+ "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "dontSetVisibleSize",
+ "description": "Do not set visible view size, rely upon explicit setVisibleSize call.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenOrientation",
+ "description": "Screen orientation override.",
+ "optional": true,
+ "$ref": "ScreenOrientation"
+ },
+ {
+ "name": "viewport",
+ "description": "If set, the visible area of the page will be overridden to this viewport. This viewport\nchange is not observed by the page, e.g. viewport-relative elements do not change positions.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Page.Viewport"
+ },
+ {
+ "name": "displayFeature",
+ "description": "If set, the display feature of a multi-segment screen. If not set, multi-segment support\nis turned-off.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DisplayFeature"
+ }
+ ]
+ },
+ {
+ "name": "setScrollbarsHidden",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hidden",
+ "description": "Whether scrollbars should be always hidden.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setDocumentCookieDisabled",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "disabled",
+ "description": "Whether document.coookie API should be disabled.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setEmitTouchEventsForMouse",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether touch emulation based on mouse input should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "configuration",
+ "description": "Touch/gesture events configuration. Default: current platform.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mobile",
+ "desktop"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setEmulatedMedia",
+ "description": "Emulates the given media type or media feature for CSS media queries.",
+ "parameters": [
+ {
+ "name": "media",
+ "description": "Media type to emulate. Empty string disables the override.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "features",
+ "description": "Media features to emulate.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "MediaFeature"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setEmulatedVisionDeficiency",
+ "description": "Emulates the given vision deficiency.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Vision deficiency to emulate. Order: best-effort emulations come first, followed by any\nphysiologically accurate emulations for medically recognized color vision deficiencies.",
+ "type": "string",
+ "enum": [
+ "none",
+ "blurredVision",
+ "reducedContrast",
+ "achromatopsia",
+ "deuteranopia",
+ "protanopia",
+ "tritanopia"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setGeolocationOverride",
+ "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.",
+ "parameters": [
+ {
+ "name": "latitude",
+ "description": "Mock latitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "longitude",
+ "description": "Mock longitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "accuracy",
+ "description": "Mock accuracy",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setIdleOverride",
+ "description": "Overrides the Idle state.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "isUserActive",
+ "description": "Mock isUserActive",
+ "type": "boolean"
+ },
+ {
+ "name": "isScreenUnlocked",
+ "description": "Mock isScreenUnlocked",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearIdleOverride",
+ "description": "Clears Idle state overrides.",
+ "experimental": true
+ },
+ {
+ "name": "setNavigatorOverrides",
+ "description": "Overrides value returned by the javascript navigator object.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setPageScaleFactor",
+ "description": "Sets a specified page scale factor.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "pageScaleFactor",
+ "description": "Page scale factor.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setScriptExecutionDisabled",
+ "description": "Switches script execution in the page.",
+ "parameters": [
+ {
+ "name": "value",
+ "description": "Whether script execution should be disabled in the page.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setTouchEmulationEnabled",
+ "description": "Enables touch on platforms which do not support them.",
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the touch event emulation should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "maxTouchPoints",
+ "description": "Maximum touch points supported. Defaults to one.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setVirtualTimePolicy",
+ "description": "Turns on virtual time for all frames (replacing real-time with a synthetic time source) and sets\nthe current virtual time policy. Note this supersedes any previous time budget.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "policy",
+ "$ref": "VirtualTimePolicy"
+ },
+ {
+ "name": "budget",
+ "description": "If set, after this many virtual milliseconds have elapsed virtual time will be paused and a\nvirtualTimeBudgetExpired event is sent.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "maxVirtualTimeTaskStarvationCount",
+ "description": "If set this specifies the maximum number of tasks that can be run before virtual is forced\nforwards to prevent deadlock.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "initialVirtualTime",
+ "description": "If set, base::Time::Now will be overridden to initially return this value.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ }
+ ],
+ "returns": [
+ {
+ "name": "virtualTimeTicksBase",
+ "description": "Absolute timestamp at which virtual time was first enabled (up time in milliseconds).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLocaleOverride",
+ "description": "Overrides default host system locale with the specified one.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "locale",
+ "description": "ICU style C locale (e.g. \"en_US\"). If not specified or empty, disables the override and\nrestores default host system locale.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setTimezoneOverride",
+ "description": "Overrides default host system timezone with the specified one.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "timezoneId",
+ "description": "The timezone identifier. If empty, disables the override and\nrestores default host system timezone.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setVisibleSize",
+ "description": "Resizes the frame/viewport of the page. Note that this does not affect the frame's container\n(e.g. browser window). Can be used to produce screenshots of the specified size. Not supported\non Android.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Frame width (DIP).",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Frame height (DIP).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setDisabledImageTypes",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "imageTypes",
+ "description": "Image types to disable.",
+ "type": "array",
+ "items": {
+ "$ref": "DisabledImageType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setHardwareConcurrencyOverride",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "hardwareConcurrency",
+ "description": "Hardware concurrency to report",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setUserAgentOverride",
+ "description": "Allows overriding user agent with the given string.",
+ "parameters": [
+ {
+ "name": "userAgent",
+ "description": "User agent to use.",
+ "type": "string"
+ },
+ {
+ "name": "acceptLanguage",
+ "description": "Browser langugage to emulate.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "userAgentMetadata",
+ "description": "To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData",
+ "experimental": true,
+ "optional": true,
+ "$ref": "UserAgentMetadata"
+ }
+ ]
+ },
+ {
+ "name": "setAutomationOverride",
+ "description": "Allows overriding the automation flag.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the override should be enabled.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "virtualTimeBudgetExpired",
+ "description": "Notification sent after the virtual time budget for the current VirtualTimePolicy has run out.",
+ "experimental": true
+ }
+ ]
+ },
+ {
+ "domain": "HeadlessExperimental",
+ "description": "This domain provides experimental commands only supported in headless mode.",
+ "experimental": true,
+ "dependencies": [
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ScreenshotParams",
+ "description": "Encoding options for a screenshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "format",
+ "description": "Image compression format (defaults to png).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png",
+ "webp"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100] (jpeg and webp only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "optimizeForSpeed",
+ "description": "Optimize image encoding for speed, not for resulting size (defaults to false)",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "beginFrame",
+ "description": "Sends a BeginFrame to the target and returns when the frame was completed. Optionally captures a\nscreenshot from the resulting frame. Requires that the target was created with enabled\nBeginFrameControl. Designed for use with --run-all-compositor-stages-before-draw, see also\nhttps://goo.gle/chrome-headless-rendering for more background.",
+ "parameters": [
+ {
+ "name": "frameTimeTicks",
+ "description": "Timestamp of this BeginFrame in Renderer TimeTicks (milliseconds of uptime). If not set,\nthe current time will be used.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "interval",
+ "description": "The interval between BeginFrames that is reported to the compositor, in milliseconds.\nDefaults to a 60 frames/second interval, i.e. about 16.666 milliseconds.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "noDisplayUpdates",
+ "description": "Whether updates should not be committed and drawn onto the display. False by default. If\ntrue, only side effects of the BeginFrame will be run, such as layout and animations, but\nany visual updates may not be visible on the display or in screenshots.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenshot",
+ "description": "If set, a screenshot of the frame will be captured and returned in the response. Otherwise,\nno screenshot will be captured. Note that capturing a screenshot can fail, for example,\nduring renderer initialization. In such a case, no screenshot data will be returned.",
+ "optional": true,
+ "$ref": "ScreenshotParams"
+ }
+ ],
+ "returns": [
+ {
+ "name": "hasDamage",
+ "description": "Whether the BeginFrame resulted in damage and, thus, a new frame was committed to the\ndisplay. Reported for diagnostic uses, may be removed in the future.",
+ "type": "boolean"
+ },
+ {
+ "name": "screenshotData",
+ "description": "Base64-encoded image data of the screenshot, if one was requested and successfully taken. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables headless events for the target.",
+ "deprecated": true
+ },
+ {
+ "name": "enable",
+ "description": "Enables headless events for the target.",
+ "deprecated": true
+ }
+ ]
+ },
+ {
+ "domain": "IO",
+ "description": "Input/Output operations for streams produced by DevTools.",
+ "types": [
+ {
+ "id": "StreamHandle",
+ "description": "This is either obtained from another method or specified as `blob:<uuid>` where\n`<uuid>` is an UUID of a Blob.",
+ "type": "string"
+ }
+ ],
+ "commands": [
+ {
+ "name": "close",
+ "description": "Close the stream, discard any temporary backing storage.",
+ "parameters": [
+ {
+ "name": "handle",
+ "description": "Handle of the stream to close.",
+ "$ref": "StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "read",
+ "description": "Read a chunk of the stream",
+ "parameters": [
+ {
+ "name": "handle",
+ "description": "Handle of the stream to read.",
+ "$ref": "StreamHandle"
+ },
+ {
+ "name": "offset",
+ "description": "Seek to the specified offset before reading (if not specificed, proceed with offset\nfollowing the last read). Some types of streams may only support sequential reads.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "size",
+ "description": "Maximum number of bytes to read (left upon the agent discretion if not specified).",
+ "optional": true,
+ "type": "integer"
+ }
+ ],
+ "returns": [
+ {
+ "name": "base64Encoded",
+ "description": "Set if the data is base64-encoded",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "data",
+ "description": "Data that were read.",
+ "type": "string"
+ },
+ {
+ "name": "eof",
+ "description": "Set if the end-of-file condition occurred while reading.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resolveBlob",
+ "description": "Return UUID of Blob object specified by a remote object id.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Object id of a Blob object wrapper.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "uuid",
+ "description": "UUID of the specified Blob.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "IndexedDB",
+ "experimental": true,
+ "dependencies": [
+ "Runtime",
+ "Storage"
+ ],
+ "types": [
+ {
+ "id": "DatabaseWithObjectStores",
+ "description": "Database with an array of object stores.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Database version (type is not 'integer', as the standard\nrequires the version number to be 'unsigned long long')",
+ "type": "number"
+ },
+ {
+ "name": "objectStores",
+ "description": "Object stores in this database.",
+ "type": "array",
+ "items": {
+ "$ref": "ObjectStore"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ObjectStore",
+ "description": "Object store.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Object store name.",
+ "type": "string"
+ },
+ {
+ "name": "keyPath",
+ "description": "Object store key path.",
+ "$ref": "KeyPath"
+ },
+ {
+ "name": "autoIncrement",
+ "description": "If true, object store has auto increment flag set.",
+ "type": "boolean"
+ },
+ {
+ "name": "indexes",
+ "description": "Indexes in this object store.",
+ "type": "array",
+ "items": {
+ "$ref": "ObjectStoreIndex"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ObjectStoreIndex",
+ "description": "Object store index.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Index name.",
+ "type": "string"
+ },
+ {
+ "name": "keyPath",
+ "description": "Index key path.",
+ "$ref": "KeyPath"
+ },
+ {
+ "name": "unique",
+ "description": "If true, index is unique.",
+ "type": "boolean"
+ },
+ {
+ "name": "multiEntry",
+ "description": "If true, index allows multiple entries for a key.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Key",
+ "description": "Key.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Key type.",
+ "type": "string",
+ "enum": [
+ "number",
+ "string",
+ "date",
+ "array"
+ ]
+ },
+ {
+ "name": "number",
+ "description": "Number value.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "string",
+ "description": "String value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "date",
+ "description": "Date value.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "array",
+ "description": "Array value.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Key"
+ }
+ }
+ ]
+ },
+ {
+ "id": "KeyRange",
+ "description": "Key range.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "lower",
+ "description": "Lower bound.",
+ "optional": true,
+ "$ref": "Key"
+ },
+ {
+ "name": "upper",
+ "description": "Upper bound.",
+ "optional": true,
+ "$ref": "Key"
+ },
+ {
+ "name": "lowerOpen",
+ "description": "If true lower bound is open.",
+ "type": "boolean"
+ },
+ {
+ "name": "upperOpen",
+ "description": "If true upper bound is open.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "DataEntry",
+ "description": "Data entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "description": "Key object.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "primaryKey",
+ "description": "Primary key object.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "value",
+ "description": "Value object.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "KeyPath",
+ "description": "Key path.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Key path type.",
+ "type": "string",
+ "enum": [
+ "null",
+ "string",
+ "array"
+ ]
+ },
+ {
+ "name": "string",
+ "description": "String value.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "array",
+ "description": "Array value.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clearObjectStore",
+ "description": "Clears all entries from an object store.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "deleteDatabase",
+ "description": "Deletes a database.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "deleteObjectStoreEntries",
+ "description": "Delete a range of entries from an object store",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "type": "string"
+ },
+ {
+ "name": "keyRange",
+ "description": "Range of entry keys to delete",
+ "$ref": "KeyRange"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables events from backend."
+ },
+ {
+ "name": "enable",
+ "description": "Enables events from backend."
+ },
+ {
+ "name": "requestData",
+ "description": "Requests data from object store or index.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ },
+ {
+ "name": "indexName",
+ "description": "Index name, empty string for object store data requests.",
+ "type": "string"
+ },
+ {
+ "name": "skipCount",
+ "description": "Number of records to skip.",
+ "type": "integer"
+ },
+ {
+ "name": "pageSize",
+ "description": "Number of records to fetch.",
+ "type": "integer"
+ },
+ {
+ "name": "keyRange",
+ "description": "Key range.",
+ "optional": true,
+ "$ref": "KeyRange"
+ }
+ ],
+ "returns": [
+ {
+ "name": "objectStoreDataEntries",
+ "description": "Array of object store data entries.",
+ "type": "array",
+ "items": {
+ "$ref": "DataEntry"
+ }
+ },
+ {
+ "name": "hasMore",
+ "description": "If true, there are more entries to fetch in the given range.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getMetadata",
+ "description": "Gets metadata of an object store.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "Object store name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entriesCount",
+ "description": "the entries count",
+ "type": "number"
+ },
+ {
+ "name": "keyGeneratorValue",
+ "description": "the current value of key generator, to become the next inserted\nkey into the object store. Valid if objectStore.autoIncrement\nis true.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "requestDatabase",
+ "description": "Requests database with given name in given frame.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database name.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "databaseWithObjectStores",
+ "description": "Database with an array of object stores.",
+ "$ref": "DatabaseWithObjectStores"
+ }
+ ]
+ },
+ {
+ "name": "requestDatabaseNames",
+ "description": "Requests database names for given security origin.",
+ "parameters": [
+ {
+ "name": "securityOrigin",
+ "description": "At least and at most one of securityOrigin, storageKey, or storageBucket must be specified.\nSecurity origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "storageBucket",
+ "description": "Storage bucket. If not specified, it uses the default bucket.",
+ "optional": true,
+ "$ref": "Storage.StorageBucket"
+ }
+ ],
+ "returns": [
+ {
+ "name": "databaseNames",
+ "description": "Database names for origin.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Input",
+ "types": [
+ {
+ "id": "TouchPoint",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "radiusX",
+ "description": "X radius of the touch area (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "radiusY",
+ "description": "Y radius of the touch area (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "rotationAngle",
+ "description": "Rotation angle (default: 0.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "force",
+ "description": "Force (default: 1.0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tangentialPressure",
+ "description": "The normalized tangential pressure, which has a range of [-1,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tiltX",
+ "description": "The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0)",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tiltY",
+ "description": "The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "twist",
+ "description": "The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "id",
+ "description": "Identifier used to track touch sources between events, must be unique within an event.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "GestureSourceType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "default",
+ "touch",
+ "mouse"
+ ]
+ },
+ {
+ "id": "MouseButton",
+ "type": "string",
+ "enum": [
+ "none",
+ "left",
+ "middle",
+ "right",
+ "back",
+ "forward"
+ ]
+ },
+ {
+ "id": "TimeSinceEpoch",
+ "description": "UTC time in seconds, counted from January 1, 1970.",
+ "type": "number"
+ },
+ {
+ "id": "DragDataItem",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "mimeType",
+ "description": "Mime type of the dragged data.",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Depending of the value of `mimeType`, it contains the dragged link,\ntext, HTML markup or any other data.",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "description": "Title associated with a link. Only valid when `mimeType` == \"text/uri-list\".",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "baseURL",
+ "description": "Stores the base URL for the contained markup. Only valid when `mimeType`\n== \"text/html\".",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "DragData",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "items",
+ "type": "array",
+ "items": {
+ "$ref": "DragDataItem"
+ }
+ },
+ {
+ "name": "files",
+ "description": "List of filenames that should be included when dropping",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "dragOperationsMask",
+ "description": "Bit field representing allowed drag operations. Copy = 1, Link = 2, Move = 16",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "dispatchDragEvent",
+ "description": "Dispatches a drag event into the page.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the drag event.",
+ "type": "string",
+ "enum": [
+ "dragEnter",
+ "dragOver",
+ "drop",
+ "dragCancel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "data",
+ "$ref": "DragData"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dispatchKeyEvent",
+ "description": "Dispatches a key event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the key event.",
+ "type": "string",
+ "enum": [
+ "keyDown",
+ "keyUp",
+ "rawKeyDown",
+ "char"
+ ]
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "text",
+ "description": "Text as generated by processing a virtual key code with a keyboard layout. Not needed for\nfor `keyUp` and `rawKeyDown` events (default: \"\")",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "unmodifiedText",
+ "description": "Text that would have been generated by the keyboard if no modifiers were pressed (except for\nshift). Useful for shortcut (accelerator) key handling (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "keyIdentifier",
+ "description": "Unique key identifier (e.g., 'U+0041') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Unique DOM defined string value for each physical key (e.g., 'KeyA') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "description": "Unique DOM defined string value describing the meaning of the key in the context of active\nmodifiers, keyboard layout, etc (e.g., 'AltGr') (default: \"\").",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "windowsVirtualKeyCode",
+ "description": "Windows virtual key code (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "nativeVirtualKeyCode",
+ "description": "Native virtual key code (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "autoRepeat",
+ "description": "Whether the event was generated from auto repeat (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isKeypad",
+ "description": "Whether the event was generated from the keypad (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isSystemKey",
+ "description": "Whether the event was a system key event (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "location",
+ "description": "Whether the event was from the left or right side of the keyboard. 1=Left, 2=Right (default:\n0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "commands",
+ "description": "Editing commands to send with the key event (e.g., 'selectAll') (default: []).\nThese are related to but not equal the command names used in `document.execCommand` and NSStandardKeyBindingResponding.\nSee https://source.chromium.org/chromium/chromium/src/+/main:third_party/blink/renderer/core/editing/commands/editor_command_names.h for valid command names.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "insertText",
+ "description": "This method emulates inserting text that doesn't come from a key press,\nfor example an emoji keyboard or an IME.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "text",
+ "description": "The text to insert.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "imeSetComposition",
+ "description": "This method sets the current candidate text for ime.\nUse imeCommitComposition to commit the final text.\nUse imeSetComposition with empty string as text to cancel composition.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "text",
+ "description": "The text to insert",
+ "type": "string"
+ },
+ {
+ "name": "selectionStart",
+ "description": "selection start",
+ "type": "integer"
+ },
+ {
+ "name": "selectionEnd",
+ "description": "selection end",
+ "type": "integer"
+ },
+ {
+ "name": "replacementStart",
+ "description": "replacement start",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "replacementEnd",
+ "description": "replacement end",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dispatchMouseEvent",
+ "description": "Dispatches a mouse event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the mouse event.",
+ "type": "string",
+ "enum": [
+ "mousePressed",
+ "mouseReleased",
+ "mouseMoved",
+ "mouseWheel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the event relative to the main frame's viewport in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the event relative to the main frame's viewport in CSS pixels. 0 refers to\nthe top of the viewport and Y increases as it proceeds towards the bottom of the viewport.",
+ "type": "number"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "button",
+ "description": "Mouse button (default: \"none\").",
+ "optional": true,
+ "$ref": "MouseButton"
+ },
+ {
+ "name": "buttons",
+ "description": "A number indicating which buttons are pressed on the mouse when a mouse event is triggered.\nLeft=1, Right=2, Middle=4, Back=8, Forward=16, None=0.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clickCount",
+ "description": "Number of times the mouse button was clicked (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "force",
+ "description": "The normalized pressure, which has a range of [0,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tangentialPressure",
+ "description": "The normalized tangential pressure, which has a range of [-1,1] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "tiltX",
+ "description": "The plane angle between the Y-Z plane and the plane containing both the stylus axis and the Y axis, in degrees of the range [-90,90], a positive tiltX is to the right (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tiltY",
+ "description": "The plane angle between the X-Z plane and the plane containing both the stylus axis and the X axis, in degrees of the range [-90,90], a positive tiltY is towards the user (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "twist",
+ "description": "The clockwise rotation of a pen stylus around its own major axis, in degrees in the range [0,359] (default: 0).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "deltaX",
+ "description": "X delta in CSS pixels for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "deltaY",
+ "description": "Y delta in CSS pixels for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "pointerType",
+ "description": "Pointer type (default: \"mouse\").",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mouse",
+ "pen"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "dispatchTouchEvent",
+ "description": "Dispatches a touch event to the page.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the touch event. TouchEnd and TouchCancel must not contain any touch points, while\nTouchStart and TouchMove must contains at least one.",
+ "type": "string",
+ "enum": [
+ "touchStart",
+ "touchEnd",
+ "touchMove",
+ "touchCancel"
+ ]
+ },
+ {
+ "name": "touchPoints",
+ "description": "Active touch points on the touch device. One event per any changed point (compared to\nprevious touch event in a sequence) is generated, emulating pressing/moving/releasing points\none by one.",
+ "type": "array",
+ "items": {
+ "$ref": "TouchPoint"
+ }
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ }
+ ]
+ },
+ {
+ "name": "cancelDragging",
+ "description": "Cancels any active dragging in the page."
+ },
+ {
+ "name": "emulateTouchFromMouseEvent",
+ "description": "Emulates touch event from the mouse event parameters.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the mouse event.",
+ "type": "string",
+ "enum": [
+ "mousePressed",
+ "mouseReleased",
+ "mouseMoved",
+ "mouseWheel"
+ ]
+ },
+ {
+ "name": "x",
+ "description": "X coordinate of the mouse pointer in DIP.",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the mouse pointer in DIP.",
+ "type": "integer"
+ },
+ {
+ "name": "button",
+ "description": "Mouse button. Only \"none\", \"left\", \"right\" are supported.",
+ "$ref": "MouseButton"
+ },
+ {
+ "name": "timestamp",
+ "description": "Time at which the event occurred (default: current time).",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "deltaX",
+ "description": "X delta in DIP for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "deltaY",
+ "description": "Y delta in DIP for mouse wheel event (default: 0).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "modifiers",
+ "description": "Bit field representing pressed modifier keys. Alt=1, Ctrl=2, Meta/Command=4, Shift=8\n(default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clickCount",
+ "description": "Number of times the mouse button was clicked (default: 0).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setIgnoreInputEvents",
+ "description": "Ignores input events (useful while auditing page).",
+ "parameters": [
+ {
+ "name": "ignore",
+ "description": "Ignores input events processing when set to true.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setInterceptDrags",
+ "description": "Prevents default drag and drop behavior and instead emits `Input.dragIntercepted` events.\nDrag and drop behavior can be directly controlled via `Input.dispatchDragEvent`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "synthesizePinchGesture",
+ "description": "Synthesizes a pinch gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "scaleFactor",
+ "description": "Relative scale factor after zooming (>1.0 zooms in, <1.0 zooms out).",
+ "type": "number"
+ },
+ {
+ "name": "relativeSpeed",
+ "description": "Relative pointer speed in pixels per second (default: 800).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ }
+ ]
+ },
+ {
+ "name": "synthesizeScrollGesture",
+ "description": "Synthesizes a scroll gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "xDistance",
+ "description": "The distance to scroll along the X axis (positive to scroll left).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "yDistance",
+ "description": "The distance to scroll along the Y axis (positive to scroll up).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "xOverscroll",
+ "description": "The number of additional pixels to scroll back along the X axis, in addition to the given\ndistance.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "yOverscroll",
+ "description": "The number of additional pixels to scroll back along the Y axis, in addition to the given\ndistance.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "preventFling",
+ "description": "Prevent fling (default: true).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "speed",
+ "description": "Swipe speed in pixels per second (default: 800).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ },
+ {
+ "name": "repeatCount",
+ "description": "The number of times to repeat the gesture (default: 0).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "repeatDelayMs",
+ "description": "The number of milliseconds delay between each repeat. (default: 250).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "interactionMarkerName",
+ "description": "The name of the interaction markers to generate, if not empty (default: \"\").",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "synthesizeTapGesture",
+ "description": "Synthesizes a tap gesture over a time period by issuing appropriate touch events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate of the start of the gesture in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "duration",
+ "description": "Duration between touchdown and touchup events in ms (default: 50).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "tapCount",
+ "description": "Number of times to perform the tap (e.g. 2 for double tap, default: 1).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "gestureSourceType",
+ "description": "Which type of input events to be generated (default: 'default', which queries the platform\nfor the preferred input type).",
+ "optional": true,
+ "$ref": "GestureSourceType"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dragIntercepted",
+ "description": "Emitted only when `Input.setInterceptDrags` is enabled. Use this data with `Input.dispatchDragEvent` to\nrestore normal drag and drop behavior.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "data",
+ "$ref": "DragData"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Inspector",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables inspector domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables inspector domain notifications."
+ }
+ ],
+ "events": [
+ {
+ "name": "detached",
+ "description": "Fired when remote debugging connection is about to be terminated. Contains detach reason.",
+ "parameters": [
+ {
+ "name": "reason",
+ "description": "The reason why connection has been terminated.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "targetCrashed",
+ "description": "Fired when debugging target has crashed"
+ },
+ {
+ "name": "targetReloadedAfterCrash",
+ "description": "Fired when debugging target has reloaded after crash"
+ }
+ ]
+ },
+ {
+ "domain": "LayerTree",
+ "experimental": true,
+ "dependencies": [
+ "DOM"
+ ],
+ "types": [
+ {
+ "id": "LayerId",
+ "description": "Unique Layer identifier.",
+ "type": "string"
+ },
+ {
+ "id": "SnapshotId",
+ "description": "Unique snapshot identifier.",
+ "type": "string"
+ },
+ {
+ "id": "ScrollRect",
+ "description": "Rectangle where scrolling happens on the main thread.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rect",
+ "description": "Rectangle itself.",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "type",
+ "description": "Reason for rectangle to force scrolling on the main thread",
+ "type": "string",
+ "enum": [
+ "RepaintsOnScroll",
+ "TouchEventHandler",
+ "WheelEventHandler"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "StickyPositionConstraint",
+ "description": "Sticky position constraints.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "stickyBoxRect",
+ "description": "Layout rectangle of the sticky element before being shifted",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "containingBlockRect",
+ "description": "Layout rectangle of the containing block of the sticky element",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "nearestLayerShiftingStickyBox",
+ "description": "The nearest sticky layer that shifts the sticky box",
+ "optional": true,
+ "$ref": "LayerId"
+ },
+ {
+ "name": "nearestLayerShiftingContainingBlock",
+ "description": "The nearest sticky layer that shifts the containing block",
+ "optional": true,
+ "$ref": "LayerId"
+ }
+ ]
+ },
+ {
+ "id": "PictureTile",
+ "description": "Serialized fragment of layer picture along with its offset within the layer.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "Offset from owning layer left boundary",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Offset from owning layer top boundary",
+ "type": "number"
+ },
+ {
+ "name": "picture",
+ "description": "Base64-encoded snapshot data. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Layer",
+ "description": "Information about a compositing layer.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "layerId",
+ "description": "The unique id for this layer.",
+ "$ref": "LayerId"
+ },
+ {
+ "name": "parentLayerId",
+ "description": "The id of parent (not present for root).",
+ "optional": true,
+ "$ref": "LayerId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "The backend id for the node associated with this layer.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "offsetX",
+ "description": "Offset from parent layer, X coordinate.",
+ "type": "number"
+ },
+ {
+ "name": "offsetY",
+ "description": "Offset from parent layer, Y coordinate.",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Layer width.",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Layer height.",
+ "type": "number"
+ },
+ {
+ "name": "transform",
+ "description": "Transformation matrix for layer, default is identity matrix",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ },
+ {
+ "name": "anchorX",
+ "description": "Transform anchor point X, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "anchorY",
+ "description": "Transform anchor point Y, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "anchorZ",
+ "description": "Transform anchor point Z, absent if no transform specified",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paintCount",
+ "description": "Indicates how many time this layer has painted.",
+ "type": "integer"
+ },
+ {
+ "name": "drawsContent",
+ "description": "Indicates whether this layer hosts any content, rather than being used for\ntransform/scrolling purposes only.",
+ "type": "boolean"
+ },
+ {
+ "name": "invisible",
+ "description": "Set if layer is not visible.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scrollRects",
+ "description": "Rectangles scrolling on main thread only.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ScrollRect"
+ }
+ },
+ {
+ "name": "stickyPositionConstraint",
+ "description": "Sticky position constraint information",
+ "optional": true,
+ "$ref": "StickyPositionConstraint"
+ }
+ ]
+ },
+ {
+ "id": "PaintProfile",
+ "description": "Array of timings, one per paint step.",
+ "type": "array",
+ "items": {
+ "type": "number"
+ }
+ }
+ ],
+ "commands": [
+ {
+ "name": "compositingReasons",
+ "description": "Provides the reasons why the given layer was composited.",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the layer for which we want to get the reasons it was composited.",
+ "$ref": "LayerId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "compositingReasons",
+ "description": "A list of strings specifying reasons for the given layer to become composited.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "compositingReasonIds",
+ "description": "A list of strings specifying reason IDs for the given layer to become composited.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables compositing tree inspection."
+ },
+ {
+ "name": "enable",
+ "description": "Enables compositing tree inspection."
+ },
+ {
+ "name": "loadSnapshot",
+ "description": "Returns the snapshot identifier.",
+ "parameters": [
+ {
+ "name": "tiles",
+ "description": "An array of tiles composing the snapshot.",
+ "type": "array",
+ "items": {
+ "$ref": "PictureTile"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "makeSnapshot",
+ "description": "Returns the layer snapshot identifier.",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the layer.",
+ "$ref": "LayerId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "profileSnapshot",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ },
+ {
+ "name": "minRepeatCount",
+ "description": "The maximum number of times to replay the snapshot (1, if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "minDuration",
+ "description": "The minimum duration (in seconds) to replay the snapshot.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "clipRect",
+ "description": "The clip rectangle to apply when replaying the snapshot.",
+ "optional": true,
+ "$ref": "DOM.Rect"
+ }
+ ],
+ "returns": [
+ {
+ "name": "timings",
+ "description": "The array of paint profiles, one per run.",
+ "type": "array",
+ "items": {
+ "$ref": "PaintProfile"
+ }
+ }
+ ]
+ },
+ {
+ "name": "releaseSnapshot",
+ "description": "Releases layer snapshot captured by the back-end.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ]
+ },
+ {
+ "name": "replaySnapshot",
+ "description": "Replays the layer snapshot and returns the resulting bitmap.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ },
+ {
+ "name": "fromStep",
+ "description": "The first step to replay from (replay from the very start if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "toStep",
+ "description": "The last step to replay to (replay till the end if not specified).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scale",
+ "description": "The scale to apply while replaying (defaults to 1).",
+ "optional": true,
+ "type": "number"
+ }
+ ],
+ "returns": [
+ {
+ "name": "dataURL",
+ "description": "A data: URL for resulting image.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "snapshotCommandLog",
+ "description": "Replays the layer snapshot and returns canvas log.",
+ "parameters": [
+ {
+ "name": "snapshotId",
+ "description": "The id of the layer snapshot.",
+ "$ref": "SnapshotId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "commandLog",
+ "description": "The array of canvas function calls.",
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "layerPainted",
+ "parameters": [
+ {
+ "name": "layerId",
+ "description": "The id of the painted layer.",
+ "$ref": "LayerId"
+ },
+ {
+ "name": "clip",
+ "description": "Clip rectangle.",
+ "$ref": "DOM.Rect"
+ }
+ ]
+ },
+ {
+ "name": "layerTreeDidChange",
+ "parameters": [
+ {
+ "name": "layers",
+ "description": "Layer tree, absent if not in the comspositing mode.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Layer"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Log",
+ "description": "Provides access to log entries.",
+ "dependencies": [
+ "Runtime",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "LogEntry",
+ "description": "Log entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Log entry source.",
+ "type": "string",
+ "enum": [
+ "xml",
+ "javascript",
+ "network",
+ "storage",
+ "appcache",
+ "rendering",
+ "security",
+ "deprecation",
+ "worker",
+ "violation",
+ "intervention",
+ "recommendation",
+ "other"
+ ]
+ },
+ {
+ "name": "level",
+ "description": "Log entry severity.",
+ "type": "string",
+ "enum": [
+ "verbose",
+ "info",
+ "warning",
+ "error"
+ ]
+ },
+ {
+ "name": "text",
+ "description": "Logged text.",
+ "type": "string"
+ },
+ {
+ "name": "category",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "cors"
+ ]
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp when this entry was added.",
+ "$ref": "Runtime.Timestamp"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource if known.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the resource.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript stack trace.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "networkRequestId",
+ "description": "Identifier of the network request associated with this entry.",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "workerId",
+ "description": "Identifier of the worker associated with this entry.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "args",
+ "description": "Call arguments.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Runtime.RemoteObject"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ViolationSetting",
+ "description": "Violation configuration setting.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Violation type.",
+ "type": "string",
+ "enum": [
+ "longTask",
+ "longLayout",
+ "blockedEvent",
+ "blockedParser",
+ "discouragedAPIUse",
+ "handler",
+ "recurringHandler"
+ ]
+ },
+ {
+ "name": "threshold",
+ "description": "Time threshold to trigger upon.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clear",
+ "description": "Clears the log."
+ },
+ {
+ "name": "disable",
+ "description": "Disables log domain, prevents further log entries from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables log domain, sends the entries collected so far to the client by means of the\n`entryAdded` notification."
+ },
+ {
+ "name": "startViolationsReport",
+ "description": "start violation reporting.",
+ "parameters": [
+ {
+ "name": "config",
+ "description": "Configuration for violations.",
+ "type": "array",
+ "items": {
+ "$ref": "ViolationSetting"
+ }
+ }
+ ]
+ },
+ {
+ "name": "stopViolationsReport",
+ "description": "Stop violation reporting."
+ }
+ ],
+ "events": [
+ {
+ "name": "entryAdded",
+ "description": "Issued when new message was logged.",
+ "parameters": [
+ {
+ "name": "entry",
+ "description": "The entry.",
+ "$ref": "LogEntry"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Memory",
+ "experimental": true,
+ "types": [
+ {
+ "id": "PressureLevel",
+ "description": "Memory pressure level.",
+ "type": "string",
+ "enum": [
+ "moderate",
+ "critical"
+ ]
+ },
+ {
+ "id": "SamplingProfileNode",
+ "description": "Heap profile sample.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "size",
+ "description": "Size of the sampled allocation.",
+ "type": "number"
+ },
+ {
+ "name": "total",
+ "description": "Total bytes attributed to this sample.",
+ "type": "number"
+ },
+ {
+ "name": "stack",
+ "description": "Execution stack at the point of allocation.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SamplingProfile",
+ "description": "Array of heap profile samples.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "samples",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingProfileNode"
+ }
+ },
+ {
+ "name": "modules",
+ "type": "array",
+ "items": {
+ "$ref": "Module"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Module",
+ "description": "Executable module information",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Name of the module.",
+ "type": "string"
+ },
+ {
+ "name": "uuid",
+ "description": "UUID of the module.",
+ "type": "string"
+ },
+ {
+ "name": "baseAddress",
+ "description": "Base address where the module is loaded into memory. Encoded as a decimal\nor hexadecimal (0x prefixed) string.",
+ "type": "string"
+ },
+ {
+ "name": "size",
+ "description": "Size of the module in bytes.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getDOMCounters",
+ "returns": [
+ {
+ "name": "documents",
+ "type": "integer"
+ },
+ {
+ "name": "nodes",
+ "type": "integer"
+ },
+ {
+ "name": "jsEventListeners",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "prepareForLeakDetection"
+ },
+ {
+ "name": "forciblyPurgeJavaScriptMemory",
+ "description": "Simulate OomIntervention by purging V8 memory."
+ },
+ {
+ "name": "setPressureNotificationsSuppressed",
+ "description": "Enable/disable suppressing memory pressure notifications in all processes.",
+ "parameters": [
+ {
+ "name": "suppressed",
+ "description": "If true, memory pressure notifications will be suppressed.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "simulatePressureNotification",
+ "description": "Simulate a memory pressure notification in all processes.",
+ "parameters": [
+ {
+ "name": "level",
+ "description": "Memory pressure level of the notification.",
+ "$ref": "PressureLevel"
+ }
+ ]
+ },
+ {
+ "name": "startSampling",
+ "description": "Start collecting native memory profile.",
+ "parameters": [
+ {
+ "name": "samplingInterval",
+ "description": "Average number of bytes between samples.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "suppressRandomness",
+ "description": "Do not randomize intervals between samples.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "stopSampling",
+ "description": "Stop collecting native memory profile."
+ },
+ {
+ "name": "getAllTimeSamplingProfile",
+ "description": "Retrieve native memory allocations profile\ncollected since renderer process startup.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserSamplingProfile",
+ "description": "Retrieve native memory allocations profile\ncollected since browser process startup.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ },
+ {
+ "name": "getSamplingProfile",
+ "description": "Retrieve native memory allocations profile collected since last\n`startSampling` call.",
+ "returns": [
+ {
+ "name": "profile",
+ "$ref": "SamplingProfile"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Network",
+ "description": "Network domain allows tracking network activities of the page. It exposes information about http,\nfile, data and other requests and responses, their headers, bodies, timing, etc.",
+ "dependencies": [
+ "Debugger",
+ "Runtime",
+ "Security"
+ ],
+ "types": [
+ {
+ "id": "ResourceType",
+ "description": "Resource type as it was perceived by the rendering engine.",
+ "type": "string",
+ "enum": [
+ "Document",
+ "Stylesheet",
+ "Image",
+ "Media",
+ "Font",
+ "Script",
+ "TextTrack",
+ "XHR",
+ "Fetch",
+ "Prefetch",
+ "EventSource",
+ "WebSocket",
+ "Manifest",
+ "SignedExchange",
+ "Ping",
+ "CSPViolationReport",
+ "Preflight",
+ "Other"
+ ]
+ },
+ {
+ "id": "LoaderId",
+ "description": "Unique loader identifier.",
+ "type": "string"
+ },
+ {
+ "id": "RequestId",
+ "description": "Unique request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "InterceptionId",
+ "description": "Unique intercepted request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "ErrorReason",
+ "description": "Network level fetch failure reason.",
+ "type": "string",
+ "enum": [
+ "Failed",
+ "Aborted",
+ "TimedOut",
+ "AccessDenied",
+ "ConnectionClosed",
+ "ConnectionReset",
+ "ConnectionRefused",
+ "ConnectionAborted",
+ "ConnectionFailed",
+ "NameNotResolved",
+ "InternetDisconnected",
+ "AddressUnreachable",
+ "BlockedByClient",
+ "BlockedByResponse"
+ ]
+ },
+ {
+ "id": "TimeSinceEpoch",
+ "description": "UTC time in seconds, counted from January 1, 1970.",
+ "type": "number"
+ },
+ {
+ "id": "MonotonicTime",
+ "description": "Monotonically increasing time in seconds since an arbitrary point in the past.",
+ "type": "number"
+ },
+ {
+ "id": "Headers",
+ "description": "Request / response headers as keys / values of JSON object.",
+ "type": "object"
+ },
+ {
+ "id": "ConnectionType",
+ "description": "The underlying connection technology that the browser is supposedly using.",
+ "type": "string",
+ "enum": [
+ "none",
+ "cellular2g",
+ "cellular3g",
+ "cellular4g",
+ "bluetooth",
+ "ethernet",
+ "wifi",
+ "wimax",
+ "other"
+ ]
+ },
+ {
+ "id": "CookieSameSite",
+ "description": "Represents the cookie's 'SameSite' status:\nhttps://tools.ietf.org/html/draft-west-first-party-cookies",
+ "type": "string",
+ "enum": [
+ "Strict",
+ "Lax",
+ "None"
+ ]
+ },
+ {
+ "id": "CookiePriority",
+ "description": "Represents the cookie's 'Priority' status:\nhttps://tools.ietf.org/html/draft-west-cookie-priority-00",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Low",
+ "Medium",
+ "High"
+ ]
+ },
+ {
+ "id": "CookieSourceScheme",
+ "description": "Represents the source scheme of the origin that originally set the cookie.\nA value of \"Unset\" allows protocol clients to emulate legacy cookie scope for the scheme.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Unset",
+ "NonSecure",
+ "Secure"
+ ]
+ },
+ {
+ "id": "ResourceTiming",
+ "description": "Timing information for the request.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestTime",
+ "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\nmilliseconds relatively to this requestTime.",
+ "type": "number"
+ },
+ {
+ "name": "proxyStart",
+ "description": "Started resolving proxy.",
+ "type": "number"
+ },
+ {
+ "name": "proxyEnd",
+ "description": "Finished resolving proxy.",
+ "type": "number"
+ },
+ {
+ "name": "dnsStart",
+ "description": "Started DNS address resolve.",
+ "type": "number"
+ },
+ {
+ "name": "dnsEnd",
+ "description": "Finished DNS address resolve.",
+ "type": "number"
+ },
+ {
+ "name": "connectStart",
+ "description": "Started connecting to the remote host.",
+ "type": "number"
+ },
+ {
+ "name": "connectEnd",
+ "description": "Connected to the remote host.",
+ "type": "number"
+ },
+ {
+ "name": "sslStart",
+ "description": "Started SSL handshake.",
+ "type": "number"
+ },
+ {
+ "name": "sslEnd",
+ "description": "Finished SSL handshake.",
+ "type": "number"
+ },
+ {
+ "name": "workerStart",
+ "description": "Started running ServiceWorker.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerReady",
+ "description": "Finished Starting ServiceWorker.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerFetchStart",
+ "description": "Started fetch event.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "workerRespondWithSettled",
+ "description": "Settled fetch event respondWith promise.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "sendStart",
+ "description": "Started sending request.",
+ "type": "number"
+ },
+ {
+ "name": "sendEnd",
+ "description": "Finished sending request.",
+ "type": "number"
+ },
+ {
+ "name": "pushStart",
+ "description": "Time the server started pushing request.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "pushEnd",
+ "description": "Time the server finished pushing request.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "receiveHeadersStart",
+ "description": "Started receiving response headers.",
+ "experimental": true,
+ "type": "number"
+ },
+ {
+ "name": "receiveHeadersEnd",
+ "description": "Finished receiving response headers.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "ResourcePriority",
+ "description": "Loading priority of a resource request.",
+ "type": "string",
+ "enum": [
+ "VeryLow",
+ "Low",
+ "Medium",
+ "High",
+ "VeryHigh"
+ ]
+ },
+ {
+ "id": "PostDataEntry",
+ "description": "Post data entry for HTTP request",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bytes",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Request",
+ "description": "HTTP request data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Request URL (without fragment).",
+ "type": "string"
+ },
+ {
+ "name": "urlFragment",
+ "description": "Fragment of the requested URL starting with hash, if present.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "HTTP request method.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP request headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "postData",
+ "description": "HTTP POST request data.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasPostData",
+ "description": "True when the request has POST data. Note that postData might still be omitted when this flag is true when the data is too long.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "postDataEntries",
+ "description": "Request body elements. This will be converted from base64 to binary",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PostDataEntry"
+ }
+ },
+ {
+ "name": "mixedContentType",
+ "description": "The mixed content type of the request.",
+ "optional": true,
+ "$ref": "Security.MixedContentType"
+ },
+ {
+ "name": "initialPriority",
+ "description": "Priority of the resource request at the time request is sent.",
+ "$ref": "ResourcePriority"
+ },
+ {
+ "name": "referrerPolicy",
+ "description": "The referrer policy of the request, as defined in https://www.w3.org/TR/referrer-policy/",
+ "type": "string",
+ "enum": [
+ "unsafe-url",
+ "no-referrer-when-downgrade",
+ "no-referrer",
+ "origin",
+ "origin-when-cross-origin",
+ "same-origin",
+ "strict-origin",
+ "strict-origin-when-cross-origin"
+ ]
+ },
+ {
+ "name": "isLinkPreload",
+ "description": "Whether is loaded via link preload.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "trustTokenParams",
+ "description": "Set for requests when the TrustToken API is used. Contains the parameters\npassed by the developer (e.g. via \"fetch\") as understood by the backend.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TrustTokenParams"
+ },
+ {
+ "name": "isSameSite",
+ "description": "True if this resource request is considered to be the 'same site' as the\nrequest correspondinfg to the main frame.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SignedCertificateTimestamp",
+ "description": "Details of a signed certificate timestamp (SCT).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "status",
+ "description": "Validation status.",
+ "type": "string"
+ },
+ {
+ "name": "origin",
+ "description": "Origin.",
+ "type": "string"
+ },
+ {
+ "name": "logDescription",
+ "description": "Log name / description.",
+ "type": "string"
+ },
+ {
+ "name": "logId",
+ "description": "Log ID.",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "Issuance date. Unlike TimeSinceEpoch, this contains the number of\nmilliseconds since January 1, 1970, UTC, not the number of seconds.",
+ "type": "number"
+ },
+ {
+ "name": "hashAlgorithm",
+ "description": "Hash algorithm.",
+ "type": "string"
+ },
+ {
+ "name": "signatureAlgorithm",
+ "description": "Signature algorithm.",
+ "type": "string"
+ },
+ {
+ "name": "signatureData",
+ "description": "Signature data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SecurityDetails",
+ "description": "Security details about a request.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "description": "Protocol name (e.g. \"TLS 1.2\" or \"QUIC\").",
+ "type": "string"
+ },
+ {
+ "name": "keyExchange",
+ "description": "Key Exchange used by the connection, or the empty string if not applicable.",
+ "type": "string"
+ },
+ {
+ "name": "keyExchangeGroup",
+ "description": "(EC)DH group used by the connection, if applicable.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cipher",
+ "description": "Cipher name.",
+ "type": "string"
+ },
+ {
+ "name": "mac",
+ "description": "TLS MAC. Note that AEAD ciphers do not have separate MACs.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificateId",
+ "description": "Certificate ID value.",
+ "$ref": "Security.CertificateId"
+ },
+ {
+ "name": "subjectName",
+ "description": "Certificate subject name.",
+ "type": "string"
+ },
+ {
+ "name": "sanList",
+ "description": "Subject Alternative Name (SAN) DNS names and IP addresses.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "issuer",
+ "description": "Name of the issuing CA.",
+ "type": "string"
+ },
+ {
+ "name": "validFrom",
+ "description": "Certificate valid from date.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "validTo",
+ "description": "Certificate valid to (expiration) date",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "signedCertificateTimestampList",
+ "description": "List of signed certificate timestamps (SCTs).",
+ "type": "array",
+ "items": {
+ "$ref": "SignedCertificateTimestamp"
+ }
+ },
+ {
+ "name": "certificateTransparencyCompliance",
+ "description": "Whether the request complied with Certificate Transparency policy",
+ "$ref": "CertificateTransparencyCompliance"
+ },
+ {
+ "name": "serverSignatureAlgorithm",
+ "description": "The signature algorithm used by the server in the TLS server signature,\nrepresented as a TLS SignatureScheme code point. Omitted if not\napplicable or not known.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "encryptedClientHello",
+ "description": "Whether the connection used Encrypted ClientHello",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "CertificateTransparencyCompliance",
+ "description": "Whether the request complied with Certificate Transparency policy.",
+ "type": "string",
+ "enum": [
+ "unknown",
+ "not-compliant",
+ "compliant"
+ ]
+ },
+ {
+ "id": "BlockedReason",
+ "description": "The reason why request was blocked.",
+ "type": "string",
+ "enum": [
+ "other",
+ "csp",
+ "mixed-content",
+ "origin",
+ "inspector",
+ "subresource-filter",
+ "content-type",
+ "coep-frame-resource-needs-coep-header",
+ "coop-sandboxed-iframe-cannot-navigate-to-coop-page",
+ "corp-not-same-origin",
+ "corp-not-same-origin-after-defaulted-to-same-origin-by-coep",
+ "corp-not-same-site"
+ ]
+ },
+ {
+ "id": "CorsError",
+ "description": "The reason why request was blocked.",
+ "type": "string",
+ "enum": [
+ "DisallowedByMode",
+ "InvalidResponse",
+ "WildcardOriginNotAllowed",
+ "MissingAllowOriginHeader",
+ "MultipleAllowOriginValues",
+ "InvalidAllowOriginValue",
+ "AllowOriginMismatch",
+ "InvalidAllowCredentials",
+ "CorsDisabledScheme",
+ "PreflightInvalidStatus",
+ "PreflightDisallowedRedirect",
+ "PreflightWildcardOriginNotAllowed",
+ "PreflightMissingAllowOriginHeader",
+ "PreflightMultipleAllowOriginValues",
+ "PreflightInvalidAllowOriginValue",
+ "PreflightAllowOriginMismatch",
+ "PreflightInvalidAllowCredentials",
+ "PreflightMissingAllowExternal",
+ "PreflightInvalidAllowExternal",
+ "PreflightMissingAllowPrivateNetwork",
+ "PreflightInvalidAllowPrivateNetwork",
+ "InvalidAllowMethodsPreflightResponse",
+ "InvalidAllowHeadersPreflightResponse",
+ "MethodDisallowedByPreflightResponse",
+ "HeaderDisallowedByPreflightResponse",
+ "RedirectContainsCredentials",
+ "InsecurePrivateNetwork",
+ "InvalidPrivateNetworkAccess",
+ "UnexpectedPrivateNetworkAccess",
+ "NoCorsRedirectModeNotFollow",
+ "PreflightMissingPrivateNetworkAccessId",
+ "PreflightMissingPrivateNetworkAccessName",
+ "PrivateNetworkAccessPermissionUnavailable",
+ "PrivateNetworkAccessPermissionDenied"
+ ]
+ },
+ {
+ "id": "CorsErrorStatus",
+ "type": "object",
+ "properties": [
+ {
+ "name": "corsError",
+ "$ref": "CorsError"
+ },
+ {
+ "name": "failedParameter",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerResponseSource",
+ "description": "Source of serviceworker response.",
+ "type": "string",
+ "enum": [
+ "cache-storage",
+ "http-cache",
+ "fallback-code",
+ "network"
+ ]
+ },
+ {
+ "id": "TrustTokenParams",
+ "description": "Determines what type of Trust Token operation is executed and\ndepending on the type, some additional parameters. The values\nare specified in third_party/blink/renderer/core/fetch/trust_token.idl.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "operation",
+ "$ref": "TrustTokenOperationType"
+ },
+ {
+ "name": "refreshPolicy",
+ "description": "Only set for \"token-redemption\" operation and determine whether\nto request a fresh SRR or use a still valid cached SRR.",
+ "type": "string",
+ "enum": [
+ "UseCached",
+ "Refresh"
+ ]
+ },
+ {
+ "name": "issuers",
+ "description": "Origins of issuers from whom to request tokens or redemption\nrecords.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TrustTokenOperationType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Issuance",
+ "Redemption",
+ "Signing"
+ ]
+ },
+ {
+ "id": "AlternateProtocolUsage",
+ "description": "The reason why Chrome uses a specific transport protocol for HTTP semantics.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "alternativeJobWonWithoutRace",
+ "alternativeJobWonRace",
+ "mainJobWonRace",
+ "mappingMissing",
+ "broken",
+ "dnsAlpnH3JobWonWithoutRace",
+ "dnsAlpnH3JobWonRace",
+ "unspecifiedReason"
+ ]
+ },
+ {
+ "id": "Response",
+ "description": "HTTP response data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Response URL. This URL can be different from CachedResource.url in case of redirect.",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "statusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "headersText",
+ "description": "HTTP response headers text. This has been replaced by the headers in Network.responseReceivedExtraInfo.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "mimeType",
+ "description": "Resource mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "Refined HTTP request headers that were actually transmitted over the network.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestHeadersText",
+ "description": "HTTP request headers text. This has been replaced by the headers in Network.requestWillBeSentExtraInfo.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "connectionReused",
+ "description": "Specifies whether physical connection was actually reused for this request.",
+ "type": "boolean"
+ },
+ {
+ "name": "connectionId",
+ "description": "Physical connection id that was actually used for this request.",
+ "type": "number"
+ },
+ {
+ "name": "remoteIPAddress",
+ "description": "Remote IP address.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "remotePort",
+ "description": "Remote port.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "fromDiskCache",
+ "description": "Specifies that the request was served from the disk cache.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "fromServiceWorker",
+ "description": "Specifies that the request was served from the ServiceWorker.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "fromPrefetchCache",
+ "description": "Specifies that the request was served from the prefetch cache.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Total number of bytes received for this request so far.",
+ "type": "number"
+ },
+ {
+ "name": "timing",
+ "description": "Timing information for the given request.",
+ "optional": true,
+ "$ref": "ResourceTiming"
+ },
+ {
+ "name": "serviceWorkerResponseSource",
+ "description": "Response source of response from ServiceWorker.",
+ "optional": true,
+ "$ref": "ServiceWorkerResponseSource"
+ },
+ {
+ "name": "responseTime",
+ "description": "The time at which the returned response was generated.",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "cacheStorageCacheName",
+ "description": "Cache Storage Cache Name.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "protocol",
+ "description": "Protocol used to fetch this request.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "alternateProtocolUsage",
+ "description": "The reason why Chrome uses a specific transport protocol for HTTP semantics.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AlternateProtocolUsage"
+ },
+ {
+ "name": "securityState",
+ "description": "Security state of the request resource.",
+ "$ref": "Security.SecurityState"
+ },
+ {
+ "name": "securityDetails",
+ "description": "Security details for the request.",
+ "optional": true,
+ "$ref": "SecurityDetails"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketRequest",
+ "description": "WebSocket request data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "headers",
+ "description": "HTTP request headers.",
+ "$ref": "Headers"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketResponse",
+ "description": "WebSocket response data.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "status",
+ "description": "HTTP response status code.",
+ "type": "integer"
+ },
+ {
+ "name": "statusText",
+ "description": "HTTP response status text.",
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "HTTP response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "headersText",
+ "description": "HTTP response headers text.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "requestHeaders",
+ "description": "HTTP request headers.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestHeadersText",
+ "description": "HTTP request headers text.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "WebSocketFrame",
+ "description": "WebSocket message data. This represents an entire WebSocket message, not just a fragmented frame as the name suggests.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "opcode",
+ "description": "WebSocket message opcode.",
+ "type": "number"
+ },
+ {
+ "name": "mask",
+ "description": "WebSocket message mask.",
+ "type": "boolean"
+ },
+ {
+ "name": "payloadData",
+ "description": "WebSocket message payload data.\nIf the opcode is 1, this is a text message and payloadData is a UTF-8 string.\nIf the opcode isn't 1, then payloadData is a base64 encoded string representing binary data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CachedResource",
+ "description": "Information about the cached resource.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Resource URL. This is the url of the original network request.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "response",
+ "description": "Cached response data.",
+ "optional": true,
+ "$ref": "Response"
+ },
+ {
+ "name": "bodySize",
+ "description": "Cached response body size.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Initiator",
+ "description": "Information about the request initiator.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of this initiator.",
+ "type": "string",
+ "enum": [
+ "parser",
+ "script",
+ "preload",
+ "SignedExchange",
+ "preflight",
+ "other"
+ ]
+ },
+ {
+ "name": "stack",
+ "description": "Initiator JavaScript stack trace, set for Script only.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "url",
+ "description": "Initiator URL, set for Parser type or for Script type (when script is importing module) or for SignedExchange type.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Initiator line number, set for Parser type or for Script type (when script is importing\nmodule) (0-based).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Initiator column number, set for Parser type or for Script type (when script is importing\nmodule) (0-based).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "requestId",
+ "description": "Set if another request triggered this request (e.g. preflight).",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "id": "Cookie",
+ "description": "Cookie object",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "type": "string"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date as the number of seconds since the UNIX epoch.",
+ "type": "number"
+ },
+ {
+ "name": "size",
+ "description": "Cookie size.",
+ "type": "integer"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "type": "boolean"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "type": "boolean"
+ },
+ {
+ "name": "session",
+ "description": "True in case of session cookie.",
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority",
+ "experimental": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "partitionKeyOpaque",
+ "description": "True if cookie partition key is opaque.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SetCookieBlockedReason",
+ "description": "Types of reasons why a cookie may not be stored from a response.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SecureOnly",
+ "SameSiteStrict",
+ "SameSiteLax",
+ "SameSiteUnspecifiedTreatedAsLax",
+ "SameSiteNoneInsecure",
+ "UserPreferences",
+ "ThirdPartyBlockedInFirstPartySet",
+ "SyntaxError",
+ "SchemeNotSupported",
+ "OverwriteSecure",
+ "InvalidDomain",
+ "InvalidPrefix",
+ "UnknownError",
+ "SchemefulSameSiteStrict",
+ "SchemefulSameSiteLax",
+ "SchemefulSameSiteUnspecifiedTreatedAsLax",
+ "SamePartyFromCrossPartyContext",
+ "SamePartyConflictsWithOtherAttributes",
+ "NameValuePairExceedsMaxSize"
+ ]
+ },
+ {
+ "id": "CookieBlockedReason",
+ "description": "Types of reasons why a cookie may not be sent with a request.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SecureOnly",
+ "NotOnPath",
+ "DomainMismatch",
+ "SameSiteStrict",
+ "SameSiteLax",
+ "SameSiteUnspecifiedTreatedAsLax",
+ "SameSiteNoneInsecure",
+ "UserPreferences",
+ "ThirdPartyBlockedInFirstPartySet",
+ "UnknownError",
+ "SchemefulSameSiteStrict",
+ "SchemefulSameSiteLax",
+ "SchemefulSameSiteUnspecifiedTreatedAsLax",
+ "SamePartyFromCrossPartyContext",
+ "NameValuePairExceedsMaxSize"
+ ]
+ },
+ {
+ "id": "BlockedSetCookieWithReason",
+ "description": "A cookie which was not stored from a response with the corresponding reason.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedReasons",
+ "description": "The reason(s) this cookie was blocked.",
+ "type": "array",
+ "items": {
+ "$ref": "SetCookieBlockedReason"
+ }
+ },
+ {
+ "name": "cookieLine",
+ "description": "The string representing this individual cookie as it would appear in the header.\nThis is not the entire \"cookie\" or \"set-cookie\" header which could have multiple cookies.",
+ "type": "string"
+ },
+ {
+ "name": "cookie",
+ "description": "The cookie object which represents the cookie which was not stored. It is optional because\nsometimes complete cookie information is not available, such as in the case of parsing\nerrors.",
+ "optional": true,
+ "$ref": "Cookie"
+ }
+ ]
+ },
+ {
+ "id": "BlockedCookieWithReason",
+ "description": "A cookie with was not sent with a request with the corresponding reason.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "blockedReasons",
+ "description": "The reason(s) the cookie was blocked.",
+ "type": "array",
+ "items": {
+ "$ref": "CookieBlockedReason"
+ }
+ },
+ {
+ "name": "cookie",
+ "description": "The cookie object representing the cookie which was not sent.",
+ "$ref": "Cookie"
+ }
+ ]
+ },
+ {
+ "id": "CookieParam",
+ "description": "Cookie parameter object",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain, path, source port, and source scheme values of the created cookie.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date, session cookie if not set",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.\nIf not set, the cookie will be set as not partitioned.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallenge",
+ "description": "Authorization challenge for HTTP status code 401 or 407.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Source of the authentication challenge.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "Server",
+ "Proxy"
+ ]
+ },
+ {
+ "name": "origin",
+ "description": "Origin of the challenger.",
+ "type": "string"
+ },
+ {
+ "name": "scheme",
+ "description": "The authentication scheme used, such as basic or digest",
+ "type": "string"
+ },
+ {
+ "name": "realm",
+ "description": "The realm of the challenge. May be empty.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallengeResponse",
+ "description": "Response to an AuthChallenge.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "response",
+ "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.",
+ "type": "string",
+ "enum": [
+ "Default",
+ "CancelAuth",
+ "ProvideCredentials"
+ ]
+ },
+ {
+ "name": "username",
+ "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "password",
+ "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InterceptionStage",
+ "description": "Stages of the interception to begin intercepting. Request will intercept before the request is\nsent. Response will intercept after the response is received.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Request",
+ "HeadersReceived"
+ ]
+ },
+ {
+ "id": "RequestPattern",
+ "description": "Request pattern for interception.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "urlPattern",
+ "description": "Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to `\"*\"`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceType",
+ "description": "If set, only requests for matching resource types will be intercepted.",
+ "optional": true,
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "interceptionStage",
+ "description": "Stage at which to begin intercepting requests. Default is Request.",
+ "optional": true,
+ "$ref": "InterceptionStage"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeSignature",
+ "description": "Information about a signed exchange signature.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#rfc.section.3.1",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "label",
+ "description": "Signed exchange signature label.",
+ "type": "string"
+ },
+ {
+ "name": "signature",
+ "description": "The hex string of signed exchange signature.",
+ "type": "string"
+ },
+ {
+ "name": "integrity",
+ "description": "Signed exchange signature integrity.",
+ "type": "string"
+ },
+ {
+ "name": "certUrl",
+ "description": "Signed exchange signature cert Url.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certSha256",
+ "description": "The hex string of signed exchange signature cert sha256.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "validityUrl",
+ "description": "Signed exchange signature validity Url.",
+ "type": "string"
+ },
+ {
+ "name": "date",
+ "description": "Signed exchange signature date.",
+ "type": "integer"
+ },
+ {
+ "name": "expires",
+ "description": "Signed exchange signature expires.",
+ "type": "integer"
+ },
+ {
+ "name": "certificates",
+ "description": "The encoded certificates.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeHeader",
+ "description": "Information about a signed exchange header.\nhttps://wicg.github.io/webpackage/draft-yasskin-httpbis-origin-signed-exchanges-impl.html#cbor-representation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestUrl",
+ "description": "Signed exchange request URL.",
+ "type": "string"
+ },
+ {
+ "name": "responseCode",
+ "description": "Signed exchange response code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Signed exchange response headers.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "signatures",
+ "description": "Signed exchange response signature.",
+ "type": "array",
+ "items": {
+ "$ref": "SignedExchangeSignature"
+ }
+ },
+ {
+ "name": "headerIntegrity",
+ "description": "Signed exchange header integrity hash in the form of `sha256-<base64-hash-value>`.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeErrorField",
+ "description": "Field type for a signed exchange related error.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "signatureSig",
+ "signatureIntegrity",
+ "signatureCertUrl",
+ "signatureCertSha256",
+ "signatureValidityUrl",
+ "signatureTimestamps"
+ ]
+ },
+ {
+ "id": "SignedExchangeError",
+ "description": "Information about a signed exchange response.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "signatureIndex",
+ "description": "The index of the signature which caused the error.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "errorField",
+ "description": "The field which caused the error.",
+ "optional": true,
+ "$ref": "SignedExchangeErrorField"
+ }
+ ]
+ },
+ {
+ "id": "SignedExchangeInfo",
+ "description": "Information about a signed exchange response.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "outerResponse",
+ "description": "The outer response of signed HTTP exchange which was received from network.",
+ "$ref": "Response"
+ },
+ {
+ "name": "header",
+ "description": "Information about the signed exchange header.",
+ "optional": true,
+ "$ref": "SignedExchangeHeader"
+ },
+ {
+ "name": "securityDetails",
+ "description": "Security details for the signed exchange header.",
+ "optional": true,
+ "$ref": "SecurityDetails"
+ },
+ {
+ "name": "errors",
+ "description": "Errors occurred while handling the signed exchagne.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "SignedExchangeError"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ContentEncoding",
+ "description": "List of content encodings supported by the backend.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "deflate",
+ "gzip",
+ "br",
+ "zstd"
+ ]
+ },
+ {
+ "id": "PrivateNetworkRequestPolicy",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Allow",
+ "BlockFromInsecureToMorePrivate",
+ "WarnFromInsecureToMorePrivate",
+ "PreflightBlock",
+ "PreflightWarn"
+ ]
+ },
+ {
+ "id": "IPAddressSpace",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Local",
+ "Private",
+ "Public",
+ "Unknown"
+ ]
+ },
+ {
+ "id": "ConnectTiming",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "requestTime",
+ "description": "Timing's requestTime is a baseline in seconds, while the other numbers are ticks in\nmilliseconds relatively to this requestTime. Matches ResourceTiming's requestTime for\nthe same request (but not for redirected requests).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "ClientSecurityState",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "initiatorIsSecureContext",
+ "type": "boolean"
+ },
+ {
+ "name": "initiatorIPAddressSpace",
+ "$ref": "IPAddressSpace"
+ },
+ {
+ "name": "privateNetworkRequestPolicy",
+ "$ref": "PrivateNetworkRequestPolicy"
+ }
+ ]
+ },
+ {
+ "id": "CrossOriginOpenerPolicyValue",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SameOrigin",
+ "SameOriginAllowPopups",
+ "RestrictProperties",
+ "UnsafeNone",
+ "SameOriginPlusCoep",
+ "RestrictPropertiesPlusCoep"
+ ]
+ },
+ {
+ "id": "CrossOriginOpenerPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "$ref": "CrossOriginOpenerPolicyValue"
+ },
+ {
+ "name": "reportOnlyValue",
+ "$ref": "CrossOriginOpenerPolicyValue"
+ },
+ {
+ "name": "reportingEndpoint",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "reportOnlyReportingEndpoint",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "CrossOriginEmbedderPolicyValue",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "None",
+ "Credentialless",
+ "RequireCorp"
+ ]
+ },
+ {
+ "id": "CrossOriginEmbedderPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "$ref": "CrossOriginEmbedderPolicyValue"
+ },
+ {
+ "name": "reportOnlyValue",
+ "$ref": "CrossOriginEmbedderPolicyValue"
+ },
+ {
+ "name": "reportingEndpoint",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "reportOnlyReportingEndpoint",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicySource",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "HTTP",
+ "Meta"
+ ]
+ },
+ {
+ "id": "ContentSecurityPolicyStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "effectiveDirectives",
+ "type": "string"
+ },
+ {
+ "name": "isEnforced",
+ "type": "boolean"
+ },
+ {
+ "name": "source",
+ "$ref": "ContentSecurityPolicySource"
+ }
+ ]
+ },
+ {
+ "id": "SecurityIsolationStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "coop",
+ "optional": true,
+ "$ref": "CrossOriginOpenerPolicyStatus"
+ },
+ {
+ "name": "coep",
+ "optional": true,
+ "$ref": "CrossOriginEmbedderPolicyStatus"
+ },
+ {
+ "name": "csp",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ContentSecurityPolicyStatus"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ReportStatus",
+ "description": "The status of a Reporting API report.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Queued",
+ "Pending",
+ "MarkedForRemoval",
+ "Success"
+ ]
+ },
+ {
+ "id": "ReportId",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "ReportingApiReport",
+ "description": "An object representing a report generated by the Reporting API.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "ReportId"
+ },
+ {
+ "name": "initiatorUrl",
+ "description": "The URL of the document that triggered the report.",
+ "type": "string"
+ },
+ {
+ "name": "destination",
+ "description": "The name of the endpoint group that should be used to deliver the report.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "The type of the report (specifies the set of data that is contained in the report body).",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "When the report was generated.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "depth",
+ "description": "How many uploads deep the related request was.",
+ "type": "integer"
+ },
+ {
+ "name": "completedAttempts",
+ "description": "The number of delivery attempts made so far, not including an active attempt.",
+ "type": "integer"
+ },
+ {
+ "name": "body",
+ "type": "object"
+ },
+ {
+ "name": "status",
+ "$ref": "ReportStatus"
+ }
+ ]
+ },
+ {
+ "id": "ReportingApiEndpoint",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL of the endpoint to which reports may be delivered.",
+ "type": "string"
+ },
+ {
+ "name": "groupName",
+ "description": "Name of the endpoint group.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "LoadNetworkResourcePageResult",
+ "description": "An object providing the result of a network resource load.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "success",
+ "type": "boolean"
+ },
+ {
+ "name": "netError",
+ "description": "Optional values used for error reporting.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "netErrorName",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "httpStatusCode",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "stream",
+ "description": "If successful, one of the following two fields holds the result.",
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ },
+ {
+ "name": "headers",
+ "description": "Response headers.",
+ "optional": true,
+ "$ref": "Network.Headers"
+ }
+ ]
+ },
+ {
+ "id": "LoadNetworkResourceOptions",
+ "description": "An options object that may be extended later to better support CORS,\nCORB and streaming.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "disableCache",
+ "type": "boolean"
+ },
+ {
+ "name": "includeCredentials",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "setAcceptedEncodings",
+ "description": "Sets a list of content encodings that will be accepted. Empty list means no encoding is accepted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "encodings",
+ "description": "List of accepted content encodings.",
+ "type": "array",
+ "items": {
+ "$ref": "ContentEncoding"
+ }
+ }
+ ]
+ },
+ {
+ "name": "clearAcceptedEncodingsOverride",
+ "description": "Clears accepted encodings set by setAcceptedEncodings",
+ "experimental": true
+ },
+ {
+ "name": "canClearBrowserCache",
+ "description": "Tells whether clearing browser cache is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if browser cache can be cleared.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "canClearBrowserCookies",
+ "description": "Tells whether clearing browser cookies is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if browser cookies can be cleared.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "canEmulateNetworkConditions",
+ "description": "Tells whether emulation of network conditions is supported.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "result",
+ "description": "True if emulation of network conditions is supported.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "clearBrowserCache",
+ "description": "Clears browser cache."
+ },
+ {
+ "name": "clearBrowserCookies",
+ "description": "Clears browser cookies."
+ },
+ {
+ "name": "continueInterceptedRequest",
+ "description": "Response to Network.requestIntercepted which either modifies the request to continue with any\nmodifications, or blocks it, or completes it with the provided response bytes. If a network\nfetch occurs as a result which encounters a redirect an additional Network.requestIntercepted\nevent will be sent with the same InterceptionId.\nDeprecated, use Fetch.continueRequest, Fetch.fulfillRequest and Fetch.failRequest instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "$ref": "InterceptionId"
+ },
+ {
+ "name": "errorReason",
+ "description": "If set this causes the request to fail with the given reason. Passing `Aborted` for requests\nmarked with `isNavigationRequest` also cancels the navigation. Must not be set in response\nto an authChallenge.",
+ "optional": true,
+ "$ref": "ErrorReason"
+ },
+ {
+ "name": "rawResponse",
+ "description": "If set the requests completes using with the provided base64 encoded raw response, including\nHTTP status line and headers etc... Must not be set in response to an authChallenge. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "If set the request url will be modified in a way that's not observable by page. Must not be\nset in response to an authChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "If set this allows the request method to be overridden. Must not be set in response to an\nauthChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "postData",
+ "description": "If set this allows postData to be set. Must not be set in response to an authChallenge.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "If set this allows the request headers to be changed. Must not be set in response to an\nauthChallenge.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "authChallengeResponse",
+ "description": "Response to a requestIntercepted with an authChallenge. Must not be set otherwise.",
+ "optional": true,
+ "$ref": "AuthChallengeResponse"
+ }
+ ]
+ },
+ {
+ "name": "deleteCookies",
+ "description": "Deletes browser cookies with matching name and url or domain/path pair.",
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Name of the cookies to remove.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "If specified, deletes all the cookies with the given name where domain and path match\nprovided URL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "If specified, deletes only cookies with the exact domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "If specified, deletes only cookies with the exact path.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables network tracking, prevents network events from being sent to the client."
+ },
+ {
+ "name": "emulateNetworkConditions",
+ "description": "Activates emulation of network conditions.",
+ "parameters": [
+ {
+ "name": "offline",
+ "description": "True to emulate internet disconnection.",
+ "type": "boolean"
+ },
+ {
+ "name": "latency",
+ "description": "Minimum latency from request sent to response headers received (ms).",
+ "type": "number"
+ },
+ {
+ "name": "downloadThroughput",
+ "description": "Maximal aggregated download throughput (bytes/sec). -1 disables download throttling.",
+ "type": "number"
+ },
+ {
+ "name": "uploadThroughput",
+ "description": "Maximal aggregated upload throughput (bytes/sec). -1 disables upload throttling.",
+ "type": "number"
+ },
+ {
+ "name": "connectionType",
+ "description": "Connection type if known.",
+ "optional": true,
+ "$ref": "ConnectionType"
+ }
+ ]
+ },
+ {
+ "name": "enable",
+ "description": "Enables network tracking, network events will now be delivered to the client.",
+ "parameters": [
+ {
+ "name": "maxTotalBufferSize",
+ "description": "Buffer size in bytes to use when preserving network payloads (XHRs, etc).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxResourceBufferSize",
+ "description": "Per-resource buffer size in bytes to use when preserving network payloads (XHRs, etc).",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxPostDataSize",
+ "description": "Longest post body size (in bytes) that would be included in requestWillBeSent notification",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "getAllCookies",
+ "description": "Returns all browser cookies. Depending on the backend support, will return detailed cookie\ninformation in the `cookies` field.\nDeprecated. Use Storage.getCookies instead.",
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getCertificate",
+ "description": "Returns the DER-encoded certificate.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to get certificate for.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "tableNames",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies for the current URL. Depending on the backend support, will return\ndetailed cookie information in the `cookies` field.",
+ "parameters": [
+ {
+ "name": "urls",
+ "description": "The list of URLs for which applicable cookies will be fetched.\nIf not specified, it's assumed to be set to the list containing\nthe URLs of the page and all of its subframes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getResponseBody",
+ "description": "Returns content served for the given request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getRequestPostData",
+ "description": "Returns post data sent with the request. Returns an error when no data was sent with the request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network request to get content for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "postData",
+ "description": "Request body string, omitting files from multipart requests",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getResponseBodyForInterception",
+ "description": "Returns content served for the given currently intercepted request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "description": "Identifier for the intercepted request to get body for.",
+ "$ref": "InterceptionId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeResponseBodyForInterceptionAsStream",
+ "description": "Returns a handle to the stream representing the response body. Note that after this command,\nthe intercepted request can't be continued as is -- you either need to cancel it or to provide\nthe response body. The stream only supports sequential read, IO.read will fail if the position\nis specified.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "$ref": "InterceptionId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stream",
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "replayXHR",
+ "description": "This method sends a new XMLHttpRequest which is identical to the original one. The following\nparameters should be identical: method, url, async, request body, extra headers, withCredentials\nattribute, user, password.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of XHR to replay.",
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "searchInResponseBody",
+ "description": "Searches for given string in response content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier of the network response to search.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "Debugger.SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBlockedURLs",
+ "description": "Blocks URLs from loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "urls",
+ "description": "URL patterns to block. Wildcards ('*') are allowed.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBypassServiceWorker",
+ "description": "Toggles ignoring of service worker for each request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "bypass",
+ "description": "Bypass service worker and load from network.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCacheDisabled",
+ "description": "Toggles ignoring cache for each request. If `true`, cache will not be used.",
+ "parameters": [
+ {
+ "name": "cacheDisabled",
+ "description": "Cache disabled state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCookie",
+ "description": "Sets a cookie with the given cookie data; may overwrite equivalent cookies if they exist.",
+ "parameters": [
+ {
+ "name": "name",
+ "description": "Cookie name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Cookie value.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The request-URI to associate with the setting of the cookie. This value can affect the\ndefault domain, path, source port, and source scheme values of the created cookie.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domain",
+ "description": "Cookie domain.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "path",
+ "description": "Cookie path.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "secure",
+ "description": "True if cookie is secure.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "httpOnly",
+ "description": "True if cookie is http-only.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sameSite",
+ "description": "Cookie SameSite type.",
+ "optional": true,
+ "$ref": "CookieSameSite"
+ },
+ {
+ "name": "expires",
+ "description": "Cookie expiration date, session cookie if not set",
+ "optional": true,
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "priority",
+ "description": "Cookie Priority type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookiePriority"
+ },
+ {
+ "name": "sameParty",
+ "description": "True if cookie is SameParty.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceScheme",
+ "description": "Cookie source scheme type.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CookieSourceScheme"
+ },
+ {
+ "name": "sourcePort",
+ "description": "Cookie source port. Valid values are {-1, [1, 65535]}, -1 indicates an unspecified port.\nAn unspecified port value allows protocol clients to emulate legacy cookie scope for the port.\nThis is a temporary ability and it will be removed in the future.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "partitionKey",
+ "description": "Cookie partition key. The site of the top-level URL the browser was visiting at the start\nof the request to the endpoint that set the cookie.\nIf not set, the cookie will be set as not partitioned.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "success",
+ "description": "Always set to true. If an error occurs, the response indicates protocol error.",
+ "deprecated": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setCookies",
+ "description": "Sets given cookies.",
+ "parameters": [
+ {
+ "name": "cookies",
+ "description": "Cookies to be set.",
+ "type": "array",
+ "items": {
+ "$ref": "CookieParam"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setExtraHTTPHeaders",
+ "description": "Specifies whether to always send extra HTTP headers with the requests from this page.",
+ "parameters": [
+ {
+ "name": "headers",
+ "description": "Map with extra HTTP headers.",
+ "$ref": "Headers"
+ }
+ ]
+ },
+ {
+ "name": "setAttachDebugStack",
+ "description": "Specifies whether to attach a page script stack id in requests",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to attach a page script stack for debugging purpose.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setRequestInterception",
+ "description": "Sets the requests to intercept that match the provided patterns and optionally resource types.\nDeprecated, please use Fetch.enable instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "Requests matching any of these patterns will be forwarded and wait for the corresponding\ncontinueInterceptedRequest call.",
+ "type": "array",
+ "items": {
+ "$ref": "RequestPattern"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setUserAgentOverride",
+ "description": "Allows overriding user agent with the given string.",
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "userAgent",
+ "description": "User agent to use.",
+ "type": "string"
+ },
+ {
+ "name": "acceptLanguage",
+ "description": "Browser langugage to emulate.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "platform",
+ "description": "The platform navigator.platform should return.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "userAgentMetadata",
+ "description": "To be sent in Sec-CH-UA-* headers and returned in navigator.userAgentData",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Emulation.UserAgentMetadata"
+ }
+ ]
+ },
+ {
+ "name": "getSecurityIsolationStatus",
+ "description": "Returns information about the COEP/COOP isolation status.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "If no frameId is provided, the status of the target is provided.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "status",
+ "$ref": "SecurityIsolationStatus"
+ }
+ ]
+ },
+ {
+ "name": "enableReportingApi",
+ "description": "Enables tracking for the Reporting API, events generated by the Reporting API will now be delivered to the client.\nEnabling triggers 'reportingApiReportAdded' for all existing reports.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "description": "Whether to enable or disable events for the Reporting API",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "loadNetworkResource",
+ "description": "Fetches the resource and returns the content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to get the resource for. Mandatory for frame targets, and\nshould be omitted for worker targets.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to get content for.",
+ "type": "string"
+ },
+ {
+ "name": "options",
+ "description": "Options for the request.",
+ "$ref": "LoadNetworkResourceOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "resource",
+ "$ref": "LoadNetworkResourcePageResult"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dataReceived",
+ "description": "Fired when data chunk was received over the network.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "dataLength",
+ "description": "Data chunk length.",
+ "type": "integer"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Actual bytes received (might be less than dataLength for compressed encodings).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "eventSourceMessageReceived",
+ "description": "Fired when EventSource message is received.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "eventName",
+ "description": "Message type.",
+ "type": "string"
+ },
+ {
+ "name": "eventId",
+ "description": "Message identifier.",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Message content.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "loadingFailed",
+ "description": "Fired when HTTP request has failed to load.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "type",
+ "description": "Resource type.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "errorText",
+ "description": "User friendly error message.",
+ "type": "string"
+ },
+ {
+ "name": "canceled",
+ "description": "True if loading was canceled.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "blockedReason",
+ "description": "The reason why loading was blocked, if any.",
+ "optional": true,
+ "$ref": "BlockedReason"
+ },
+ {
+ "name": "corsErrorStatus",
+ "description": "The reason why loading was blocked by CORS, if any.",
+ "optional": true,
+ "$ref": "CorsErrorStatus"
+ }
+ ]
+ },
+ {
+ "name": "loadingFinished",
+ "description": "Fired when HTTP request has finished loading.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "encodedDataLength",
+ "description": "Total number of bytes received for this request.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "requestIntercepted",
+ "description": "Details of an intercepted HTTP request, which must be either allowed, blocked, modified or\nmocked.\nDeprecated, use Fetch.requestPaused instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "interceptionId",
+ "description": "Each request the page makes will have a unique id, however if any redirects are encountered\nwhile processing that fetch, they will be reported with the same id as the original fetch.\nLikewise if HTTP authentication is needed then the same fetch id will be used.",
+ "$ref": "InterceptionId"
+ },
+ {
+ "name": "request",
+ "$ref": "Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "isNavigationRequest",
+ "description": "Whether this is a navigation request, which can abort the navigation completely.",
+ "type": "boolean"
+ },
+ {
+ "name": "isDownload",
+ "description": "Set if the request is a navigation that will result in a download.\nOnly present after response is received from the server (i.e. HeadersReceived stage).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "redirectUrl",
+ "description": "Redirect location, only sent if a redirect was intercepted.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "authChallenge",
+ "description": "Details of the Authorization Challenge encountered. If this is set then\ncontinueInterceptedRequest must contain an authChallengeResponse.",
+ "optional": true,
+ "$ref": "AuthChallenge"
+ },
+ {
+ "name": "responseErrorReason",
+ "description": "Response error if intercepted at response stage or if redirect occurred while intercepting\nrequest.",
+ "optional": true,
+ "$ref": "ErrorReason"
+ },
+ {
+ "name": "responseStatusCode",
+ "description": "Response code if intercepted at response stage or if redirect occurred while intercepting\nrequest or auth retry occurred.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers if intercepted at the response stage or if redirect occurred while\nintercepting request or auth retry occurred.",
+ "optional": true,
+ "$ref": "Headers"
+ },
+ {
+ "name": "requestId",
+ "description": "If the intercepted request had a corresponding requestWillBeSent event fired for it, then\nthis requestId will be the same as the requestId present in the requestWillBeSent event.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "requestServedFromCache",
+ "description": "Fired if request ended up loading from cache.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "requestWillBeSent",
+ "description": "Fired when page is about to send HTTP request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "LoaderId"
+ },
+ {
+ "name": "documentURL",
+ "description": "URL of the document this request is loaded for.",
+ "type": "string"
+ },
+ {
+ "name": "request",
+ "description": "Request data.",
+ "$ref": "Request"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "wallTime",
+ "description": "Timestamp.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "$ref": "Initiator"
+ },
+ {
+ "name": "redirectHasExtraInfo",
+ "description": "In the case that redirectResponse is populated, this flag indicates whether\nrequestWillBeSentExtraInfo and responseReceivedExtraInfo events will be or were emitted\nfor the request which was just redirected.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "redirectResponse",
+ "description": "Redirect response data.",
+ "optional": true,
+ "$ref": "Response"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "optional": true,
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame identifier.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "hasUserGesture",
+ "description": "Whether the request is initiated by a user gesture. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resourceChangedPriority",
+ "description": "Fired when resource loading priority is changed",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "newPriority",
+ "description": "New priority",
+ "$ref": "ResourcePriority"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "signedExchangeReceived",
+ "description": "Fired when a signed exchange was received over the network",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "info",
+ "description": "Information about the signed exchange response.",
+ "$ref": "SignedExchangeInfo"
+ }
+ ]
+ },
+ {
+ "name": "responseReceived",
+ "description": "Fired when HTTP response is available.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "LoaderId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "type",
+ "description": "Resource type.",
+ "$ref": "ResourceType"
+ },
+ {
+ "name": "response",
+ "description": "Response data.",
+ "$ref": "Response"
+ },
+ {
+ "name": "hasExtraInfo",
+ "description": "Indicates whether requestWillBeSentExtraInfo and responseReceivedExtraInfo events will be\nor were emitted for this request.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame identifier.",
+ "optional": true,
+ "$ref": "Page.FrameId"
+ }
+ ]
+ },
+ {
+ "name": "webSocketClosed",
+ "description": "Fired when WebSocket is closed.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "webSocketCreated",
+ "description": "Fired upon WebSocket creation.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "WebSocket request URL.",
+ "type": "string"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "optional": true,
+ "$ref": "Initiator"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameError",
+ "description": "Fired when WebSocket message error occurs.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "errorMessage",
+ "description": "WebSocket error message.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameReceived",
+ "description": "Fired when WebSocket message is received.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketFrame"
+ }
+ ]
+ },
+ {
+ "name": "webSocketFrameSent",
+ "description": "Fired when WebSocket message is sent.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketFrame"
+ }
+ ]
+ },
+ {
+ "name": "webSocketHandshakeResponseReceived",
+ "description": "Fired when WebSocket handshake response becomes available.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "response",
+ "description": "WebSocket response data.",
+ "$ref": "WebSocketResponse"
+ }
+ ]
+ },
+ {
+ "name": "webSocketWillSendHandshakeRequest",
+ "description": "Fired when WebSocket is about to initiate handshake.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "wallTime",
+ "description": "UTC Timestamp.",
+ "$ref": "TimeSinceEpoch"
+ },
+ {
+ "name": "request",
+ "description": "WebSocket request data.",
+ "$ref": "WebSocketRequest"
+ }
+ ]
+ },
+ {
+ "name": "webTransportCreated",
+ "description": "Fired upon WebTransport creation.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "WebTransport request URL.",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ },
+ {
+ "name": "initiator",
+ "description": "Request initiator.",
+ "optional": true,
+ "$ref": "Initiator"
+ }
+ ]
+ },
+ {
+ "name": "webTransportConnectionEstablished",
+ "description": "Fired when WebTransport handshake is finished.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "webTransportClosed",
+ "description": "Fired when WebTransport is disposed.",
+ "parameters": [
+ {
+ "name": "transportId",
+ "description": "WebTransport identifier.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Timestamp.",
+ "$ref": "MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "requestWillBeSentExtraInfo",
+ "description": "Fired when additional information about a requestWillBeSent event is available from the\nnetwork stack. Not every requestWillBeSent event will have an additional\nrequestWillBeSentExtraInfo fired for it, and there is no guarantee whether requestWillBeSent\nor requestWillBeSentExtraInfo will be fired first for the same request.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to an existing requestWillBeSent event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "associatedCookies",
+ "description": "A list of cookies potentially associated to the requested URL. This includes both cookies sent with\nthe request and the ones not sent; the latter are distinguished by having blockedReason field set.",
+ "type": "array",
+ "items": {
+ "$ref": "BlockedCookieWithReason"
+ }
+ },
+ {
+ "name": "headers",
+ "description": "Raw request headers as they will be sent over the wire.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "connectTiming",
+ "description": "Connection timing information for the request.",
+ "experimental": true,
+ "$ref": "ConnectTiming"
+ },
+ {
+ "name": "clientSecurityState",
+ "description": "The client security state set for the request.",
+ "optional": true,
+ "$ref": "ClientSecurityState"
+ },
+ {
+ "name": "siteHasCookieInOtherPartition",
+ "description": "Whether the site has partitioned cookies stored in a partition different than the current one.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "responseReceivedExtraInfo",
+ "description": "Fired when additional information about a responseReceived event is available from the network\nstack. Not every responseReceived event will have an additional responseReceivedExtraInfo for\nit, and responseReceivedExtraInfo may be fired before or after responseReceived.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another responseReceived event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "blockedCookies",
+ "description": "A list of cookies which were not stored from the response along with the corresponding\nreasons for blocking. The cookies here may not be valid due to syntax errors, which\nare represented by the invalid cookie line string instead of a proper cookie.",
+ "type": "array",
+ "items": {
+ "$ref": "BlockedSetCookieWithReason"
+ }
+ },
+ {
+ "name": "headers",
+ "description": "Raw response headers as they were received over the wire.",
+ "$ref": "Headers"
+ },
+ {
+ "name": "resourceIPAddressSpace",
+ "description": "The IP address space of the resource. The address space can only be determined once the transport\nestablished the connection, so we can't send it in `requestWillBeSentExtraInfo`.",
+ "$ref": "IPAddressSpace"
+ },
+ {
+ "name": "statusCode",
+ "description": "The status code of the response. This is useful in cases the request failed and no responseReceived\nevent is triggered, which is the case for, e.g., CORS errors. This is also the correct status code\nfor cached requests, where the status in responseReceived is a 200 and this will be 304.",
+ "type": "integer"
+ },
+ {
+ "name": "headersText",
+ "description": "Raw response header text as it was received over the wire. The raw text may not always be\navailable, such as in the case of HTTP/2 or QUIC.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookiePartitionKey",
+ "description": "The cookie partition key that will be used to store partitioned cookies set in this response.\nOnly sent when partitioned cookies are enabled.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cookiePartitionKeyOpaque",
+ "description": "True if partitioned cookies are enabled, but the partition key is not serializeable to string.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "trustTokenOperationDone",
+ "description": "Fired exactly once for each Trust Token operation. Depending on\nthe type of the operation and whether the operation succeeded or\nfailed, the event is fired before the corresponding request was sent\nor after the response was received.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "status",
+ "description": "Detailed success or error status of the operation.\n'AlreadyExists' also signifies a successful operation, as the result\nof the operation already exists und thus, the operation was abort\npreemptively (e.g. a cache hit).",
+ "type": "string",
+ "enum": [
+ "Ok",
+ "InvalidArgument",
+ "MissingIssuerKeys",
+ "FailedPrecondition",
+ "ResourceExhausted",
+ "AlreadyExists",
+ "Unavailable",
+ "Unauthorized",
+ "BadResponse",
+ "InternalError",
+ "UnknownError",
+ "FulfilledLocally"
+ ]
+ },
+ {
+ "name": "type",
+ "$ref": "TrustTokenOperationType"
+ },
+ {
+ "name": "requestId",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "topLevelOrigin",
+ "description": "Top level origin. The context in which the operation was attempted.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "issuerOrigin",
+ "description": "Origin of the issuer in case of a \"Issuance\" or \"Redemption\" operation.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "issuedTokenCount",
+ "description": "The number of obtained Trust Tokens on a successful \"Issuance\" operation.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleMetadataReceived",
+ "description": "Fired once when parsing the .wbn file has succeeded.\nThe event contains the information about the web bundle contents.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "urls",
+ "description": "A list of URLs of resources in the subresource Web Bundle.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleMetadataError",
+ "description": "Fired once when parsing the .wbn file has failed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Request identifier. Used to match this information to another event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "errorMessage",
+ "description": "Error message",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleInnerResponseParsed",
+ "description": "Fired when handling requests for resources within a .wbn file.\nNote: this will only be fired for resources that are requested by the webpage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "innerRequestId",
+ "description": "Request identifier of the subresource request",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "innerRequestURL",
+ "description": "URL of the subresource resource.",
+ "type": "string"
+ },
+ {
+ "name": "bundleRequestId",
+ "description": "Bundle request identifier. Used to match this information to another event.\nThis made be absent in case when the instrumentation was enabled only\nafter webbundle was parsed.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "subresourceWebBundleInnerResponseError",
+ "description": "Fired when request for resources within a .wbn file failed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "innerRequestId",
+ "description": "Request identifier of the subresource request",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "innerRequestURL",
+ "description": "URL of the subresource resource.",
+ "type": "string"
+ },
+ {
+ "name": "errorMessage",
+ "description": "Error message",
+ "type": "string"
+ },
+ {
+ "name": "bundleRequestId",
+ "description": "Bundle request identifier. Used to match this information to another event.\nThis made be absent in case when the instrumentation was enabled only\nafter webbundle was parsed.",
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiReportAdded",
+ "description": "Is sent whenever a new report is added.\nAnd after 'enableReportingApi' for all existing reports.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "report",
+ "$ref": "ReportingApiReport"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiReportUpdated",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "report",
+ "$ref": "ReportingApiReport"
+ }
+ ]
+ },
+ {
+ "name": "reportingApiEndpointsChangedForOrigin",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin of the document(s) which configured the endpoints.",
+ "type": "string"
+ },
+ {
+ "name": "endpoints",
+ "type": "array",
+ "items": {
+ "$ref": "ReportingApiEndpoint"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Overlay",
+ "description": "This domain provides various functionality related to drawing atop the inspected page.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Page",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "SourceOrderConfig",
+ "description": "Configuration data for drawing the source order of an elements children.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "parentOutlineColor",
+ "description": "the color to outline the givent element in.",
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "childOutlineColor",
+ "description": "the color to outline the child elements in.",
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "GridHighlightConfig",
+ "description": "Configuration data for the highlighting of Grid elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "showGridExtensionLines",
+ "description": "Whether the extension lines from grid cells to the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showPositiveLineNumbers",
+ "description": "Show Positive line number labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showNegativeLineNumbers",
+ "description": "Show Negative line number labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showAreaNames",
+ "description": "Show area name labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showLineNames",
+ "description": "Show line name labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showTrackSizes",
+ "description": "Show track size labels (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "gridBorderColor",
+ "description": "The grid container border highlight color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "cellBorderColor",
+ "description": "The cell border color (default: transparent). Deprecated, please use rowLineColor and columnLineColor instead.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "rowLineColor",
+ "description": "The row line color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnLineColor",
+ "description": "The column line color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "gridBorderDash",
+ "description": "Whether the grid border is dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "cellBorderDash",
+ "description": "Whether the cell border is dashed (default: false). Deprecated, please us rowLineDash and columnLineDash instead.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "rowLineDash",
+ "description": "Whether row lines are dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "columnLineDash",
+ "description": "Whether column lines are dashed (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "rowGapColor",
+ "description": "The row gap highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "rowHatchColor",
+ "description": "The row gap hatching fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnGapColor",
+ "description": "The column gap highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "columnHatchColor",
+ "description": "The column gap hatching fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "areaBorderColor",
+ "description": "The named grid areas border color (Default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "gridBackgroundColor",
+ "description": "The grid container background color (Default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "FlexContainerHighlightConfig",
+ "description": "Configuration data for the highlighting of Flex container elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerBorder",
+ "description": "The style of the container border",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "lineSeparator",
+ "description": "The style of the separator between lines",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "itemSeparator",
+ "description": "The style of the separator between items",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "mainDistributedSpace",
+ "description": "Style of content-distribution space on the main axis (justify-content).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "crossDistributedSpace",
+ "description": "Style of content-distribution space on the cross axis (align-content).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "rowGapSpace",
+ "description": "Style of empty space caused by row gaps (gap/row-gap).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "columnGapSpace",
+ "description": "Style of empty space caused by columns gaps (gap/column-gap).",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "crossAlignment",
+ "description": "Style of the self-alignment line (align-items).",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "FlexItemHighlightConfig",
+ "description": "Configuration data for the highlighting of Flex item elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "baseSizeBox",
+ "description": "Style of the box representing the item's base size",
+ "optional": true,
+ "$ref": "BoxStyle"
+ },
+ {
+ "name": "baseSizeBorder",
+ "description": "Style of the border around the box representing the item's base size",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "flexibilityArrow",
+ "description": "Style of the arrow representing if the item grew or shrank",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "LineStyle",
+ "description": "Style information for drawing a line.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "color",
+ "description": "The color of the line (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "pattern",
+ "description": "The line pattern (default: solid)",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "dashed",
+ "dotted"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "BoxStyle",
+ "description": "Style information for drawing a box.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "fillColor",
+ "description": "The background color for the box (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "hatchColor",
+ "description": "The hatching color for the box (default: transparent)",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ContrastAlgorithm",
+ "type": "string",
+ "enum": [
+ "aa",
+ "aaa",
+ "apca"
+ ]
+ },
+ {
+ "id": "HighlightConfig",
+ "description": "Configuration data for the highlighting of page elements.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "showInfo",
+ "description": "Whether the node info tooltip should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showStyles",
+ "description": "Whether the node styles in the tooltip (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showRulers",
+ "description": "Whether the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showAccessibilityInfo",
+ "description": "Whether the a11y info should be shown (default: true).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "showExtensionLines",
+ "description": "Whether the extension lines from node to the rulers should be shown (default: false).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "paddingColor",
+ "description": "The padding highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "borderColor",
+ "description": "The border highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "marginColor",
+ "description": "The margin highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "eventTargetColor",
+ "description": "The event target element highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "shapeColor",
+ "description": "The shape outside fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "shapeMarginColor",
+ "description": "The shape margin fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "cssGridColor",
+ "description": "The grid layout color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "colorFormat",
+ "description": "The color format used to format color styles (default: hex).",
+ "optional": true,
+ "$ref": "ColorFormat"
+ },
+ {
+ "name": "gridHighlightConfig",
+ "description": "The grid layout highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "GridHighlightConfig"
+ },
+ {
+ "name": "flexContainerHighlightConfig",
+ "description": "The flex container highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "FlexContainerHighlightConfig"
+ },
+ {
+ "name": "flexItemHighlightConfig",
+ "description": "The flex item highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "FlexItemHighlightConfig"
+ },
+ {
+ "name": "contrastAlgorithm",
+ "description": "The contrast algorithm to use for the contrast ratio (default: aa).",
+ "optional": true,
+ "$ref": "ContrastAlgorithm"
+ },
+ {
+ "name": "containerQueryContainerHighlightConfig",
+ "description": "The container query container highlight configuration (default: all transparent).",
+ "optional": true,
+ "$ref": "ContainerQueryContainerHighlightConfig"
+ }
+ ]
+ },
+ {
+ "id": "ColorFormat",
+ "type": "string",
+ "enum": [
+ "rgb",
+ "hsl",
+ "hwb",
+ "hex"
+ ]
+ },
+ {
+ "id": "GridNodeHighlightConfig",
+ "description": "Configurations for Persistent Grid Highlight",
+ "type": "object",
+ "properties": [
+ {
+ "name": "gridHighlightConfig",
+ "description": "A descriptor for the highlight appearance.",
+ "$ref": "GridHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "FlexNodeHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "flexContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of flex containers.",
+ "$ref": "FlexContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "ScrollSnapContainerHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "snapportBorder",
+ "description": "The style of the snapport border (default: transparent)",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "snapAreaBorder",
+ "description": "The style of the snap area border (default: transparent)",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "scrollMarginColor",
+ "description": "The margin highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "scrollPaddingColor",
+ "description": "The padding highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ScrollSnapHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scrollSnapContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of scroll snap containers.",
+ "$ref": "ScrollSnapContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "HingeConfig",
+ "description": "Configuration for dual screen hinge",
+ "type": "object",
+ "properties": [
+ {
+ "name": "rect",
+ "description": "A rectangle represent hinge",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: a dark color).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The content box highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "ContainerQueryHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerQueryContainerHighlightConfig",
+ "description": "A descriptor for the highlight appearance of container query containers.",
+ "$ref": "ContainerQueryContainerHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the container node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "ContainerQueryContainerHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "containerBorder",
+ "description": "The style of the container border.",
+ "optional": true,
+ "$ref": "LineStyle"
+ },
+ {
+ "name": "descendantBorder",
+ "description": "The style of the descendants' borders.",
+ "optional": true,
+ "$ref": "LineStyle"
+ }
+ ]
+ },
+ {
+ "id": "IsolatedElementHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "isolationModeHighlightConfig",
+ "description": "A descriptor for the highlight appearance of an element in isolation mode.",
+ "$ref": "IsolationModeHighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the isolated element to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "id": "IsolationModeHighlightConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "resizerColor",
+ "description": "The fill color of the resizers (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "resizerHandleColor",
+ "description": "The fill color for resizer handles (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "maskColor",
+ "description": "The fill color for the mask covering non-isolated elements (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "id": "InspectMode",
+ "type": "string",
+ "enum": [
+ "searchForNode",
+ "searchForUAShadowDOM",
+ "captureAreaScreenshot",
+ "showDistances",
+ "none"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables domain notifications."
+ },
+ {
+ "name": "getHighlightObjectForTest",
+ "description": "For testing.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to get highlight object for.",
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "includeDistance",
+ "description": "Whether to include distance info.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeStyle",
+ "description": "Whether to include style info.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "colorFormat",
+ "description": "The color format to get config with (default: hex).",
+ "optional": true,
+ "$ref": "ColorFormat"
+ },
+ {
+ "name": "showAccessibilityInfo",
+ "description": "Whether to show accessibility info (default: true).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlight",
+ "description": "Highlight data for the node.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "getGridHighlightObjectsForTest",
+ "description": "For Persistent Grid testing.",
+ "parameters": [
+ {
+ "name": "nodeIds",
+ "description": "Ids of the node to get highlight object for.",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.NodeId"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlights",
+ "description": "Grid Highlight data for the node ids provided.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "getSourceOrderHighlightObjectForTest",
+ "description": "For Source Order Viewer testing.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "description": "Id of the node to highlight.",
+ "$ref": "DOM.NodeId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "highlight",
+ "description": "Source order highlight data for the node id provided.",
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "name": "hideHighlight",
+ "description": "Hides any highlight."
+ },
+ {
+ "name": "highlightFrame",
+ "description": "Highlights owner element of the frame with given id.\nDeprecated: Doesn't work reliablity and cannot be fixed due to process\nseparatation (the owner node might be in a different process). Determine\nthe owner node in the client and use highlightNode.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Identifier of the frame to highlight.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "contentColor",
+ "description": "The content box highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "contentOutlineColor",
+ "description": "The content box highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightNode",
+ "description": "Highlights DOM node with given id or with the given JavaScript object wrapper. Either nodeId or\nobjectId must be specified.",
+ "parameters": [
+ {
+ "name": "highlightConfig",
+ "description": "A descriptor for the highlight appearance.",
+ "$ref": "HighlightConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to highlight.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node to be highlighted.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "selector",
+ "description": "Selectors to highlight relevant nodes.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "highlightQuad",
+ "description": "Highlights given quad. Coordinates are absolute with respect to the main frame viewport.",
+ "parameters": [
+ {
+ "name": "quad",
+ "description": "Quad to highlight",
+ "$ref": "DOM.Quad"
+ },
+ {
+ "name": "color",
+ "description": "The highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightRect",
+ "description": "Highlights given rectangle. Coordinates are absolute with respect to the main frame viewport.",
+ "parameters": [
+ {
+ "name": "x",
+ "description": "X coordinate",
+ "type": "integer"
+ },
+ {
+ "name": "y",
+ "description": "Y coordinate",
+ "type": "integer"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height",
+ "type": "integer"
+ },
+ {
+ "name": "color",
+ "description": "The highlight fill color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ },
+ {
+ "name": "outlineColor",
+ "description": "The highlight outline color (default: transparent).",
+ "optional": true,
+ "$ref": "DOM.RGBA"
+ }
+ ]
+ },
+ {
+ "name": "highlightSourceOrder",
+ "description": "Highlights the source order of the children of the DOM node with given id or with the given\nJavaScript object wrapper. Either nodeId or objectId must be specified.",
+ "parameters": [
+ {
+ "name": "sourceOrderConfig",
+ "description": "A descriptor for the appearance of the overlay drawing.",
+ "$ref": "SourceOrderConfig"
+ },
+ {
+ "name": "nodeId",
+ "description": "Identifier of the node to highlight.",
+ "optional": true,
+ "$ref": "DOM.NodeId"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Identifier of the backend node to highlight.",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "objectId",
+ "description": "JavaScript object id of the node to be highlighted.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "setInspectMode",
+ "description": "Enters the 'inspect' mode. In this mode, elements that user is hovering over are highlighted.\nBackend then generates 'inspectNodeRequested' event upon element selection.",
+ "parameters": [
+ {
+ "name": "mode",
+ "description": "Set an inspection mode.",
+ "$ref": "InspectMode"
+ },
+ {
+ "name": "highlightConfig",
+ "description": "A descriptor for the highlight appearance of hovered-over nodes. May be omitted if `enabled\n== false`.",
+ "optional": true,
+ "$ref": "HighlightConfig"
+ }
+ ]
+ },
+ {
+ "name": "setShowAdHighlights",
+ "description": "Highlights owner element of all frames detected to be ads.",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing ad highlights",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPausedInDebuggerMessage",
+ "parameters": [
+ {
+ "name": "message",
+ "description": "The message to display, also triggers resume and step over controls.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setShowDebugBorders",
+ "description": "Requests that backend shows debug borders on layers",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing debug borders",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowFPSCounter",
+ "description": "Requests that backend shows the FPS counter",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing the FPS counter",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowGridOverlays",
+ "description": "Highlight multiple elements with the CSS Grid overlay.",
+ "parameters": [
+ {
+ "name": "gridNodeHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "GridNodeHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowFlexOverlays",
+ "parameters": [
+ {
+ "name": "flexNodeHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "FlexNodeHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowScrollSnapOverlays",
+ "parameters": [
+ {
+ "name": "scrollSnapHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "ScrollSnapHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowContainerQueryOverlays",
+ "parameters": [
+ {
+ "name": "containerQueryHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "ContainerQueryHighlightConfig"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setShowPaintRects",
+ "description": "Requests that backend shows paint rectangles",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "True for showing paint rectangles",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowLayoutShiftRegions",
+ "description": "Requests that backend shows layout shift regions",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "True for showing layout shift regions",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowScrollBottleneckRects",
+ "description": "Requests that backend shows scroll bottleneck rects",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing scroll bottleneck rects",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowHitTestBorders",
+ "description": "Deprecated, no longer has any effect.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "show",
+ "description": "True for showing hit-test borders",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowWebVitals",
+ "description": "Request that backend shows an overlay with web vital metrics.",
+ "parameters": [
+ {
+ "name": "show",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowViewportSizeOnResize",
+ "description": "Paints viewport size upon main frame resize.",
+ "parameters": [
+ {
+ "name": "show",
+ "description": "Whether to paint size or not.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setShowHinge",
+ "description": "Add a dual screen device hinge",
+ "parameters": [
+ {
+ "name": "hingeConfig",
+ "description": "hinge data, null means hideHinge",
+ "optional": true,
+ "$ref": "HingeConfig"
+ }
+ ]
+ },
+ {
+ "name": "setShowIsolatedElements",
+ "description": "Show elements in isolation mode with overlays.",
+ "parameters": [
+ {
+ "name": "isolatedElementHighlightConfigs",
+ "description": "An array of node identifiers and descriptors for the highlight appearance.",
+ "type": "array",
+ "items": {
+ "$ref": "IsolatedElementHighlightConfig"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "inspectNodeRequested",
+ "description": "Fired when the node should be inspected. This happens after call to `setInspectMode` or when\nuser manually inspects an element.",
+ "parameters": [
+ {
+ "name": "backendNodeId",
+ "description": "Id of the node to inspect.",
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "name": "nodeHighlightRequested",
+ "description": "Fired when the node should be highlighted. This happens after call to `setInspectMode`.",
+ "parameters": [
+ {
+ "name": "nodeId",
+ "$ref": "DOM.NodeId"
+ }
+ ]
+ },
+ {
+ "name": "screenshotRequested",
+ "description": "Fired when user asks to capture screenshot of some area on the page.",
+ "parameters": [
+ {
+ "name": "viewport",
+ "description": "Viewport to capture, in device independent pixels (dip).",
+ "$ref": "Page.Viewport"
+ }
+ ]
+ },
+ {
+ "name": "inspectModeCanceled",
+ "description": "Fired when user cancels the inspect mode."
+ }
+ ]
+ },
+ {
+ "domain": "Page",
+ "description": "Actions and events related to the inspected page belong to the page domain.",
+ "dependencies": [
+ "Debugger",
+ "DOM",
+ "IO",
+ "Network",
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "FrameId",
+ "description": "Unique frame identifier.",
+ "type": "string"
+ },
+ {
+ "id": "AdFrameType",
+ "description": "Indicates whether a frame has been identified as an ad.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "child",
+ "root"
+ ]
+ },
+ {
+ "id": "AdFrameExplanation",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "ParentIsAd",
+ "CreatedByAdScript",
+ "MatchedBlockingRule"
+ ]
+ },
+ {
+ "id": "AdFrameStatus",
+ "description": "Indicates whether a frame has been identified as an ad and why.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "adFrameType",
+ "$ref": "AdFrameType"
+ },
+ {
+ "name": "explanations",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "AdFrameExplanation"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AdScriptId",
+ "description": "Identifies the bottom-most script which caused the frame to be labelled\nas an ad.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script Id of the bottom-most script which caused the frame to be labelled\nas an ad.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "debuggerId",
+ "description": "Id of adScriptId's debugger.",
+ "$ref": "Runtime.UniqueDebuggerId"
+ }
+ ]
+ },
+ {
+ "id": "SecureContextType",
+ "description": "Indicates whether the frame is a secure context and why it is the case.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Secure",
+ "SecureLocalhost",
+ "InsecureScheme",
+ "InsecureAncestor"
+ ]
+ },
+ {
+ "id": "CrossOriginIsolatedContextType",
+ "description": "Indicates whether the frame is cross-origin isolated and why it is the case.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Isolated",
+ "NotIsolated",
+ "NotIsolatedFeatureDisabled"
+ ]
+ },
+ {
+ "id": "GatedAPIFeatures",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SharedArrayBuffers",
+ "SharedArrayBuffersTransferAllowed",
+ "PerformanceMeasureMemory",
+ "PerformanceProfile"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyFeature",
+ "description": "All Permissions Policy features. This enum should match the one defined\nin third_party/blink/renderer/core/permissions_policy/permissions_policy_features.json5.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "accelerometer",
+ "ambient-light-sensor",
+ "attribution-reporting",
+ "autoplay",
+ "bluetooth",
+ "browsing-topics",
+ "camera",
+ "ch-dpr",
+ "ch-device-memory",
+ "ch-downlink",
+ "ch-ect",
+ "ch-prefers-color-scheme",
+ "ch-prefers-reduced-motion",
+ "ch-rtt",
+ "ch-save-data",
+ "ch-ua",
+ "ch-ua-arch",
+ "ch-ua-bitness",
+ "ch-ua-platform",
+ "ch-ua-model",
+ "ch-ua-mobile",
+ "ch-ua-form-factor",
+ "ch-ua-full-version",
+ "ch-ua-full-version-list",
+ "ch-ua-platform-version",
+ "ch-ua-wow64",
+ "ch-viewport-height",
+ "ch-viewport-width",
+ "ch-width",
+ "clipboard-read",
+ "clipboard-write",
+ "compute-pressure",
+ "cross-origin-isolated",
+ "direct-sockets",
+ "display-capture",
+ "document-domain",
+ "encrypted-media",
+ "execution-while-out-of-viewport",
+ "execution-while-not-rendered",
+ "focus-without-user-activation",
+ "fullscreen",
+ "frobulate",
+ "gamepad",
+ "geolocation",
+ "gyroscope",
+ "hid",
+ "identity-credentials-get",
+ "idle-detection",
+ "interest-cohort",
+ "join-ad-interest-group",
+ "keyboard-map",
+ "local-fonts",
+ "magnetometer",
+ "microphone",
+ "midi",
+ "otp-credentials",
+ "payment",
+ "picture-in-picture",
+ "private-aggregation",
+ "private-state-token-issuance",
+ "private-state-token-redemption",
+ "publickey-credentials-get",
+ "run-ad-auction",
+ "screen-wake-lock",
+ "serial",
+ "shared-autofill",
+ "shared-storage",
+ "shared-storage-select-url",
+ "smart-card",
+ "storage-access",
+ "sync-xhr",
+ "unload",
+ "usb",
+ "vertical-scroll",
+ "web-share",
+ "window-management",
+ "window-placement",
+ "xr-spatial-tracking"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyBlockReason",
+ "description": "Reason for a permissions policy feature to be disabled.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Header",
+ "IframeAttribute",
+ "InFencedFrameTree",
+ "InIsolatedApp"
+ ]
+ },
+ {
+ "id": "PermissionsPolicyBlockLocator",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "blockReason",
+ "$ref": "PermissionsPolicyBlockReason"
+ }
+ ]
+ },
+ {
+ "id": "PermissionsPolicyFeatureState",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "feature",
+ "$ref": "PermissionsPolicyFeature"
+ },
+ {
+ "name": "allowed",
+ "type": "boolean"
+ },
+ {
+ "name": "locator",
+ "optional": true,
+ "$ref": "PermissionsPolicyBlockLocator"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrialTokenStatus",
+ "description": "Origin Trial(https://www.chromium.org/blink/origin-trials) support.\nStatus for an Origin Trial token.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Success",
+ "NotSupported",
+ "Insecure",
+ "Expired",
+ "WrongOrigin",
+ "InvalidSignature",
+ "Malformed",
+ "WrongVersion",
+ "FeatureDisabled",
+ "TokenDisabled",
+ "FeatureDisabledForUser",
+ "UnknownTrial"
+ ]
+ },
+ {
+ "id": "OriginTrialStatus",
+ "description": "Status for an Origin Trial.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Enabled",
+ "ValidTokenNotProvided",
+ "OSNotSupported",
+ "TrialNotAllowed"
+ ]
+ },
+ {
+ "id": "OriginTrialUsageRestriction",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "None",
+ "Subset"
+ ]
+ },
+ {
+ "id": "OriginTrialToken",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "matchSubDomains",
+ "type": "boolean"
+ },
+ {
+ "name": "trialName",
+ "type": "string"
+ },
+ {
+ "name": "expiryTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "isThirdParty",
+ "type": "boolean"
+ },
+ {
+ "name": "usageRestriction",
+ "$ref": "OriginTrialUsageRestriction"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrialTokenWithStatus",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "rawTokenText",
+ "type": "string"
+ },
+ {
+ "name": "parsedToken",
+ "description": "`parsedToken` is present only when the token is extractable and\nparsable.",
+ "optional": true,
+ "$ref": "OriginTrialToken"
+ },
+ {
+ "name": "status",
+ "$ref": "OriginTrialTokenStatus"
+ }
+ ]
+ },
+ {
+ "id": "OriginTrial",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "trialName",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "$ref": "OriginTrialStatus"
+ },
+ {
+ "name": "tokensWithStatus",
+ "type": "array",
+ "items": {
+ "$ref": "OriginTrialTokenWithStatus"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Frame",
+ "description": "Information about the Frame on the page.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Frame unique identifier.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "parentId",
+ "description": "Parent frame identifier.",
+ "optional": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Identifier of the loader associated with this frame.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "name",
+ "description": "Frame's name as specified in the tag.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "Frame document's URL without fragment.",
+ "type": "string"
+ },
+ {
+ "name": "urlFragment",
+ "description": "Frame document's URL fragment including the '#'.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "domainAndRegistry",
+ "description": "Frame document's registered domain, taking the public suffixes list into account.\nExtracted from the Frame's url.\nExample URLs: http://www.google.com/file.html -> \"google.com\"\n http://a.b.co.uk/file.html -> \"b.co.uk\"",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "name": "securityOrigin",
+ "description": "Frame document's security origin.",
+ "type": "string"
+ },
+ {
+ "name": "mimeType",
+ "description": "Frame document's mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "unreachableUrl",
+ "description": "If the frame failed to load, this contains the URL that could not be loaded. Note that unlike url above, this URL may contain a fragment.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "adFrameStatus",
+ "description": "Indicates whether this frame was tagged as an ad and why.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AdFrameStatus"
+ },
+ {
+ "name": "secureContextType",
+ "description": "Indicates whether the main document is a secure context and explains why that is the case.",
+ "experimental": true,
+ "$ref": "SecureContextType"
+ },
+ {
+ "name": "crossOriginIsolatedContextType",
+ "description": "Indicates whether this is a cross origin isolated context.",
+ "experimental": true,
+ "$ref": "CrossOriginIsolatedContextType"
+ },
+ {
+ "name": "gatedAPIFeatures",
+ "description": "Indicated which gated APIs / features are available.",
+ "experimental": true,
+ "type": "array",
+ "items": {
+ "$ref": "GatedAPIFeatures"
+ }
+ }
+ ]
+ },
+ {
+ "id": "FrameResource",
+ "description": "Information about the Resource on the page.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Resource URL.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Type of this resource.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "mimeType",
+ "description": "Resource mimeType as determined by the browser.",
+ "type": "string"
+ },
+ {
+ "name": "lastModified",
+ "description": "last-modified timestamp as reported by server.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "contentSize",
+ "description": "Resource content size.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "failed",
+ "description": "True if the resource failed to load.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "canceled",
+ "description": "True if the resource was canceled during loading.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "FrameResourceTree",
+ "description": "Information about the Frame hierarchy along with their cached resources.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "frame",
+ "description": "Frame information for this tree item.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "childFrames",
+ "description": "Child frames.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FrameResourceTree"
+ }
+ },
+ {
+ "name": "resources",
+ "description": "Information about frame resources.",
+ "type": "array",
+ "items": {
+ "$ref": "FrameResource"
+ }
+ }
+ ]
+ },
+ {
+ "id": "FrameTree",
+ "description": "Information about the Frame hierarchy.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frame",
+ "description": "Frame information for this tree item.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "childFrames",
+ "description": "Child frames.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "FrameTree"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ScriptIdentifier",
+ "description": "Unique script identifier.",
+ "type": "string"
+ },
+ {
+ "id": "TransitionType",
+ "description": "Transition type.",
+ "type": "string",
+ "enum": [
+ "link",
+ "typed",
+ "address_bar",
+ "auto_bookmark",
+ "auto_subframe",
+ "manual_subframe",
+ "generated",
+ "auto_toplevel",
+ "form_submit",
+ "reload",
+ "keyword",
+ "keyword_generated",
+ "other"
+ ]
+ },
+ {
+ "id": "NavigationEntry",
+ "description": "Navigation history entry.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the navigation history entry.",
+ "type": "integer"
+ },
+ {
+ "name": "url",
+ "description": "URL of the navigation history entry.",
+ "type": "string"
+ },
+ {
+ "name": "userTypedURL",
+ "description": "URL that the user typed in the url bar.",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "description": "Title of the navigation history entry.",
+ "type": "string"
+ },
+ {
+ "name": "transitionType",
+ "description": "Transition type.",
+ "$ref": "TransitionType"
+ }
+ ]
+ },
+ {
+ "id": "ScreencastFrameMetadata",
+ "description": "Screencast frame metadata.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "offsetTop",
+ "description": "Top offset in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "pageScaleFactor",
+ "description": "Page scale factor.",
+ "type": "number"
+ },
+ {
+ "name": "deviceWidth",
+ "description": "Device screen width in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "deviceHeight",
+ "description": "Device screen height in DIP.",
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetX",
+ "description": "Position of horizontal scroll in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "scrollOffsetY",
+ "description": "Position of vertical scroll in CSS pixels.",
+ "type": "number"
+ },
+ {
+ "name": "timestamp",
+ "description": "Frame swap timestamp.",
+ "optional": true,
+ "$ref": "Network.TimeSinceEpoch"
+ }
+ ]
+ },
+ {
+ "id": "DialogType",
+ "description": "Javascript dialog type.",
+ "type": "string",
+ "enum": [
+ "alert",
+ "confirm",
+ "prompt",
+ "beforeunload"
+ ]
+ },
+ {
+ "id": "AppManifestError",
+ "description": "Error while paring app manifest.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "message",
+ "description": "Error message.",
+ "type": "string"
+ },
+ {
+ "name": "critical",
+ "description": "If criticial, this is a non-recoverable parse error.",
+ "type": "integer"
+ },
+ {
+ "name": "line",
+ "description": "Error line.",
+ "type": "integer"
+ },
+ {
+ "name": "column",
+ "description": "Error column.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "AppManifestParsedProperties",
+ "description": "Parsed app manifest properties.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scope",
+ "description": "Computed scope value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "LayoutViewport",
+ "description": "Layout viewport position and dimensions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "pageX",
+ "description": "Horizontal offset relative to the document (CSS pixels).",
+ "type": "integer"
+ },
+ {
+ "name": "pageY",
+ "description": "Vertical offset relative to the document (CSS pixels).",
+ "type": "integer"
+ },
+ {
+ "name": "clientWidth",
+ "description": "Width (CSS pixels), excludes scrollbar if present.",
+ "type": "integer"
+ },
+ {
+ "name": "clientHeight",
+ "description": "Height (CSS pixels), excludes scrollbar if present.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "VisualViewport",
+ "description": "Visual viewport position, dimensions, and scale.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "offsetX",
+ "description": "Horizontal offset relative to the layout viewport (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "offsetY",
+ "description": "Vertical offset relative to the layout viewport (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "pageX",
+ "description": "Horizontal offset relative to the document (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "pageY",
+ "description": "Vertical offset relative to the document (CSS pixels).",
+ "type": "number"
+ },
+ {
+ "name": "clientWidth",
+ "description": "Width (CSS pixels), excludes scrollbar if present.",
+ "type": "number"
+ },
+ {
+ "name": "clientHeight",
+ "description": "Height (CSS pixels), excludes scrollbar if present.",
+ "type": "number"
+ },
+ {
+ "name": "scale",
+ "description": "Scale relative to the ideal viewport (size at width=device-width).",
+ "type": "number"
+ },
+ {
+ "name": "zoom",
+ "description": "Page zoom factor (CSS to device independent pixels ratio).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "Viewport",
+ "description": "Viewport for capturing screenshot.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "x",
+ "description": "X offset in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "y",
+ "description": "Y offset in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "width",
+ "description": "Rectangle width in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "height",
+ "description": "Rectangle height in device independent pixels (dip).",
+ "type": "number"
+ },
+ {
+ "name": "scale",
+ "description": "Page scale factor.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "FontFamilies",
+ "description": "Generic font families collection.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "standard",
+ "description": "The standard font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "fixed",
+ "description": "The fixed font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "serif",
+ "description": "The serif font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "sansSerif",
+ "description": "The sansSerif font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cursive",
+ "description": "The cursive font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "fantasy",
+ "description": "The fantasy font-family.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "math",
+ "description": "The math font-family.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "ScriptFontFamilies",
+ "description": "Font families collection for a script.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "script",
+ "description": "Name of the script which these font families are defined for.",
+ "type": "string"
+ },
+ {
+ "name": "fontFamilies",
+ "description": "Generic font families collection for the script.",
+ "$ref": "FontFamilies"
+ }
+ ]
+ },
+ {
+ "id": "FontSizes",
+ "description": "Default font sizes.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "standard",
+ "description": "Default standard font size.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "fixed",
+ "description": "Default fixed font size.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ClientNavigationReason",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "formSubmissionGet",
+ "formSubmissionPost",
+ "httpHeaderRefresh",
+ "scriptInitiated",
+ "metaTagRefresh",
+ "pageBlockInterstitial",
+ "reload",
+ "anchorClick"
+ ]
+ },
+ {
+ "id": "ClientNavigationDisposition",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "currentTab",
+ "newTab",
+ "newWindow",
+ "download"
+ ]
+ },
+ {
+ "id": "InstallabilityErrorArgument",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Argument name (e.g. name:'minimum-icon-size-in-pixels').",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Argument value (e.g. value:'64').",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InstallabilityError",
+ "description": "The installability error",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorId",
+ "description": "The error id (e.g. 'manifest-missing-suitable-icon').",
+ "type": "string"
+ },
+ {
+ "name": "errorArguments",
+ "description": "The list of error arguments (e.g. {name:'minimum-icon-size-in-pixels', value:'64'}).",
+ "type": "array",
+ "items": {
+ "$ref": "InstallabilityErrorArgument"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ReferrerPolicy",
+ "description": "The referring-policy used for the navigation.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "noReferrer",
+ "noReferrerWhenDowngrade",
+ "origin",
+ "originWhenCrossOrigin",
+ "sameOrigin",
+ "strictOrigin",
+ "strictOriginWhenCrossOrigin",
+ "unsafeUrl"
+ ]
+ },
+ {
+ "id": "CompilationCacheParams",
+ "description": "Per-script compilation cache parameters for `Page.produceCompilationCache`",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "The URL of the script to produce a compilation cache entry for.",
+ "type": "string"
+ },
+ {
+ "name": "eager",
+ "description": "A hint to the backend whether eager compilation is recommended.\n(the actual compilation mode used is upon backend discretion).",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "AutoResponseMode",
+ "description": "Enum of possible auto-reponse for permisison / prompt dialogs.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "none",
+ "autoAccept",
+ "autoReject",
+ "autoOptOut"
+ ]
+ },
+ {
+ "id": "NavigationType",
+ "description": "The type of a frameNavigated event.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Navigation",
+ "BackForwardCacheRestore"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredReason",
+ "description": "List of not restored reasons for back-forward cache.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "NotPrimaryMainFrame",
+ "BackForwardCacheDisabled",
+ "RelatedActiveContentsExist",
+ "HTTPStatusNotOK",
+ "SchemeNotHTTPOrHTTPS",
+ "Loading",
+ "WasGrantedMediaAccess",
+ "DisableForRenderFrameHostCalled",
+ "DomainNotAllowed",
+ "HTTPMethodNotGET",
+ "SubframeIsNavigating",
+ "Timeout",
+ "CacheLimit",
+ "JavaScriptExecution",
+ "RendererProcessKilled",
+ "RendererProcessCrashed",
+ "SchedulerTrackedFeatureUsed",
+ "ConflictingBrowsingInstance",
+ "CacheFlushed",
+ "ServiceWorkerVersionActivation",
+ "SessionRestored",
+ "ServiceWorkerPostMessage",
+ "EnteredBackForwardCacheBeforeServiceWorkerHostAdded",
+ "RenderFrameHostReused_SameSite",
+ "RenderFrameHostReused_CrossSite",
+ "ServiceWorkerClaim",
+ "IgnoreEventAndEvict",
+ "HaveInnerContents",
+ "TimeoutPuttingInCache",
+ "BackForwardCacheDisabledByLowMemory",
+ "BackForwardCacheDisabledByCommandLine",
+ "NetworkRequestDatapipeDrainedAsBytesConsumer",
+ "NetworkRequestRedirected",
+ "NetworkRequestTimeout",
+ "NetworkExceedsBufferLimit",
+ "NavigationCancelledWhileRestoring",
+ "NotMostRecentNavigationEntry",
+ "BackForwardCacheDisabledForPrerender",
+ "UserAgentOverrideDiffers",
+ "ForegroundCacheLimit",
+ "BrowsingInstanceNotSwapped",
+ "BackForwardCacheDisabledForDelegate",
+ "UnloadHandlerExistsInMainFrame",
+ "UnloadHandlerExistsInSubFrame",
+ "ServiceWorkerUnregistration",
+ "CacheControlNoStore",
+ "CacheControlNoStoreCookieModified",
+ "CacheControlNoStoreHTTPOnlyCookieModified",
+ "NoResponseHead",
+ "Unknown",
+ "ActivationNavigationsDisallowedForBug1234857",
+ "ErrorDocument",
+ "FencedFramesEmbedder",
+ "CookieDisabled",
+ "HTTPAuthRequired",
+ "CookieFlushed",
+ "WebSocket",
+ "WebTransport",
+ "WebRTC",
+ "MainResourceHasCacheControlNoStore",
+ "MainResourceHasCacheControlNoCache",
+ "SubresourceHasCacheControlNoStore",
+ "SubresourceHasCacheControlNoCache",
+ "ContainsPlugins",
+ "DocumentLoaded",
+ "DedicatedWorkerOrWorklet",
+ "OutstandingNetworkRequestOthers",
+ "RequestedMIDIPermission",
+ "RequestedAudioCapturePermission",
+ "RequestedVideoCapturePermission",
+ "RequestedBackForwardCacheBlockedSensors",
+ "RequestedBackgroundWorkPermission",
+ "BroadcastChannel",
+ "WebXR",
+ "SharedWorker",
+ "WebLocks",
+ "WebHID",
+ "WebShare",
+ "RequestedStorageAccessGrant",
+ "WebNfc",
+ "OutstandingNetworkRequestFetch",
+ "OutstandingNetworkRequestXHR",
+ "AppBanner",
+ "Printing",
+ "WebDatabase",
+ "PictureInPicture",
+ "Portal",
+ "SpeechRecognizer",
+ "IdleManager",
+ "PaymentManager",
+ "SpeechSynthesis",
+ "KeyboardLock",
+ "WebOTPService",
+ "OutstandingNetworkRequestDirectSocket",
+ "InjectedJavascript",
+ "InjectedStyleSheet",
+ "KeepaliveRequest",
+ "IndexedDBEvent",
+ "Dummy",
+ "JsNetworkRequestReceivedCacheControlNoStoreResource",
+ "WebRTCSticky",
+ "WebTransportSticky",
+ "WebSocketSticky",
+ "ContentSecurityHandler",
+ "ContentWebAuthenticationAPI",
+ "ContentFileChooser",
+ "ContentSerial",
+ "ContentFileSystemAccess",
+ "ContentMediaDevicesDispatcherHost",
+ "ContentWebBluetooth",
+ "ContentWebUSB",
+ "ContentMediaSessionService",
+ "ContentScreenReader",
+ "EmbedderPopupBlockerTabHelper",
+ "EmbedderSafeBrowsingTriggeredPopupBlocker",
+ "EmbedderSafeBrowsingThreatDetails",
+ "EmbedderAppBannerManager",
+ "EmbedderDomDistillerViewerSource",
+ "EmbedderDomDistillerSelfDeletingRequestDelegate",
+ "EmbedderOomInterventionTabHelper",
+ "EmbedderOfflinePage",
+ "EmbedderChromePasswordManagerClientBindCredentialManager",
+ "EmbedderPermissionRequestManager",
+ "EmbedderModalDialog",
+ "EmbedderExtensions",
+ "EmbedderExtensionMessaging",
+ "EmbedderExtensionMessagingForOpenPort",
+ "EmbedderExtensionSentMessageToCachedFrame"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredReasonType",
+ "description": "Types of not restored reasons for back-forward cache.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "SupportPending",
+ "PageSupportNeeded",
+ "Circumstantial"
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredExplanation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of the reason",
+ "$ref": "BackForwardCacheNotRestoredReasonType"
+ },
+ {
+ "name": "reason",
+ "description": "Not restored reason",
+ "$ref": "BackForwardCacheNotRestoredReason"
+ },
+ {
+ "name": "context",
+ "description": "Context associated with the reason. The meaning of this context is\ndependent on the reason:\n- EmbedderExtensionSentMessageToCachedFrame: the extension ID.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BackForwardCacheNotRestoredExplanationTree",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "URL of each frame",
+ "type": "string"
+ },
+ {
+ "name": "explanations",
+ "description": "Not restored reasons of each frame",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanation"
+ }
+ },
+ {
+ "name": "children",
+ "description": "Array of children frame",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanationTree"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addScriptToEvaluateOnLoad",
+ "description": "Deprecated, please use addScriptToEvaluateOnNewDocument instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "scriptSource",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "identifier",
+ "description": "Identifier of the added script.",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "addScriptToEvaluateOnNewDocument",
+ "description": "Evaluates given script in every frame upon creation (before loading frame's scripts).",
+ "parameters": [
+ {
+ "name": "source",
+ "type": "string"
+ },
+ {
+ "name": "worldName",
+ "description": "If specified, creates an isolated world with the given name and evaluates given script in it.\nThis world name will be used as the ExecutionContextDescription::name when the corresponding\nevent is emitted.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Specifies whether command line API should be available to the script, defaults\nto false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "runImmediately",
+ "description": "If true, runs the script immediately on existing execution contexts or worlds.\nDefault: false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "identifier",
+ "description": "Identifier of the added script.",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "bringToFront",
+ "description": "Brings page to front (activates tab)."
+ },
+ {
+ "name": "captureScreenshot",
+ "description": "Capture page screenshot.",
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Image compression format (defaults to png).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png",
+ "webp"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100] (jpeg only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "clip",
+ "description": "Capture the screenshot of a given region only.",
+ "optional": true,
+ "$ref": "Viewport"
+ },
+ {
+ "name": "fromSurface",
+ "description": "Capture the screenshot from the surface, rather than the view. Defaults to true.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureBeyondViewport",
+ "description": "Capture the screenshot beyond the viewport. Defaults to false.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "optimizeForSpeed",
+ "description": "Optimize image encoding for speed, not for resulting size (defaults to false)",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Base64-encoded image data. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "captureSnapshot",
+ "description": "Returns a snapshot of the page as a string. For MHTML format, the serialization includes\niframes, shadow DOM, external resources, and element-inline styles.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Format (defaults to mhtml).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mhtml"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Serialized page data.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearDeviceMetricsOverride",
+ "description": "Clears the overridden device metrics.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation"
+ },
+ {
+ "name": "clearDeviceOrientationOverride",
+ "description": "Clears the overridden Device Orientation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "DeviceOrientation"
+ },
+ {
+ "name": "clearGeolocationOverride",
+ "description": "Clears the overridden Geolocation Position and Error.",
+ "deprecated": true,
+ "redirect": "Emulation"
+ },
+ {
+ "name": "createIsolatedWorld",
+ "description": "Creates an isolated world for the given frame.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame in which the isolated world should be created.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "worldName",
+ "description": "An optional name which is reported in the Execution Context.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "grantUniveralAccess",
+ "description": "Whether or not universal access should be granted to the isolated world. This is a powerful\noption, use with caution.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "executionContextId",
+ "description": "Execution context of the isolated world.",
+ "$ref": "Runtime.ExecutionContextId"
+ }
+ ]
+ },
+ {
+ "name": "deleteCookie",
+ "description": "Deletes browser cookie with given name, domain and path.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Network",
+ "parameters": [
+ {
+ "name": "cookieName",
+ "description": "Name of the cookie to remove.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL to match cooke domain and path.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables page domain notifications."
+ },
+ {
+ "name": "enable",
+ "description": "Enables page domain notifications."
+ },
+ {
+ "name": "getAppManifest",
+ "returns": [
+ {
+ "name": "url",
+ "description": "Manifest location.",
+ "type": "string"
+ },
+ {
+ "name": "errors",
+ "type": "array",
+ "items": {
+ "$ref": "AppManifestError"
+ }
+ },
+ {
+ "name": "data",
+ "description": "Manifest content.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "parsed",
+ "description": "Parsed manifest properties",
+ "experimental": true,
+ "optional": true,
+ "$ref": "AppManifestParsedProperties"
+ }
+ ]
+ },
+ {
+ "name": "getInstallabilityErrors",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "installabilityErrors",
+ "type": "array",
+ "items": {
+ "$ref": "InstallabilityError"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getManifestIcons",
+ "description": "Deprecated because it's not guaranteed that the returned icon is in fact the one used for PWA installation.",
+ "experimental": true,
+ "deprecated": true,
+ "returns": [
+ {
+ "name": "primaryIcon",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getAppId",
+ "description": "Returns the unique (PWA) app id.\nOnly returns values if the feature flag 'WebAppEnableManifestId' is enabled",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "appId",
+ "description": "App id, either from manifest's id attribute or computed from start_url",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "recommendedId",
+ "description": "Recommendation for manifest's id attribute to match current id computed from start_url",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getAdScriptId",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "adScriptId",
+ "description": "Identifies the bottom-most script which caused the frame to be labelled\nas an ad. Only sent if frame is labelled as an ad and id is available.",
+ "optional": true,
+ "$ref": "AdScriptId"
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies for the page and all of its subframes. Depending\non the backend support, will return detailed cookie information in the\n`cookies` field.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Network",
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getFrameTree",
+ "description": "Returns present frame tree structure.",
+ "returns": [
+ {
+ "name": "frameTree",
+ "description": "Present frame tree structure.",
+ "$ref": "FrameTree"
+ }
+ ]
+ },
+ {
+ "name": "getLayoutMetrics",
+ "description": "Returns metrics relating to the layouting of the page, such as viewport bounds/scale.",
+ "returns": [
+ {
+ "name": "layoutViewport",
+ "description": "Deprecated metrics relating to the layout viewport. Is in device pixels. Use `cssLayoutViewport` instead.",
+ "deprecated": true,
+ "$ref": "LayoutViewport"
+ },
+ {
+ "name": "visualViewport",
+ "description": "Deprecated metrics relating to the visual viewport. Is in device pixels. Use `cssVisualViewport` instead.",
+ "deprecated": true,
+ "$ref": "VisualViewport"
+ },
+ {
+ "name": "contentSize",
+ "description": "Deprecated size of scrollable area. Is in DP. Use `cssContentSize` instead.",
+ "deprecated": true,
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "cssLayoutViewport",
+ "description": "Metrics relating to the layout viewport in CSS pixels.",
+ "$ref": "LayoutViewport"
+ },
+ {
+ "name": "cssVisualViewport",
+ "description": "Metrics relating to the visual viewport in CSS pixels.",
+ "$ref": "VisualViewport"
+ },
+ {
+ "name": "cssContentSize",
+ "description": "Size of scrollable area in CSS pixels.",
+ "$ref": "DOM.Rect"
+ }
+ ]
+ },
+ {
+ "name": "getNavigationHistory",
+ "description": "Returns navigation history for the current page.",
+ "returns": [
+ {
+ "name": "currentIndex",
+ "description": "Index of the current navigation history entry.",
+ "type": "integer"
+ },
+ {
+ "name": "entries",
+ "description": "Array of navigation history entries.",
+ "type": "array",
+ "items": {
+ "$ref": "NavigationEntry"
+ }
+ }
+ ]
+ },
+ {
+ "name": "resetNavigationHistory",
+ "description": "Resets navigation history for the current page."
+ },
+ {
+ "name": "getResourceContent",
+ "description": "Returns content of the given resource.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to get resource for.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to get content for.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "content",
+ "description": "Resource content.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was served as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getResourceTree",
+ "description": "Returns present frame / resource tree structure.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "frameTree",
+ "description": "Present frame / resource tree structure.",
+ "$ref": "FrameResourceTree"
+ }
+ ]
+ },
+ {
+ "name": "handleJavaScriptDialog",
+ "description": "Accepts or dismisses a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload).",
+ "parameters": [
+ {
+ "name": "accept",
+ "description": "Whether to accept or dismiss the dialog.",
+ "type": "boolean"
+ },
+ {
+ "name": "promptText",
+ "description": "The text to enter into the dialog prompt before accepting. Used only if this is a prompt\ndialog.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "navigate",
+ "description": "Navigates current page to the given URL.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "URL to navigate the page to.",
+ "type": "string"
+ },
+ {
+ "name": "referrer",
+ "description": "Referrer URL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "transitionType",
+ "description": "Intended transition type.",
+ "optional": true,
+ "$ref": "TransitionType"
+ },
+ {
+ "name": "frameId",
+ "description": "Frame id to navigate, if not specified navigates the top frame.",
+ "optional": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "referrerPolicy",
+ "description": "Referrer-policy used for the navigation.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ReferrerPolicy"
+ }
+ ],
+ "returns": [
+ {
+ "name": "frameId",
+ "description": "Frame id that has navigated (or failed to navigate)",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. This is omitted in case of same-document navigation,\nas the previously committed loaderId would not change.",
+ "optional": true,
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "errorText",
+ "description": "User friendly error message, present if and only if navigation has failed.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "navigateToHistoryEntry",
+ "description": "Navigates current page to the given history entry.",
+ "parameters": [
+ {
+ "name": "entryId",
+ "description": "Unique id of the entry to navigate to.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "printToPDF",
+ "description": "Print page as PDF.",
+ "parameters": [
+ {
+ "name": "landscape",
+ "description": "Paper orientation. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "displayHeaderFooter",
+ "description": "Display header and footer. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "printBackground",
+ "description": "Print background graphics. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale of the webpage rendering. Defaults to 1.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paperWidth",
+ "description": "Paper width in inches. Defaults to 8.5 inches.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "paperHeight",
+ "description": "Paper height in inches. Defaults to 11 inches.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginTop",
+ "description": "Top margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginBottom",
+ "description": "Bottom margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginLeft",
+ "description": "Left margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "marginRight",
+ "description": "Right margin in inches. Defaults to 1cm (~0.4 inches).",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "pageRanges",
+ "description": "Paper ranges to print, one based, e.g., '1-5, 8, 11-13'. Pages are\nprinted in the document order, not in the order specified, and no\nmore than once.\nDefaults to empty string, which implies the entire document is printed.\nThe page numbers are quietly capped to actual page count of the\ndocument, and ranges beyond the end of the document are ignored.\nIf this results in no pages to print, an error is reported.\nIt is an error to specify a range with start greater than end.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headerTemplate",
+ "description": "HTML template for the print header. Should be valid HTML markup with following\nclasses used to inject printing values into them:\n- `date`: formatted print date\n- `title`: document title\n- `url`: document location\n- `pageNumber`: current page number\n- `totalPages`: total pages in the document\n\nFor example, `<span class=title></span>` would generate span containing the title.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "footerTemplate",
+ "description": "HTML template for the print footer. Should use the same format as the `headerTemplate`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "preferCSSPageSize",
+ "description": "Whether or not to prefer page size as defined by css. Defaults to false,\nin which case the content will be scaled to fit the paper size.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "transferMode",
+ "description": "return as stream",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "ReturnAsBase64",
+ "ReturnAsStream"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "data",
+ "description": "Base64-encoded pdf data. Empty if |returnAsStream| is specified. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "stream",
+ "description": "A handle of the stream that holds resulting PDF data.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ },
+ {
+ "name": "reload",
+ "description": "Reloads given page optionally ignoring the cache.",
+ "parameters": [
+ {
+ "name": "ignoreCache",
+ "description": "If true, browser cache is ignored (as if the user pressed Shift+refresh).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "scriptToEvaluateOnLoad",
+ "description": "If set, the script will be injected into all frames of the inspected page after reload.\nArgument will be ignored if reloading dataURL origin.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeScriptToEvaluateOnLoad",
+ "description": "Deprecated, please use removeScriptToEvaluateOnNewDocument instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "identifier",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "removeScriptToEvaluateOnNewDocument",
+ "description": "Removes given script from the list.",
+ "parameters": [
+ {
+ "name": "identifier",
+ "$ref": "ScriptIdentifier"
+ }
+ ]
+ },
+ {
+ "name": "screencastFrameAck",
+ "description": "Acknowledges that a screencast frame has been received by the frontend.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Frame number.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "searchInResource",
+ "description": "Searches for given string in resource content.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id for resource to search in.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource to search in.",
+ "type": "string"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "Debugger.SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAdBlockingEnabled",
+ "description": "Enable Chrome's experimental ad filter on all sites.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to block ads.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setBypassCSP",
+ "description": "Enable page Content Security Policy by-passing.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether to bypass page CSP.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getPermissionsPolicyState",
+ "description": "Get Permissions Policy state on given frame.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "states",
+ "type": "array",
+ "items": {
+ "$ref": "PermissionsPolicyFeatureState"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getOriginTrials",
+ "description": "Get Origin Trials on given frame.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "originTrials",
+ "type": "array",
+ "items": {
+ "$ref": "OriginTrial"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setDeviceMetricsOverride",
+ "description": "Overrides the values of device screen dimensions (window.screen.width, window.screen.height,\nwindow.innerWidth, window.innerHeight, and \"device-width\"/\"device-height\"-related CSS media\nquery results).",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "width",
+ "description": "Overriding width value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Overriding height value in pixels (minimum 0, maximum 10000000). 0 disables the override.",
+ "type": "integer"
+ },
+ {
+ "name": "deviceScaleFactor",
+ "description": "Overriding device scale factor value. 0 disables the override.",
+ "type": "number"
+ },
+ {
+ "name": "mobile",
+ "description": "Whether to emulate mobile device. This includes viewport meta tag, overlay scrollbars, text\nautosizing and more.",
+ "type": "boolean"
+ },
+ {
+ "name": "scale",
+ "description": "Scale to apply to resulting view image.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "screenWidth",
+ "description": "Overriding screen width value in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "screenHeight",
+ "description": "Overriding screen height value in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionX",
+ "description": "Overriding view X position on screen in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "positionY",
+ "description": "Overriding view Y position on screen in pixels (minimum 0, maximum 10000000).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "dontSetVisibleSize",
+ "description": "Do not set visible view size, rely upon explicit setVisibleSize call.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "screenOrientation",
+ "description": "Screen orientation override.",
+ "optional": true,
+ "$ref": "Emulation.ScreenOrientation"
+ },
+ {
+ "name": "viewport",
+ "description": "The viewport dimensions and scale. If not set, the override is cleared.",
+ "optional": true,
+ "$ref": "Viewport"
+ }
+ ]
+ },
+ {
+ "name": "setDeviceOrientationOverride",
+ "description": "Overrides the Device Orientation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "DeviceOrientation",
+ "parameters": [
+ {
+ "name": "alpha",
+ "description": "Mock alpha",
+ "type": "number"
+ },
+ {
+ "name": "beta",
+ "description": "Mock beta",
+ "type": "number"
+ },
+ {
+ "name": "gamma",
+ "description": "Mock gamma",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setFontFamilies",
+ "description": "Set generic font families.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "fontFamilies",
+ "description": "Specifies font families to set. If a font family is not specified, it won't be changed.",
+ "$ref": "FontFamilies"
+ },
+ {
+ "name": "forScripts",
+ "description": "Specifies font families to set for individual scripts.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "ScriptFontFamilies"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setFontSizes",
+ "description": "Set default font sizes.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "fontSizes",
+ "description": "Specifies font sizes to set. If a font size is not specified, it won't be changed.",
+ "$ref": "FontSizes"
+ }
+ ]
+ },
+ {
+ "name": "setDocumentContent",
+ "description": "Sets given markup as the document's HTML.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Frame id to set HTML for.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "html",
+ "description": "HTML content to set.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setDownloadBehavior",
+ "description": "Set the behavior when downloading a file.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "behavior",
+ "description": "Whether to allow all or deny all download requests, or use default Chrome behavior if\navailable (otherwise deny).",
+ "type": "string",
+ "enum": [
+ "deny",
+ "allow",
+ "default"
+ ]
+ },
+ {
+ "name": "downloadPath",
+ "description": "The default path to save downloaded files to. This is required if behavior is set to 'allow'",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setGeolocationOverride",
+ "description": "Overrides the Geolocation Position or Error. Omitting any of the parameters emulates position\nunavailable.",
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "latitude",
+ "description": "Mock latitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "longitude",
+ "description": "Mock longitude",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "accuracy",
+ "description": "Mock accuracy",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "setLifecycleEventsEnabled",
+ "description": "Controls whether page will emit lifecycle events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "If true, starts emitting lifecycle events.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setTouchEmulationEnabled",
+ "description": "Toggles mouse event-based touch event emulation.",
+ "experimental": true,
+ "deprecated": true,
+ "redirect": "Emulation",
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "Whether the touch event emulation should be enabled.",
+ "type": "boolean"
+ },
+ {
+ "name": "configuration",
+ "description": "Touch/gesture events configuration. Default: current platform.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "mobile",
+ "desktop"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "startScreencast",
+ "description": "Starts sending each frame using the `screencastFrame` event.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "format",
+ "description": "Image compression format.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "png"
+ ]
+ },
+ {
+ "name": "quality",
+ "description": "Compression quality from range [0..100].",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxWidth",
+ "description": "Maximum screenshot width.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "maxHeight",
+ "description": "Maximum screenshot height.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "everyNthFrame",
+ "description": "Send every n-th frame.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "stopLoading",
+ "description": "Force the page stop all navigations and pending resource fetches."
+ },
+ {
+ "name": "crash",
+ "description": "Crashes renderer on the IO thread, generates minidumps.",
+ "experimental": true
+ },
+ {
+ "name": "close",
+ "description": "Tries to close page, running its beforeunload hooks, if any.",
+ "experimental": true
+ },
+ {
+ "name": "setWebLifecycleState",
+ "description": "Tries to update the web lifecycle state of the page.\nIt will transition the page to the given state according to:\nhttps://github.com/WICG/web-lifecycle/",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "state",
+ "description": "Target lifecycle state",
+ "type": "string",
+ "enum": [
+ "frozen",
+ "active"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "stopScreencast",
+ "description": "Stops sending each frame in the `screencastFrame`.",
+ "experimental": true
+ },
+ {
+ "name": "produceCompilationCache",
+ "description": "Requests backend to produce compilation cache for the specified scripts.\n`scripts` are appeneded to the list of scripts for which the cache\nwould be produced. The list may be reset during page navigation.\nWhen script with a matching URL is encountered, the cache is optionally\nproduced upon backend discretion, based on internal heuristics.\nSee also: `Page.compilationCacheProduced`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scripts",
+ "type": "array",
+ "items": {
+ "$ref": "CompilationCacheParams"
+ }
+ }
+ ]
+ },
+ {
+ "name": "addCompilationCache",
+ "description": "Seeds compilation cache for given url. Compilation cache does not survive\ncross-process navigation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Base64-encoded data (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearCompilationCache",
+ "description": "Clears seeded compilation cache.",
+ "experimental": true
+ },
+ {
+ "name": "setSPCTransactionMode",
+ "description": "Sets the Secure Payment Confirmation transaction mode.\nhttps://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "mode",
+ "$ref": "AutoResponseMode"
+ }
+ ]
+ },
+ {
+ "name": "setRPHRegistrationMode",
+ "description": "Extensions for Custom Handlers API:\nhttps://html.spec.whatwg.org/multipage/system-state.html#rph-automation",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "mode",
+ "$ref": "AutoResponseMode"
+ }
+ ]
+ },
+ {
+ "name": "generateTestReport",
+ "description": "Generates a report for testing.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "message",
+ "description": "Message to be displayed in the report.",
+ "type": "string"
+ },
+ {
+ "name": "group",
+ "description": "Specifies the endpoint group to deliver the report to.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "waitForDebugger",
+ "description": "Pauses page execution. Can be resumed using generic Runtime.runIfWaitingForDebugger.",
+ "experimental": true
+ },
+ {
+ "name": "setInterceptFileChooserDialog",
+ "description": "Intercept file chooser requests and transfer control to protocol clients.\nWhen file chooser interception is enabled, native file chooser dialog is not shown.\nInstead, a protocol event `Page.fileChooserOpened` is emitted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPrerenderingAllowed",
+ "description": "Enable/disable prerendering manually.\n\nThis command is a short-term solution for https://crbug.com/1440085.\nSee https://docs.google.com/document/d/12HVmFxYj5Jc-eJr5OmWsa2bqTJsbgGLKI6ZIyx0_wpA\nfor more details.\n\nTODO(https://crbug.com/1440085): Remove this once Puppeteer supports tab targets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "isAllowed",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "domContentEventFired",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "fileChooserOpened",
+ "description": "Emitted only when `page.interceptFileChooser` is enabled.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame containing input node.",
+ "experimental": true,
+ "$ref": "FrameId"
+ },
+ {
+ "name": "mode",
+ "description": "Input mode.",
+ "type": "string",
+ "enum": [
+ "selectSingle",
+ "selectMultiple"
+ ]
+ },
+ {
+ "name": "backendNodeId",
+ "description": "Input node id. Only present for file choosers opened via an `<input type=\"file\">` element.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "name": "frameAttached",
+ "description": "Fired when frame has been attached to its parent.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has been attached.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "parentFrameId",
+ "description": "Parent frame identifier.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "stack",
+ "description": "JavaScript stack trace of when frame was attached, only set if frame initiated from script.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "frameClearedScheduledNavigation",
+ "description": "Fired when frame no longer has a scheduled navigation.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has cleared its scheduled navigation.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "frameDetached",
+ "description": "Fired when frame has been detached from its parent.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has been detached.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "reason",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "remove",
+ "swap"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "frameNavigated",
+ "description": "Fired once navigation of the frame has completed. Frame is now associated with the new loader.",
+ "parameters": [
+ {
+ "name": "frame",
+ "description": "Frame object.",
+ "$ref": "Frame"
+ },
+ {
+ "name": "type",
+ "experimental": true,
+ "$ref": "NavigationType"
+ }
+ ]
+ },
+ {
+ "name": "documentOpened",
+ "description": "Fired when opening document to write to.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frame",
+ "description": "Frame object.",
+ "$ref": "Frame"
+ }
+ ]
+ },
+ {
+ "name": "frameResized",
+ "experimental": true
+ },
+ {
+ "name": "frameRequestedNavigation",
+ "description": "Fired when a renderer-initiated navigation is requested.\nNavigation may still be cancelled after the event is issued.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that is being navigated.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "reason",
+ "description": "The reason for the navigation.",
+ "$ref": "ClientNavigationReason"
+ },
+ {
+ "name": "url",
+ "description": "The destination URL for the requested navigation.",
+ "type": "string"
+ },
+ {
+ "name": "disposition",
+ "description": "The disposition for the navigation.",
+ "$ref": "ClientNavigationDisposition"
+ }
+ ]
+ },
+ {
+ "name": "frameScheduledNavigation",
+ "description": "Fired when frame schedules a potential navigation.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has scheduled a navigation.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "delay",
+ "description": "Delay (in seconds) until the navigation is scheduled to begin. The navigation is not\nguaranteed to start.",
+ "type": "number"
+ },
+ {
+ "name": "reason",
+ "description": "The reason for the navigation.",
+ "$ref": "ClientNavigationReason"
+ },
+ {
+ "name": "url",
+ "description": "The destination URL for the scheduled navigation.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "frameStartedLoading",
+ "description": "Fired when frame has started loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has started loading.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "frameStoppedLoading",
+ "description": "Fired when frame has stopped loading.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that has stopped loading.",
+ "$ref": "FrameId"
+ }
+ ]
+ },
+ {
+ "name": "downloadWillBegin",
+ "description": "Fired when page is about to start a download.\nDeprecated. Use Browser.downloadWillBegin instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame that caused download to begin.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resource being downloaded.",
+ "type": "string"
+ },
+ {
+ "name": "suggestedFilename",
+ "description": "Suggested file name of the resource (the actual name of the file saved on disk may differ).",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "downloadProgress",
+ "description": "Fired when download makes progress. Last call has |done| == true.\nDeprecated. Use Browser.downloadProgress instead.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "guid",
+ "description": "Global unique identifier of the download.",
+ "type": "string"
+ },
+ {
+ "name": "totalBytes",
+ "description": "Total expected bytes to download.",
+ "type": "number"
+ },
+ {
+ "name": "receivedBytes",
+ "description": "Total bytes received.",
+ "type": "number"
+ },
+ {
+ "name": "state",
+ "description": "Download status.",
+ "type": "string",
+ "enum": [
+ "inProgress",
+ "completed",
+ "canceled"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "interstitialHidden",
+ "description": "Fired when interstitial page was hidden"
+ },
+ {
+ "name": "interstitialShown",
+ "description": "Fired when interstitial page was shown"
+ },
+ {
+ "name": "javascriptDialogClosed",
+ "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) has been\nclosed.",
+ "parameters": [
+ {
+ "name": "result",
+ "description": "Whether dialog was confirmed.",
+ "type": "boolean"
+ },
+ {
+ "name": "userInput",
+ "description": "User input in case of prompt.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "javascriptDialogOpening",
+ "description": "Fired when a JavaScript initiated dialog (alert, confirm, prompt, or onbeforeunload) is about to\nopen.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "Frame url.",
+ "type": "string"
+ },
+ {
+ "name": "message",
+ "description": "Message that will be displayed by the dialog.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Dialog type.",
+ "$ref": "DialogType"
+ },
+ {
+ "name": "hasBrowserHandler",
+ "description": "True iff browser is capable showing or acting on the given dialog. When browser has no\ndialog handler for given target, calling alert while Page domain is engaged will stall\nthe page execution. Execution can be resumed via calling Page.handleJavaScriptDialog.",
+ "type": "boolean"
+ },
+ {
+ "name": "defaultPrompt",
+ "description": "Default dialog prompt.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "lifecycleEvent",
+ "description": "Fired for top level page lifecycle events such as navigation, load, paint, etc.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Loader identifier. Empty string if the request is fetched from worker.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "backForwardCacheNotUsed",
+ "description": "Fired for failed bfcache history navigations if BackForwardCache feature is enabled. Do\nnot assume any ordering with the Page.frameNavigated event. This event is fired only for\nmain-frame history navigation where the document changes (non-same-document navigations),\nwhen bfcache navigation fails.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "loaderId",
+ "description": "The loader id for the associated navgation.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "frameId",
+ "description": "The frame id of the associated frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "notRestoredExplanations",
+ "description": "Array of reasons why the page could not be cached. This must not be empty.",
+ "type": "array",
+ "items": {
+ "$ref": "BackForwardCacheNotRestoredExplanation"
+ }
+ },
+ {
+ "name": "notRestoredExplanationsTree",
+ "description": "Tree structure of reasons why the page could not be cached for each frame.",
+ "optional": true,
+ "$ref": "BackForwardCacheNotRestoredExplanationTree"
+ }
+ ]
+ },
+ {
+ "name": "loadEventFired",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "$ref": "Network.MonotonicTime"
+ }
+ ]
+ },
+ {
+ "name": "navigatedWithinDocument",
+ "description": "Fired when same-document navigation happens, e.g. due to history API usage or anchor navigation.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "frameId",
+ "description": "Id of the frame.",
+ "$ref": "FrameId"
+ },
+ {
+ "name": "url",
+ "description": "Frame's new url.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "screencastFrame",
+ "description": "Compressed image data requested by the `startScreencast`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "data",
+ "description": "Base64-encoded compressed image. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "metadata",
+ "description": "Screencast frame metadata.",
+ "$ref": "ScreencastFrameMetadata"
+ },
+ {
+ "name": "sessionId",
+ "description": "Frame number.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "screencastVisibilityChanged",
+ "description": "Fired when the page with currently enabled screencast was shown or hidden `.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "visible",
+ "description": "True if the page is visible.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "windowOpen",
+ "description": "Fired when a new window is going to be opened, via window.open(), link click, form submission,\netc.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "The URL for the new window.",
+ "type": "string"
+ },
+ {
+ "name": "windowName",
+ "description": "Window name.",
+ "type": "string"
+ },
+ {
+ "name": "windowFeatures",
+ "description": "An array of enabled window features.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether or not it was triggered by user gesture.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "compilationCacheProduced",
+ "description": "Issued for every compilation cache generated. Is only available\nif Page.setGenerateCompilationCache is enabled.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "data",
+ "description": "Base64-encoded data (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Performance",
+ "types": [
+ {
+ "id": "Metric",
+ "description": "Run-time execution metric.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Metric name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Metric value.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disable collecting and reporting metrics."
+ },
+ {
+ "name": "enable",
+ "description": "Enable collecting and reporting metrics.",
+ "parameters": [
+ {
+ "name": "timeDomain",
+ "description": "Time domain to use for collecting and reporting duration metrics.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "timeTicks",
+ "threadTicks"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setTimeDomain",
+ "description": "Sets time domain to use for collecting and reporting duration metrics.\nNote that this must be called before enabling metrics collection. Calling\nthis method while metrics collection is enabled returns an error.",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "timeDomain",
+ "description": "Time domain",
+ "type": "string",
+ "enum": [
+ "timeTicks",
+ "threadTicks"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "getMetrics",
+ "description": "Retrieve current values of run-time metrics.",
+ "returns": [
+ {
+ "name": "metrics",
+ "description": "Current values for run-time metrics.",
+ "type": "array",
+ "items": {
+ "$ref": "Metric"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "metrics",
+ "description": "Current values of the metrics.",
+ "parameters": [
+ {
+ "name": "metrics",
+ "description": "Current values of the metrics.",
+ "type": "array",
+ "items": {
+ "$ref": "Metric"
+ }
+ },
+ {
+ "name": "title",
+ "description": "Timestamp title.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "PerformanceTimeline",
+ "description": "Reporting of performance timeline events, as specified in\nhttps://w3c.github.io/performance-timeline/#dom-performanceobserver.",
+ "experimental": true,
+ "dependencies": [
+ "DOM",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "LargestContentfulPaint",
+ "description": "See https://github.com/WICG/LargestContentfulPaint and largest_contentful_paint.idl",
+ "type": "object",
+ "properties": [
+ {
+ "name": "renderTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "loadTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "size",
+ "description": "The number of pixels being painted.",
+ "type": "number"
+ },
+ {
+ "name": "elementId",
+ "description": "The id attribute of the element, if available.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "The URL of the image (may be trimmed).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "nodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "LayoutShiftAttribution",
+ "type": "object",
+ "properties": [
+ {
+ "name": "previousRect",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "currentRect",
+ "$ref": "DOM.Rect"
+ },
+ {
+ "name": "nodeId",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ }
+ ]
+ },
+ {
+ "id": "LayoutShift",
+ "description": "See https://wicg.github.io/layout-instability/#sec-layout-shift and layout_shift.idl",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Score increment produced by this event.",
+ "type": "number"
+ },
+ {
+ "name": "hadRecentInput",
+ "type": "boolean"
+ },
+ {
+ "name": "lastInputTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "sources",
+ "type": "array",
+ "items": {
+ "$ref": "LayoutShiftAttribution"
+ }
+ }
+ ]
+ },
+ {
+ "id": "TimelineEvent",
+ "type": "object",
+ "properties": [
+ {
+ "name": "frameId",
+ "description": "Identifies the frame that this event is related to. Empty for non-frame targets.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "type",
+ "description": "The event type, as specified in https://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\nThis determines which of the optional \"details\" fiedls is present.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Name may be empty depending on the type.",
+ "type": "string"
+ },
+ {
+ "name": "time",
+ "description": "Time in seconds since Epoch, monotonically increasing within document lifetime.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "duration",
+ "description": "Event duration, if applicable.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "lcpDetails",
+ "optional": true,
+ "$ref": "LargestContentfulPaint"
+ },
+ {
+ "name": "layoutShiftDetails",
+ "optional": true,
+ "$ref": "LayoutShift"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Previously buffered events would be reported before method returns.\nSee also: timelineEventAdded",
+ "parameters": [
+ {
+ "name": "eventTypes",
+ "description": "The types of event to report, as specified in\nhttps://w3c.github.io/performance-timeline/#dom-performanceentry-entrytype\nThe specified filter overrides any previous filters, passing empty\nfilter disables recording.\nNote that not all types exposed to the web platform are currently supported.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "timelineEventAdded",
+ "description": "Sent when a performance timeline event is added. See reportPerformanceTimeline method.",
+ "parameters": [
+ {
+ "name": "event",
+ "$ref": "TimelineEvent"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Security",
+ "description": "Security",
+ "types": [
+ {
+ "id": "CertificateId",
+ "description": "An internal certificate ID value.",
+ "type": "integer"
+ },
+ {
+ "id": "MixedContentType",
+ "description": "A description of mixed content (HTTP resources on HTTPS pages), as defined by\nhttps://www.w3.org/TR/mixed-content/#categories",
+ "type": "string",
+ "enum": [
+ "blockable",
+ "optionally-blockable",
+ "none"
+ ]
+ },
+ {
+ "id": "SecurityState",
+ "description": "The security level of a page or resource.",
+ "type": "string",
+ "enum": [
+ "unknown",
+ "neutral",
+ "insecure",
+ "secure",
+ "info",
+ "insecure-broken"
+ ]
+ },
+ {
+ "id": "CertificateSecurityState",
+ "description": "Details about the security state of the page certificate.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "description": "Protocol name (e.g. \"TLS 1.2\" or \"QUIC\").",
+ "type": "string"
+ },
+ {
+ "name": "keyExchange",
+ "description": "Key Exchange used by the connection, or the empty string if not applicable.",
+ "type": "string"
+ },
+ {
+ "name": "keyExchangeGroup",
+ "description": "(EC)DH group used by the connection, if applicable.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "cipher",
+ "description": "Cipher name.",
+ "type": "string"
+ },
+ {
+ "name": "mac",
+ "description": "TLS MAC. Note that AEAD ciphers do not have separate MACs.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificate",
+ "description": "Page certificate.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "subjectName",
+ "description": "Certificate subject name.",
+ "type": "string"
+ },
+ {
+ "name": "issuer",
+ "description": "Name of the issuing CA.",
+ "type": "string"
+ },
+ {
+ "name": "validFrom",
+ "description": "Certificate valid from date.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "validTo",
+ "description": "Certificate valid to (expiration) date",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "certificateNetworkError",
+ "description": "The highest priority network error code, if the certificate has an error.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "certificateHasWeakSignature",
+ "description": "True if the certificate uses a weak signature aglorithm.",
+ "type": "boolean"
+ },
+ {
+ "name": "certificateHasSha1Signature",
+ "description": "True if the certificate has a SHA1 signature in the chain.",
+ "type": "boolean"
+ },
+ {
+ "name": "modernSSL",
+ "description": "True if modern SSL",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslProtocol",
+ "description": "True if the connection is using an obsolete SSL protocol.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslKeyExchange",
+ "description": "True if the connection is using an obsolete SSL key exchange.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslCipher",
+ "description": "True if the connection is using an obsolete SSL cipher.",
+ "type": "boolean"
+ },
+ {
+ "name": "obsoleteSslSignature",
+ "description": "True if the connection is using an obsolete SSL signature.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "SafetyTipStatus",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "badReputation",
+ "lookalike"
+ ]
+ },
+ {
+ "id": "SafetyTipInfo",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "safetyTipStatus",
+ "description": "Describes whether the page triggers any safety tips or reputation warnings. Default is unknown.",
+ "$ref": "SafetyTipStatus"
+ },
+ {
+ "name": "safeUrl",
+ "description": "The URL the safety tip suggested (\"Did you mean?\"). Only filled in for lookalike matches.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "VisibleSecurityState",
+ "description": "Security state information about the page.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityState",
+ "description": "The security level of the page.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "certificateSecurityState",
+ "description": "Security state details about the page certificate.",
+ "optional": true,
+ "$ref": "CertificateSecurityState"
+ },
+ {
+ "name": "safetyTipInfo",
+ "description": "The type of Safety Tip triggered on the page. Note that this field will be set even if the Safety Tip UI was not actually shown.",
+ "optional": true,
+ "$ref": "SafetyTipInfo"
+ },
+ {
+ "name": "securityStateIssueIds",
+ "description": "Array of security state issues ids.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SecurityStateExplanation",
+ "description": "An explanation of an factor contributing to the security state.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "securityState",
+ "description": "Security state representing the severity of the factor being explained.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "title",
+ "description": "Title describing the type of factor.",
+ "type": "string"
+ },
+ {
+ "name": "summary",
+ "description": "Short phrase describing the type of factor.",
+ "type": "string"
+ },
+ {
+ "name": "description",
+ "description": "Full text explanation of the factor.",
+ "type": "string"
+ },
+ {
+ "name": "mixedContentType",
+ "description": "The type of mixed content described by the explanation.",
+ "$ref": "MixedContentType"
+ },
+ {
+ "name": "certificate",
+ "description": "Page certificate.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "recommendations",
+ "description": "Recommendations to fix any issues.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "InsecureContentStatus",
+ "description": "Information about insecure content on the page.",
+ "deprecated": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "ranMixedContent",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "displayedMixedContent",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "containedMixedForm",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "ranContentWithCertErrors",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "displayedContentWithCertErrors",
+ "description": "Always false.",
+ "type": "boolean"
+ },
+ {
+ "name": "ranInsecureContentStyle",
+ "description": "Always set to unknown.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "displayedInsecureContentStyle",
+ "description": "Always set to unknown.",
+ "$ref": "SecurityState"
+ }
+ ]
+ },
+ {
+ "id": "CertificateErrorAction",
+ "description": "The action to take when a certificate error occurs. continue will continue processing the\nrequest and cancel will cancel the request.",
+ "type": "string",
+ "enum": [
+ "continue",
+ "cancel"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables tracking security state changes."
+ },
+ {
+ "name": "enable",
+ "description": "Enables tracking security state changes."
+ },
+ {
+ "name": "setIgnoreCertificateErrors",
+ "description": "Enable/disable whether all certificate errors should be ignored.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ignore",
+ "description": "If true, all certificate errors will be ignored.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "handleCertificateError",
+ "description": "Handles a certificate error that fired a certificateError event.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "eventId",
+ "description": "The ID of the event.",
+ "type": "integer"
+ },
+ {
+ "name": "action",
+ "description": "The action to take on the certificate error.",
+ "$ref": "CertificateErrorAction"
+ }
+ ]
+ },
+ {
+ "name": "setOverrideCertificateErrors",
+ "description": "Enable/disable overriding certificate errors. If enabled, all certificate error events need to\nbe handled by the DevTools client and should be answered with `handleCertificateError` commands.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "override",
+ "description": "If true, certificate errors will be overridden.",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "certificateError",
+ "description": "There is a certificate error. If overriding certificate errors is enabled, then it should be\nhandled with the `handleCertificateError` command. Note: this event does not fire if the\ncertificate error has been allowed internally. Only one client per target should override\ncertificate errors at the same time.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "eventId",
+ "description": "The ID of the event.",
+ "type": "integer"
+ },
+ {
+ "name": "errorType",
+ "description": "The type of the error.",
+ "type": "string"
+ },
+ {
+ "name": "requestURL",
+ "description": "The url that was requested.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "visibleSecurityStateChanged",
+ "description": "The security state of the page changed.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "visibleSecurityState",
+ "description": "Security state information about the page.",
+ "$ref": "VisibleSecurityState"
+ }
+ ]
+ },
+ {
+ "name": "securityStateChanged",
+ "description": "The security state of the page changed. No longer being sent.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "securityState",
+ "description": "Security state.",
+ "$ref": "SecurityState"
+ },
+ {
+ "name": "schemeIsCryptographic",
+ "description": "True if the page was loaded over cryptographic transport such as HTTPS.",
+ "deprecated": true,
+ "type": "boolean"
+ },
+ {
+ "name": "explanations",
+ "description": "Previously a list of explanations for the security state. Now always\nempty.",
+ "deprecated": true,
+ "type": "array",
+ "items": {
+ "$ref": "SecurityStateExplanation"
+ }
+ },
+ {
+ "name": "insecureContentStatus",
+ "description": "Information about insecure content on the page.",
+ "deprecated": true,
+ "$ref": "InsecureContentStatus"
+ },
+ {
+ "name": "summary",
+ "description": "Overrides user-visible description of the state. Always omitted.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "ServiceWorker",
+ "experimental": true,
+ "dependencies": [
+ "Target"
+ ],
+ "types": [
+ {
+ "id": "RegistrationID",
+ "type": "string"
+ },
+ {
+ "id": "ServiceWorkerRegistration",
+ "description": "ServiceWorker registration.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "scopeURL",
+ "type": "string"
+ },
+ {
+ "name": "isDeleted",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersionRunningStatus",
+ "type": "string",
+ "enum": [
+ "stopped",
+ "starting",
+ "running",
+ "stopping"
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersionStatus",
+ "type": "string",
+ "enum": [
+ "new",
+ "installing",
+ "installed",
+ "activating",
+ "activated",
+ "redundant"
+ ]
+ },
+ {
+ "id": "ServiceWorkerVersion",
+ "description": "ServiceWorker version.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "versionId",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "scriptURL",
+ "type": "string"
+ },
+ {
+ "name": "runningStatus",
+ "$ref": "ServiceWorkerVersionRunningStatus"
+ },
+ {
+ "name": "status",
+ "$ref": "ServiceWorkerVersionStatus"
+ },
+ {
+ "name": "scriptLastModified",
+ "description": "The Last-Modified header value of the main script.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "scriptResponseTime",
+ "description": "The time at which the response headers of the main script were received from the server.\nFor cached script it is the last time the cache entry was validated.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "controlledClients",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "Target.TargetID"
+ }
+ },
+ {
+ "name": "targetId",
+ "optional": true,
+ "$ref": "Target.TargetID"
+ }
+ ]
+ },
+ {
+ "id": "ServiceWorkerErrorMessage",
+ "description": "ServiceWorker error message.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorMessage",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "versionId",
+ "type": "string"
+ },
+ {
+ "name": "sourceURL",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "deliverPushMessage",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "data",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "dispatchSyncEvent",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "tag",
+ "type": "string"
+ },
+ {
+ "name": "lastChance",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "dispatchPeriodicSyncEvent",
+ "parameters": [
+ {
+ "name": "origin",
+ "type": "string"
+ },
+ {
+ "name": "registrationId",
+ "$ref": "RegistrationID"
+ },
+ {
+ "name": "tag",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "inspectWorker",
+ "parameters": [
+ {
+ "name": "versionId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setForceUpdateOnPageLoad",
+ "parameters": [
+ {
+ "name": "forceUpdateOnPageLoad",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "skipWaiting",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "startWorker",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "stopAllWorkers"
+ },
+ {
+ "name": "stopWorker",
+ "parameters": [
+ {
+ "name": "versionId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "unregister",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "updateRegistration",
+ "parameters": [
+ {
+ "name": "scopeURL",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "workerErrorReported",
+ "parameters": [
+ {
+ "name": "errorMessage",
+ "$ref": "ServiceWorkerErrorMessage"
+ }
+ ]
+ },
+ {
+ "name": "workerRegistrationUpdated",
+ "parameters": [
+ {
+ "name": "registrations",
+ "type": "array",
+ "items": {
+ "$ref": "ServiceWorkerRegistration"
+ }
+ }
+ ]
+ },
+ {
+ "name": "workerVersionUpdated",
+ "parameters": [
+ {
+ "name": "versions",
+ "type": "array",
+ "items": {
+ "$ref": "ServiceWorkerVersion"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Storage",
+ "experimental": true,
+ "dependencies": [
+ "Browser",
+ "Network"
+ ],
+ "types": [
+ {
+ "id": "SerializedStorageKey",
+ "type": "string"
+ },
+ {
+ "id": "StorageType",
+ "description": "Enum of possible storage types.",
+ "type": "string",
+ "enum": [
+ "appcache",
+ "cookies",
+ "file_systems",
+ "indexeddb",
+ "local_storage",
+ "shader_cache",
+ "websql",
+ "service_workers",
+ "cache_storage",
+ "interest_groups",
+ "shared_storage",
+ "storage_buckets",
+ "all",
+ "other"
+ ]
+ },
+ {
+ "id": "UsageForType",
+ "description": "Usage for a storage type.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "storageType",
+ "description": "Name of storage type.",
+ "$ref": "StorageType"
+ },
+ {
+ "name": "usage",
+ "description": "Storage usage (bytes).",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "TrustTokens",
+ "description": "Pair of issuer origin and number of available (signed, but not used) Trust\nTokens from that issuer.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "issuerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "count",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "InterestGroupAccessType",
+ "description": "Enum of interest group access types.",
+ "type": "string",
+ "enum": [
+ "join",
+ "leave",
+ "update",
+ "loaded",
+ "bid",
+ "win"
+ ]
+ },
+ {
+ "id": "InterestGroupAd",
+ "description": "Ad advertising element inside an interest group.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "renderUrl",
+ "type": "string"
+ },
+ {
+ "name": "metadata",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "InterestGroupDetails",
+ "description": "The full details of an interest group.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "expirationTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "joiningOrigin",
+ "type": "string"
+ },
+ {
+ "name": "biddingUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "biddingWasmHelperUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "updateUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "trustedBiddingSignalsUrl",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "trustedBiddingSignalsKeys",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "userBiddingSignals",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "ads",
+ "type": "array",
+ "items": {
+ "$ref": "InterestGroupAd"
+ }
+ },
+ {
+ "name": "adComponents",
+ "type": "array",
+ "items": {
+ "$ref": "InterestGroupAd"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageAccessType",
+ "description": "Enum of shared storage access types.",
+ "type": "string",
+ "enum": [
+ "documentAddModule",
+ "documentSelectURL",
+ "documentRun",
+ "documentSet",
+ "documentAppend",
+ "documentDelete",
+ "documentClear",
+ "workletSet",
+ "workletAppend",
+ "workletDelete",
+ "workletClear",
+ "workletGet",
+ "workletKeys",
+ "workletEntries",
+ "workletLength",
+ "workletRemainingBudget"
+ ]
+ },
+ {
+ "id": "SharedStorageEntry",
+ "description": "Struct for a single key-value pair in an origin's shared storage.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageMetadata",
+ "description": "Details for an origin's shared storage.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "creationTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "length",
+ "type": "integer"
+ },
+ {
+ "name": "remainingBudget",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageReportingMetadata",
+ "description": "Pair of reporting metadata details for a candidate URL for `selectURL()`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "eventType",
+ "type": "string"
+ },
+ {
+ "name": "reportingUrl",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageUrlWithMetadata",
+ "description": "Bundles a candidate URL with its reporting metadata.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "url",
+ "description": "Spec of candidate URL.",
+ "type": "string"
+ },
+ {
+ "name": "reportingMetadata",
+ "description": "Any associated reporting metadata.",
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageReportingMetadata"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SharedStorageAccessParams",
+ "description": "Bundles the parameters for shared storage access events whose\npresence/absence can vary according to SharedStorageAccessType.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptSourceUrl",
+ "description": "Spec of the module script URL.\nPresent only for SharedStorageAccessType.documentAddModule.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "operationName",
+ "description": "Name of the registered operation to be run.\nPresent only for SharedStorageAccessType.documentRun and\nSharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "serializedData",
+ "description": "The operation's serialized data in bytes (converted to a string).\nPresent only for SharedStorageAccessType.documentRun and\nSharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "urlsWithMetadata",
+ "description": "Array of candidate URLs' specs, along with any associated metadata.\nPresent only for SharedStorageAccessType.documentSelectURL.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageUrlWithMetadata"
+ }
+ },
+ {
+ "name": "key",
+ "description": "Key for a specific entry in an origin's shared storage.\nPresent only for SharedStorageAccessType.documentSet,\nSharedStorageAccessType.documentAppend,\nSharedStorageAccessType.documentDelete,\nSharedStorageAccessType.workletSet,\nSharedStorageAccessType.workletAppend,\nSharedStorageAccessType.workletDelete, and\nSharedStorageAccessType.workletGet.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Value for a specific entry in an origin's shared storage.\nPresent only for SharedStorageAccessType.documentSet,\nSharedStorageAccessType.documentAppend,\nSharedStorageAccessType.workletSet, and\nSharedStorageAccessType.workletAppend.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "ignoreIfPresent",
+ "description": "Whether or not to set an entry for a key if that key is already present.\nPresent only for SharedStorageAccessType.documentSet and\nSharedStorageAccessType.workletSet.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "StorageBucketsDurability",
+ "type": "string",
+ "enum": [
+ "relaxed",
+ "strict"
+ ]
+ },
+ {
+ "id": "StorageBucket",
+ "type": "object",
+ "properties": [
+ {
+ "name": "storageKey",
+ "$ref": "SerializedStorageKey"
+ },
+ {
+ "name": "name",
+ "description": "If not specified, it is the default bucket of the storageKey.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "StorageBucketInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "bucket",
+ "$ref": "StorageBucket"
+ },
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "expiration",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "quota",
+ "description": "Storage quota (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "persistent",
+ "type": "boolean"
+ },
+ {
+ "name": "durability",
+ "$ref": "StorageBucketsDurability"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceType",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "navigation",
+ "event"
+ ]
+ },
+ {
+ "id": "UnsignedInt64AsBase10",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "UnsignedInt128AsBase16",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "SignedInt64AsBase10",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "AttributionReportingFilterDataEntry",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "values",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingAggregationKeysEntry",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "$ref": "UnsignedInt128AsBase16"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceRegistration",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "time",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "expiry",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "eventReportWindow",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "aggregatableReportWindow",
+ "description": "duration in seconds",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "type",
+ "$ref": "AttributionReportingSourceType"
+ },
+ {
+ "name": "sourceOrigin",
+ "type": "string"
+ },
+ {
+ "name": "reportingOrigin",
+ "type": "string"
+ },
+ {
+ "name": "destinationSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "eventId",
+ "$ref": "UnsignedInt64AsBase10"
+ },
+ {
+ "name": "priority",
+ "$ref": "SignedInt64AsBase10"
+ },
+ {
+ "name": "filterData",
+ "type": "array",
+ "items": {
+ "$ref": "AttributionReportingFilterDataEntry"
+ }
+ },
+ {
+ "name": "aggregationKeys",
+ "type": "array",
+ "items": {
+ "$ref": "AttributionReportingAggregationKeysEntry"
+ }
+ },
+ {
+ "name": "debugKey",
+ "optional": true,
+ "$ref": "UnsignedInt64AsBase10"
+ }
+ ]
+ },
+ {
+ "id": "AttributionReportingSourceRegistrationResult",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "success",
+ "internalError",
+ "insufficientSourceCapacity",
+ "insufficientUniqueDestinationCapacity",
+ "excessiveReportingOrigins",
+ "prohibitedByBrowserPolicy",
+ "successNoised",
+ "destinationReportingLimitReached",
+ "destinationGlobalLimitReached",
+ "destinationBothLimitsReached",
+ "reportingOriginsPerSiteLimitReached"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getStorageKeyForFrame",
+ "description": "Returns a storage key given a frame id.",
+ "parameters": [
+ {
+ "name": "frameId",
+ "$ref": "Page.FrameId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "storageKey",
+ "$ref": "SerializedStorageKey"
+ }
+ ]
+ },
+ {
+ "name": "clearDataForOrigin",
+ "description": "Clears storage for origin.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ },
+ {
+ "name": "storageTypes",
+ "description": "Comma separated list of StorageType to clear.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearDataForStorageKey",
+ "description": "Clears storage for storage key.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ },
+ {
+ "name": "storageTypes",
+ "description": "Comma separated list of StorageType to clear.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getCookies",
+ "description": "Returns all browser cookies.",
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "cookies",
+ "description": "Array of cookie objects.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.Cookie"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setCookies",
+ "description": "Sets given cookies.",
+ "parameters": [
+ {
+ "name": "cookies",
+ "description": "Cookies to be set.",
+ "type": "array",
+ "items": {
+ "$ref": "Network.CookieParam"
+ }
+ },
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "clearCookies",
+ "description": "Clears cookies.",
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "description": "Browser context to use when called on the browser endpoint.",
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getUsageAndQuota",
+ "description": "Returns usage and quota in bytes.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "usage",
+ "description": "Storage usage (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "quota",
+ "description": "Storage quota (bytes).",
+ "type": "number"
+ },
+ {
+ "name": "overrideActive",
+ "description": "Whether or not the origin has an active storage quota override",
+ "type": "boolean"
+ },
+ {
+ "name": "usageBreakdown",
+ "description": "Storage usage per type (bytes).",
+ "type": "array",
+ "items": {
+ "$ref": "UsageForType"
+ }
+ }
+ ]
+ },
+ {
+ "name": "overrideQuotaForOrigin",
+ "description": "Override quota for the specified origin",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ },
+ {
+ "name": "quotaSize",
+ "description": "The quota size (in bytes) to override the original quota with.\nIf this is called multiple times, the overridden quota will be equal to\nthe quotaSize provided in the final call. If this is called without\nspecifying a quotaSize, the quota will be reset to the default value for\nthe specified origin. If this is called multiple times with different\norigins, the override will be maintained for each origin until it is\ndisabled (called without a quotaSize).",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "trackCacheStorageForOrigin",
+ "description": "Registers origin to be notified when an update occurs to its cache storage list.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackCacheStorageForStorageKey",
+ "description": "Registers storage key to be notified when an update occurs to its cache storage list.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackIndexedDBForOrigin",
+ "description": "Registers origin to be notified when an update occurs to its IndexedDB.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "trackIndexedDBForStorageKey",
+ "description": "Registers storage key to be notified when an update occurs to its IndexedDB.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackCacheStorageForOrigin",
+ "description": "Unregisters origin from receiving notifications for cache storage.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackCacheStorageForStorageKey",
+ "description": "Unregisters storage key from receiving notifications for cache storage.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackIndexedDBForOrigin",
+ "description": "Unregisters origin from receiving notifications for IndexedDB.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Security origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "untrackIndexedDBForStorageKey",
+ "description": "Unregisters storage key from receiving notifications for IndexedDB.",
+ "parameters": [
+ {
+ "name": "storageKey",
+ "description": "Storage key.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getTrustTokens",
+ "description": "Returns the number of stored Trust Tokens per issuer for the\ncurrent browsing context.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "tokens",
+ "type": "array",
+ "items": {
+ "$ref": "TrustTokens"
+ }
+ }
+ ]
+ },
+ {
+ "name": "clearTrustTokens",
+ "description": "Removes all Trust Tokens issued by the provided issuerOrigin.\nLeaves other stored data, including the issuer's Redemption Records, intact.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "issuerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "didDeleteTokens",
+ "description": "True if any tokens were deleted, false otherwise.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getInterestGroupDetails",
+ "description": "Gets details for a named interest group.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "details",
+ "$ref": "InterestGroupDetails"
+ }
+ ]
+ },
+ {
+ "name": "setInterestGroupTracking",
+ "description": "Enables/Disables issuing of interestGroupAccessed events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getSharedStorageMetadata",
+ "description": "Gets metadata for an origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "metadata",
+ "$ref": "SharedStorageMetadata"
+ }
+ ]
+ },
+ {
+ "name": "getSharedStorageEntries",
+ "description": "Gets the entries in an given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "entries",
+ "type": "array",
+ "items": {
+ "$ref": "SharedStorageEntry"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setSharedStorageEntry",
+ "description": "Sets entry with `key` and `value` for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ },
+ {
+ "name": "ignoreIfPresent",
+ "description": "If `ignoreIfPresent` is included and true, then only sets the entry if\n`key` doesn't already exist.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "deleteSharedStorageEntry",
+ "description": "Deletes entry for `key` (if it exists) for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "key",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearSharedStorageEntries",
+ "description": "Clears all entries for a given origin's shared storage.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "resetSharedStorageBudget",
+ "description": "Resets the budget for `ownerOrigin` by clearing all budget withdrawals.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "setSharedStorageTracking",
+ "description": "Enables/disables issuing of sharedStorageAccessed events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setStorageBucketTracking",
+ "description": "Set tracking for a storage key's buckets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "storageKey",
+ "type": "string"
+ },
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "deleteStorageBucket",
+ "description": "Deletes the Storage Bucket with the given storage key and bucket name.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "bucket",
+ "$ref": "StorageBucket"
+ }
+ ]
+ },
+ {
+ "name": "runBounceTrackingMitigations",
+ "description": "Deletes state for sites identified as potential bounce trackers, immediately.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "deletedSites",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAttributionReportingLocalTestingMode",
+ "description": "https://wicg.github.io/attribution-reporting-api/",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "description": "If enabled, noise is suppressed and reports are sent immediately.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAttributionReportingTracking",
+ "description": "Enables/disables issuing of Attribution Reporting events.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enable",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "cacheStorageContentUpdated",
+ "description": "A cache's contents have been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ },
+ {
+ "name": "cacheName",
+ "description": "Name of cache in origin.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "cacheStorageListUpdated",
+ "description": "A cache has been added/deleted.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "indexedDBContentUpdated",
+ "description": "The origin's IndexedDB object store has been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ },
+ {
+ "name": "databaseName",
+ "description": "Database to update.",
+ "type": "string"
+ },
+ {
+ "name": "objectStoreName",
+ "description": "ObjectStore to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "indexedDBListUpdated",
+ "description": "The origin's IndexedDB database list has been modified.",
+ "parameters": [
+ {
+ "name": "origin",
+ "description": "Origin to update.",
+ "type": "string"
+ },
+ {
+ "name": "storageKey",
+ "description": "Storage key to update.",
+ "type": "string"
+ },
+ {
+ "name": "bucketId",
+ "description": "Storage bucket to update.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "interestGroupAccessed",
+ "description": "One of the interest groups was accessed by the associated page.",
+ "parameters": [
+ {
+ "name": "accessTime",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "type",
+ "$ref": "InterestGroupAccessType"
+ },
+ {
+ "name": "ownerOrigin",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "sharedStorageAccessed",
+ "description": "Shared storage was accessed by the associated page.\nThe following parameters are included in all events.",
+ "parameters": [
+ {
+ "name": "accessTime",
+ "description": "Time of the access.",
+ "$ref": "Network.TimeSinceEpoch"
+ },
+ {
+ "name": "type",
+ "description": "Enum value indicating the Shared Storage API method invoked.",
+ "$ref": "SharedStorageAccessType"
+ },
+ {
+ "name": "mainFrameId",
+ "description": "DevTools Frame Token for the primary frame tree's root.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "ownerOrigin",
+ "description": "Serialized origin for the context that invoked the Shared Storage API.",
+ "type": "string"
+ },
+ {
+ "name": "params",
+ "description": "The sub-parameters warapped by `params` are all optional and their\npresence/absence depends on `type`.",
+ "$ref": "SharedStorageAccessParams"
+ }
+ ]
+ },
+ {
+ "name": "storageBucketCreatedOrUpdated",
+ "parameters": [
+ {
+ "name": "bucketInfo",
+ "$ref": "StorageBucketInfo"
+ }
+ ]
+ },
+ {
+ "name": "storageBucketDeleted",
+ "parameters": [
+ {
+ "name": "bucketId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "attributionReportingSourceRegistered",
+ "description": "TODO(crbug.com/1458532): Add other Attribution Reporting events, e.g.\ntrigger registration.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "registration",
+ "$ref": "AttributionReportingSourceRegistration"
+ },
+ {
+ "name": "result",
+ "$ref": "AttributionReportingSourceRegistrationResult"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "SystemInfo",
+ "description": "The SystemInfo domain defines methods and events for querying low-level system information.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "GPUDevice",
+ "description": "Describes a single graphics processor (GPU).",
+ "type": "object",
+ "properties": [
+ {
+ "name": "vendorId",
+ "description": "PCI ID of the GPU vendor, if available; 0 otherwise.",
+ "type": "number"
+ },
+ {
+ "name": "deviceId",
+ "description": "PCI ID of the GPU device, if available; 0 otherwise.",
+ "type": "number"
+ },
+ {
+ "name": "subSysId",
+ "description": "Sub sys ID of the GPU, only available on Windows.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "revision",
+ "description": "Revision of the GPU, only available on Windows.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "vendorString",
+ "description": "String description of the GPU vendor, if the PCI ID is not available.",
+ "type": "string"
+ },
+ {
+ "name": "deviceString",
+ "description": "String description of the GPU device, if the PCI ID is not available.",
+ "type": "string"
+ },
+ {
+ "name": "driverVendor",
+ "description": "String description of the GPU driver vendor.",
+ "type": "string"
+ },
+ {
+ "name": "driverVersion",
+ "description": "String description of the GPU driver version.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "Size",
+ "description": "Describes the width and height dimensions of an entity.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "width",
+ "description": "Width in pixels.",
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Height in pixels.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "VideoDecodeAcceleratorCapability",
+ "description": "Describes a supported video decoding profile with its associated minimum and\nmaximum resolutions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "profile",
+ "description": "Video codec profile that is supported, e.g. VP9 Profile 2.",
+ "type": "string"
+ },
+ {
+ "name": "maxResolution",
+ "description": "Maximum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ },
+ {
+ "name": "minResolution",
+ "description": "Minimum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ }
+ ]
+ },
+ {
+ "id": "VideoEncodeAcceleratorCapability",
+ "description": "Describes a supported video encoding profile with its associated maximum\nresolution and maximum framerate.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "profile",
+ "description": "Video codec profile that is supported, e.g H264 Main.",
+ "type": "string"
+ },
+ {
+ "name": "maxResolution",
+ "description": "Maximum video dimensions in pixels supported for this |profile|.",
+ "$ref": "Size"
+ },
+ {
+ "name": "maxFramerateNumerator",
+ "description": "Maximum encoding framerate in frames per second supported for this\n|profile|, as fraction's numerator and denominator, e.g. 24/1 fps,\n24000/1001 fps, etc.",
+ "type": "integer"
+ },
+ {
+ "name": "maxFramerateDenominator",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "SubsamplingFormat",
+ "description": "YUV subsampling type of the pixels of a given image.",
+ "type": "string",
+ "enum": [
+ "yuv420",
+ "yuv422",
+ "yuv444"
+ ]
+ },
+ {
+ "id": "ImageType",
+ "description": "Image format of a given image.",
+ "type": "string",
+ "enum": [
+ "jpeg",
+ "webp",
+ "unknown"
+ ]
+ },
+ {
+ "id": "ImageDecodeAcceleratorCapability",
+ "description": "Describes a supported image decoding profile with its associated minimum and\nmaximum resolutions and subsampling.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "imageType",
+ "description": "Image coded, e.g. Jpeg.",
+ "$ref": "ImageType"
+ },
+ {
+ "name": "maxDimensions",
+ "description": "Maximum supported dimensions of the image in pixels.",
+ "$ref": "Size"
+ },
+ {
+ "name": "minDimensions",
+ "description": "Minimum supported dimensions of the image in pixels.",
+ "$ref": "Size"
+ },
+ {
+ "name": "subsamplings",
+ "description": "Optional array of supported subsampling formats, e.g. 4:2:0, if known.",
+ "type": "array",
+ "items": {
+ "$ref": "SubsamplingFormat"
+ }
+ }
+ ]
+ },
+ {
+ "id": "GPUInfo",
+ "description": "Provides information about the GPU(s) on the system.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "devices",
+ "description": "The graphics devices on the system. Element 0 is the primary GPU.",
+ "type": "array",
+ "items": {
+ "$ref": "GPUDevice"
+ }
+ },
+ {
+ "name": "auxAttributes",
+ "description": "An optional dictionary of additional GPU related attributes.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "featureStatus",
+ "description": "An optional dictionary of graphics features and their status.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "driverBugWorkarounds",
+ "description": "An optional array of GPU driver bug workarounds.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "videoDecoding",
+ "description": "Supported accelerated video decoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "VideoDecodeAcceleratorCapability"
+ }
+ },
+ {
+ "name": "videoEncoding",
+ "description": "Supported accelerated video encoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "VideoEncodeAcceleratorCapability"
+ }
+ },
+ {
+ "name": "imageDecoding",
+ "description": "Supported accelerated image decoding capabilities.",
+ "type": "array",
+ "items": {
+ "$ref": "ImageDecodeAcceleratorCapability"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ProcessInfo",
+ "description": "Represents process info.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Specifies process type.",
+ "type": "string"
+ },
+ {
+ "name": "id",
+ "description": "Specifies process id.",
+ "type": "integer"
+ },
+ {
+ "name": "cpuTime",
+ "description": "Specifies cumulative CPU usage in seconds across all threads of the\nprocess since the process start.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getInfo",
+ "description": "Returns information about the system.",
+ "returns": [
+ {
+ "name": "gpu",
+ "description": "Information about the GPUs on the system.",
+ "$ref": "GPUInfo"
+ },
+ {
+ "name": "modelName",
+ "description": "A platform-dependent description of the model of the machine. On Mac OS, this is, for\nexample, 'MacBookPro'. Will be the empty string if not supported.",
+ "type": "string"
+ },
+ {
+ "name": "modelVersion",
+ "description": "A platform-dependent description of the version of the machine. On Mac OS, this is, for\nexample, '10.1'. Will be the empty string if not supported.",
+ "type": "string"
+ },
+ {
+ "name": "commandLine",
+ "description": "The command line string used to launch the browser. Will be the empty string if not\nsupported.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getFeatureState",
+ "description": "Returns information about the feature state.",
+ "parameters": [
+ {
+ "name": "featureState",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "featureEnabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "getProcessInfo",
+ "description": "Returns information about all running processes.",
+ "returns": [
+ {
+ "name": "processInfo",
+ "description": "An array of process info blocks.",
+ "type": "array",
+ "items": {
+ "$ref": "ProcessInfo"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Target",
+ "description": "Supports additional targets discovery and allows to attach to them.",
+ "types": [
+ {
+ "id": "TargetID",
+ "type": "string"
+ },
+ {
+ "id": "SessionID",
+ "description": "Unique identifier of attached debugging session.",
+ "type": "string"
+ },
+ {
+ "id": "TargetInfo",
+ "type": "object",
+ "properties": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "type",
+ "type": "string"
+ },
+ {
+ "name": "title",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "attached",
+ "description": "Whether the target has an attached client.",
+ "type": "boolean"
+ },
+ {
+ "name": "openerId",
+ "description": "Opener target Id",
+ "optional": true,
+ "$ref": "TargetID"
+ },
+ {
+ "name": "canAccessOpener",
+ "description": "Whether the target has access to the originating window.",
+ "experimental": true,
+ "type": "boolean"
+ },
+ {
+ "name": "openerFrameId",
+ "description": "Frame id of originating window (is only set if target has an opener).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "browserContextId",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ },
+ {
+ "name": "subtype",
+ "description": "Provides additional details for specific target types. For example, for\nthe type of \"page\", this may be set to \"portal\" or \"prerender\".",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "FilterEntry",
+ "description": "A filter used by target query/discovery/auto-attach operations.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "exclude",
+ "description": "If set, causes exclusion of mathcing targets from the list.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "type",
+ "description": "If not present, matches any type.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "TargetFilter",
+ "description": "The entries in TargetFilter are matched sequentially against targets and\nthe first entry that matches determines if the target is included or not,\ndepending on the value of `exclude` field in the entry.\nIf filter is not specified, the one assumed is\n[{type: \"browser\", exclude: true}, {type: \"tab\", exclude: true}, {}]\n(i.e. include everything but `browser` and `tab`).",
+ "experimental": true,
+ "type": "array",
+ "items": {
+ "$ref": "FilterEntry"
+ }
+ },
+ {
+ "id": "RemoteLocation",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "host",
+ "type": "string"
+ },
+ {
+ "name": "port",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "activateTarget",
+ "description": "Activates (focuses) the target.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "attachToTarget",
+ "description": "Attaches to the target with given id.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "flatten",
+ "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.\nWe plan to make this the default, deprecate non-flattened mode,\nand eventually retire it. See crbug.com/991325.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "sessionId",
+ "description": "Id assigned to the session.",
+ "$ref": "SessionID"
+ }
+ ]
+ },
+ {
+ "name": "attachToBrowserTarget",
+ "description": "Attaches to the browser target, only uses flat sessionId mode.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "sessionId",
+ "description": "Id assigned to the session.",
+ "$ref": "SessionID"
+ }
+ ]
+ },
+ {
+ "name": "closeTarget",
+ "description": "Closes the target. If the target is a page that gets closed too.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "success",
+ "description": "Always set to true. If an error occurs, the response indicates protocol error.",
+ "deprecated": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "exposeDevToolsProtocol",
+ "description": "Inject object to the target's main frame that provides a communication\nchannel with browser target.\n\nInjected object will be available as `window[bindingName]`.\n\nThe object has the follwing API:\n- `binding.send(json)` - a method to send messages over the remote debugging protocol\n- `binding.onmessage = json => handleMessage(json)` - a callback that will be called for the protocol notifications and command responses.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "bindingName",
+ "description": "Binding name, 'cdp' if not specified.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "createBrowserContext",
+ "description": "Creates a new empty BrowserContext. Similar to an incognito profile but you can have more than\none.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "disposeOnDetach",
+ "description": "If specified, disposes this context when debugging session disconnects.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "proxyServer",
+ "description": "Proxy server, similar to the one passed to --proxy-server",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "proxyBypassList",
+ "description": "Proxy bypass list, similar to the one passed to --proxy-bypass-list",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "originsWithUniversalNetworkAccess",
+ "description": "An optional list of origins to grant unlimited cross-origin access to.\nParts of the URL other than those constituting origin are ignored.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ],
+ "returns": [
+ {
+ "name": "browserContextId",
+ "description": "The id of the context created.",
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getBrowserContexts",
+ "description": "Returns all browser contexts created with `Target.createBrowserContext` method.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "browserContextIds",
+ "description": "An array of browser context ids.",
+ "type": "array",
+ "items": {
+ "$ref": "Browser.BrowserContextID"
+ }
+ }
+ ]
+ },
+ {
+ "name": "createTarget",
+ "description": "Creates a new page.",
+ "parameters": [
+ {
+ "name": "url",
+ "description": "The initial URL the page will be navigated to. An empty string indicates about:blank.",
+ "type": "string"
+ },
+ {
+ "name": "width",
+ "description": "Frame width in DIP (headless chrome only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "height",
+ "description": "Frame height in DIP (headless chrome only).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "browserContextId",
+ "description": "The browser context to create the page in.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Browser.BrowserContextID"
+ },
+ {
+ "name": "enableBeginFrameControl",
+ "description": "Whether BeginFrames for this target will be controlled via DevTools (headless chrome only,\nnot supported on MacOS yet, false by default).",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "newWindow",
+ "description": "Whether to create a new Window or Tab (chrome-only, false by default).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "background",
+ "description": "Whether to create the target in background or foreground (chrome-only,\nfalse by default).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "forTab",
+ "description": "Whether to create the target of type \"tab\".",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetId",
+ "description": "The id of the page opened.",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "detachFromTarget",
+ "description": "Detaches session with given id.",
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Session to detach.",
+ "optional": true,
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "disposeBrowserContext",
+ "description": "Deletes a BrowserContext. All the belonging pages will be closed without calling their\nbeforeunload hooks.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "browserContextId",
+ "$ref": "Browser.BrowserContextID"
+ }
+ ]
+ },
+ {
+ "name": "getTargetInfo",
+ "description": "Returns information about a target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ },
+ {
+ "name": "getTargets",
+ "description": "Retrieves a list of available targets.",
+ "parameters": [
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be reported. If filter is not specified\nand target discovery is currently enabled, a filter used for target discovery\nis used for consistency.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ],
+ "returns": [
+ {
+ "name": "targetInfos",
+ "description": "The list of targets.",
+ "type": "array",
+ "items": {
+ "$ref": "TargetInfo"
+ }
+ }
+ ]
+ },
+ {
+ "name": "sendMessageToTarget",
+ "description": "Sends protocol message over session with given id.\nConsider using flat mode instead; see commands attachToTarget, setAutoAttach,\nand crbug.com/991325.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "message",
+ "type": "string"
+ },
+ {
+ "name": "sessionId",
+ "description": "Identifier of the session.",
+ "optional": true,
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "setAutoAttach",
+ "description": "Controls whether to automatically attach to new targets which are considered to be related to\nthis one. When turned on, attaches to all existing related targets as well. When turned off,\nautomatically detaches from all currently attached targets.\nThis also clears all targets added by `autoAttachRelated` from the list of targets to watch\nfor creation of related targets.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "autoAttach",
+ "description": "Whether to auto-attach to related targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "waitForDebuggerOnStart",
+ "description": "Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\nto run paused targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "flatten",
+ "description": "Enables \"flat\" access to the session via specifying sessionId attribute in the commands.\nWe plan to make this the default, deprecate non-flattened mode,\nand eventually retire it. See crbug.com/991325.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "autoAttachRelated",
+ "description": "Adds the specified target to the list of targets that will be monitored for any related target\ncreation (such as child frames, child workers and new versions of service worker) and reported\nthrough `attachedToTarget`. The specified target is also auto-attached.\nThis cancels the effect of any previous `setAutoAttach` and is also cancelled by subsequent\n`setAutoAttach`. Only available at the Browser target.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "waitForDebuggerOnStart",
+ "description": "Whether to pause new targets when attaching to them. Use `Runtime.runIfWaitingForDebugger`\nto run paused targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "setDiscoverTargets",
+ "description": "Controls whether to discover available targets and notify via\n`targetCreated/targetInfoChanged/targetDestroyed` events.",
+ "parameters": [
+ {
+ "name": "discover",
+ "description": "Whether to discover available targets.",
+ "type": "boolean"
+ },
+ {
+ "name": "filter",
+ "description": "Only targets matching filter will be attached. If `discover` is false,\n`filter` must be omitted or empty.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TargetFilter"
+ }
+ ]
+ },
+ {
+ "name": "setRemoteLocations",
+ "description": "Enables target discovery for the specified locations, when `setDiscoverTargets` was set to\n`true`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "locations",
+ "description": "List of remote locations.",
+ "type": "array",
+ "items": {
+ "$ref": "RemoteLocation"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "attachedToTarget",
+ "description": "Issued when attached to target because of auto-attach or `attachToTarget` command.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Identifier assigned to the session used to send/receive messages.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ },
+ {
+ "name": "waitingForDebugger",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "detachedFromTarget",
+ "description": "Issued when detached from target for any reason (including `detachFromTarget` command). Can be\nissued multiple times per target if multiple sessions have been attached to it.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Detached session identifier.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "receivedMessageFromTarget",
+ "description": "Notifies about a new protocol message received from the session (as reported in\n`attachedToTarget` event).",
+ "parameters": [
+ {
+ "name": "sessionId",
+ "description": "Identifier of a session which sends a message.",
+ "$ref": "SessionID"
+ },
+ {
+ "name": "message",
+ "type": "string"
+ },
+ {
+ "name": "targetId",
+ "description": "Deprecated.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "targetCreated",
+ "description": "Issued when a possible inspection target is created.",
+ "parameters": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ },
+ {
+ "name": "targetDestroyed",
+ "description": "Issued when a target is destroyed.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ }
+ ]
+ },
+ {
+ "name": "targetCrashed",
+ "description": "Issued when a target has crashed.",
+ "parameters": [
+ {
+ "name": "targetId",
+ "$ref": "TargetID"
+ },
+ {
+ "name": "status",
+ "description": "Termination status type.",
+ "type": "string"
+ },
+ {
+ "name": "errorCode",
+ "description": "Termination error code.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "targetInfoChanged",
+ "description": "Issued when some information about a target has changed. This only happens between\n`targetCreated` and `targetDestroyed`.",
+ "parameters": [
+ {
+ "name": "targetInfo",
+ "$ref": "TargetInfo"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Tethering",
+ "description": "The Tethering domain defines methods and events for browser port binding.",
+ "experimental": true,
+ "commands": [
+ {
+ "name": "bind",
+ "description": "Request browser port binding.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number to bind.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "unbind",
+ "description": "Request browser port unbinding.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number to unbind.",
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "accepted",
+ "description": "Informs that port was successfully bound and got a specified connection id.",
+ "parameters": [
+ {
+ "name": "port",
+ "description": "Port number that was successfully bound.",
+ "type": "integer"
+ },
+ {
+ "name": "connectionId",
+ "description": "Connection id to be used.",
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Tracing",
+ "experimental": true,
+ "dependencies": [
+ "IO"
+ ],
+ "types": [
+ {
+ "id": "MemoryDumpConfig",
+ "description": "Configuration for memory dump. Used only when \"memory-infra\" category is enabled.",
+ "type": "object"
+ },
+ {
+ "id": "TraceConfig",
+ "type": "object",
+ "properties": [
+ {
+ "name": "recordMode",
+ "description": "Controls how the trace buffer stores data.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "recordUntilFull",
+ "recordContinuously",
+ "recordAsMuchAsPossible",
+ "echoToConsole"
+ ]
+ },
+ {
+ "name": "traceBufferSizeInKb",
+ "description": "Size of the trace buffer in kilobytes. If not specified or zero is passed, a default value\nof 200 MB would be used.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "enableSampling",
+ "description": "Turns on JavaScript stack sampling.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "enableSystrace",
+ "description": "Turns on system tracing.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "enableArgumentFilter",
+ "description": "Turns on argument filter.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includedCategories",
+ "description": "Included category filters.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "excludedCategories",
+ "description": "Excluded category filters.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "syntheticDelays",
+ "description": "Configuration to synthesize the delays in tracing.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "memoryDumpConfig",
+ "description": "Configuration for memory dump triggers. Used only when \"memory-infra\" category is enabled.",
+ "optional": true,
+ "$ref": "MemoryDumpConfig"
+ }
+ ]
+ },
+ {
+ "id": "StreamFormat",
+ "description": "Data format of a trace. Can be either the legacy JSON format or the\nprotocol buffer format. Note that the JSON format will be deprecated soon.",
+ "type": "string",
+ "enum": [
+ "json",
+ "proto"
+ ]
+ },
+ {
+ "id": "StreamCompression",
+ "description": "Compression type to use for traces returned via streams.",
+ "type": "string",
+ "enum": [
+ "none",
+ "gzip"
+ ]
+ },
+ {
+ "id": "MemoryDumpLevelOfDetail",
+ "description": "Details exposed when memory request explicitly declared.\nKeep consistent with memory_dump_request_args.h and\nmemory_instrumentation.mojom",
+ "type": "string",
+ "enum": [
+ "background",
+ "light",
+ "detailed"
+ ]
+ },
+ {
+ "id": "TracingBackend",
+ "description": "Backend type to use for tracing. `chrome` uses the Chrome-integrated\ntracing service and is supported on all platforms. `system` is only\nsupported on Chrome OS and uses the Perfetto system tracing service.\n`auto` chooses `system` when the perfettoConfig provided to Tracing.start\nspecifies at least one non-Chrome data source; otherwise uses `chrome`.",
+ "type": "string",
+ "enum": [
+ "auto",
+ "chrome",
+ "system"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "end",
+ "description": "Stop trace events collection."
+ },
+ {
+ "name": "getCategories",
+ "description": "Gets supported tracing categories.",
+ "returns": [
+ {
+ "name": "categories",
+ "description": "A list of supported tracing categories.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "recordClockSyncMarker",
+ "description": "Record a clock sync marker in the trace.",
+ "parameters": [
+ {
+ "name": "syncId",
+ "description": "The ID of this clock sync marker",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "requestMemoryDump",
+ "description": "Request a global memory dump.",
+ "parameters": [
+ {
+ "name": "deterministic",
+ "description": "Enables more deterministic results by forcing garbage collection",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "levelOfDetail",
+ "description": "Specifies level of details in memory dump. Defaults to \"detailed\".",
+ "optional": true,
+ "$ref": "MemoryDumpLevelOfDetail"
+ }
+ ],
+ "returns": [
+ {
+ "name": "dumpGuid",
+ "description": "GUID of the resulting global memory dump.",
+ "type": "string"
+ },
+ {
+ "name": "success",
+ "description": "True iff the global memory dump succeeded.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "start",
+ "description": "Start trace events collection.",
+ "parameters": [
+ {
+ "name": "categories",
+ "description": "Category/tag filter",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "options",
+ "description": "Tracing options",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "bufferUsageReportingInterval",
+ "description": "If set, the agent will issue bufferUsage events at this interval, specified in milliseconds",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "transferMode",
+ "description": "Whether to report trace events as series of dataCollected events or to save trace to a\nstream (defaults to `ReportEvents`).",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "ReportEvents",
+ "ReturnAsStream"
+ ]
+ },
+ {
+ "name": "streamFormat",
+ "description": "Trace data format to use. This only applies when using `ReturnAsStream`\ntransfer mode (defaults to `json`).",
+ "optional": true,
+ "$ref": "StreamFormat"
+ },
+ {
+ "name": "streamCompression",
+ "description": "Compression format to use. This only applies when using `ReturnAsStream`\ntransfer mode (defaults to `none`)",
+ "optional": true,
+ "$ref": "StreamCompression"
+ },
+ {
+ "name": "traceConfig",
+ "optional": true,
+ "$ref": "TraceConfig"
+ },
+ {
+ "name": "perfettoConfig",
+ "description": "Base64-encoded serialized perfetto.protos.TraceConfig protobuf message\nWhen specified, the parameters `categories`, `options`, `traceConfig`\nare ignored. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "tracingBackend",
+ "description": "Backend type (defaults to `auto`)",
+ "optional": true,
+ "$ref": "TracingBackend"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "bufferUsage",
+ "parameters": [
+ {
+ "name": "percentFull",
+ "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "eventCount",
+ "description": "An approximate number of events in the trace log.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "value",
+ "description": "A number in range [0..1] that indicates the used size of event buffer as a fraction of its\ntotal size.",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "dataCollected",
+ "description": "Contains a bucket of collected trace events. When tracing is stopped collected events will be\nsent as a sequence of dataCollected events followed by tracingComplete event.",
+ "parameters": [
+ {
+ "name": "value",
+ "type": "array",
+ "items": {
+ "type": "object"
+ }
+ }
+ ]
+ },
+ {
+ "name": "tracingComplete",
+ "description": "Signals that tracing is stopped and there is no trace buffers pending flush, all data were\ndelivered via dataCollected events.",
+ "parameters": [
+ {
+ "name": "dataLossOccurred",
+ "description": "Indicates whether some trace data is known to have been lost, e.g. because the trace ring\nbuffer wrapped around.",
+ "type": "boolean"
+ },
+ {
+ "name": "stream",
+ "description": "A handle of the stream that holds resulting trace data.",
+ "optional": true,
+ "$ref": "IO.StreamHandle"
+ },
+ {
+ "name": "traceFormat",
+ "description": "Trace data format of returned stream.",
+ "optional": true,
+ "$ref": "StreamFormat"
+ },
+ {
+ "name": "streamCompression",
+ "description": "Compression format of returned stream.",
+ "optional": true,
+ "$ref": "StreamCompression"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Fetch",
+ "description": "A domain for letting clients substitute browser's network layer with client code.",
+ "dependencies": [
+ "Network",
+ "IO",
+ "Page"
+ ],
+ "types": [
+ {
+ "id": "RequestId",
+ "description": "Unique request identifier.",
+ "type": "string"
+ },
+ {
+ "id": "RequestStage",
+ "description": "Stages of the request to handle. Request will intercept before the request is\nsent. Response will intercept after the response is received (but before response\nbody is received).",
+ "type": "string",
+ "enum": [
+ "Request",
+ "Response"
+ ]
+ },
+ {
+ "id": "RequestPattern",
+ "type": "object",
+ "properties": [
+ {
+ "name": "urlPattern",
+ "description": "Wildcards (`'*'` -> zero or more, `'?'` -> exactly one) are allowed. Escape character is\nbackslash. Omitting is equivalent to `\"*\"`.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "resourceType",
+ "description": "If set, only requests for matching resource types will be intercepted.",
+ "optional": true,
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "requestStage",
+ "description": "Stage at which to begin intercepting requests. Default is Request.",
+ "optional": true,
+ "$ref": "RequestStage"
+ }
+ ]
+ },
+ {
+ "id": "HeaderEntry",
+ "description": "Response HTTP header entry",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallenge",
+ "description": "Authorization challenge for HTTP status code 401 or 407.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Source of the authentication challenge.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "Server",
+ "Proxy"
+ ]
+ },
+ {
+ "name": "origin",
+ "description": "Origin of the challenger.",
+ "type": "string"
+ },
+ {
+ "name": "scheme",
+ "description": "The authentication scheme used, such as basic or digest",
+ "type": "string"
+ },
+ {
+ "name": "realm",
+ "description": "The realm of the challenge. May be empty.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "AuthChallengeResponse",
+ "description": "Response to an AuthChallenge.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "response",
+ "description": "The decision on what to do in response to the authorization challenge. Default means\ndeferring to the default behavior of the net stack, which will likely either the Cancel\nauthentication or display a popup dialog box.",
+ "type": "string",
+ "enum": [
+ "Default",
+ "CancelAuth",
+ "ProvideCredentials"
+ ]
+ },
+ {
+ "name": "username",
+ "description": "The username to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "password",
+ "description": "The password to provide, possibly empty. Should only be set if response is\nProvideCredentials.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable",
+ "description": "Disables the fetch domain."
+ },
+ {
+ "name": "enable",
+ "description": "Enables issuing of requestPaused events. A request will be paused until client\ncalls one of failRequest, fulfillRequest or continueRequest/continueWithAuth.",
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "If specified, only requests matching any of these patterns will produce\nfetchRequested event and will be paused until clients response. If not set,\nall requests will be affected.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "RequestPattern"
+ }
+ },
+ {
+ "name": "handleAuthRequests",
+ "description": "If true, authRequired events will be issued and requests will be paused\nexpecting a call to continueWithAuth.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "failRequest",
+ "description": "Causes the request to fail with specified reason.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "errorReason",
+ "description": "Causes the request to fail with the given reason.",
+ "$ref": "Network.ErrorReason"
+ }
+ ]
+ },
+ {
+ "name": "fulfillRequest",
+ "description": "Provides response to the request.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "responseCode",
+ "description": "An HTTP response code.",
+ "type": "integer"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "binaryResponseHeaders",
+ "description": "Alternative way of specifying response headers as a \\0-separated\nseries of name: value pairs. Prefer the above method unless you\nneed to represent some non-UTF8 values that can't be transmitted\nover the protocol as text. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "body",
+ "description": "A response body. If absent, original response body will be used if\nthe request is intercepted at the response stage and empty body\nwill be used if the request is intercepted at the request stage. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responsePhrase",
+ "description": "A textual representation of responseCode.\nIf absent, a standard phrase matching responseCode is used.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "continueRequest",
+ "description": "Continues the request, optionally modifying some of its parameters.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "url",
+ "description": "If set, the request url will be modified in a way that's not observable by page.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "method",
+ "description": "If set, the request method is overridden.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "postData",
+ "description": "If set, overrides the post data in the request. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "headers",
+ "description": "If set, overrides the request headers. Note that the overrides do not\nextend to subsequent redirect hops, if a redirect happens. Another override\nmay be applied to a different request produced by a redirect.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "interceptResponse",
+ "description": "If set, overrides response interception behavior for this request.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "continueWithAuth",
+ "description": "Continues a request supplying authChallengeResponse following authRequired event.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in authRequired event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "authChallengeResponse",
+ "description": "Response to with an authChallenge.",
+ "$ref": "AuthChallengeResponse"
+ }
+ ]
+ },
+ {
+ "name": "continueResponse",
+ "description": "Continues loading of the paused response, optionally modifying the\nresponse headers. If either responseCode or headers are modified, all of them\nmust be present.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "An id the client received in requestPaused event.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "responseCode",
+ "description": "An HTTP response code. If absent, original response code will be used.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responsePhrase",
+ "description": "A textual representation of responseCode.\nIf absent, a standard phrase matching responseCode is used.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers. If absent, original response headers will be used.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "binaryResponseHeaders",
+ "description": "Alternative way of specifying response headers as a \\0-separated\nseries of name: value pairs. Prefer the above method unless you\nneed to represent some non-UTF8 values that can't be transmitted\nover the protocol as text. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getResponseBody",
+ "description": "Causes the body of the response to be received from the server and\nreturned as a single string. May only be issued for a request that\nis paused in the Response stage and is mutually exclusive with\ntakeResponseBodyForInterceptionAsStream. Calling other methods that\naffect the request or disabling fetch domain before body is received\nresults in an undefined behavior.\nNote that the response body is not available for redirects. Requests\npaused in the _redirect received_ state may be differentiated by\n`responseCode` and presence of `location` response header, see\ncomments to `requestPaused` for details.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Identifier for the intercepted request to get body for.",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "body",
+ "description": "Response body.",
+ "type": "string"
+ },
+ {
+ "name": "base64Encoded",
+ "description": "True, if content was sent as base64.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeResponseBodyAsStream",
+ "description": "Returns a handle to the stream representing the response body.\nThe request must be paused in the HeadersReceived stage.\nNote that after this command the request can't be continued\nas is -- client either needs to cancel it or to provide the\nresponse body.\nThe stream only supports sequential read, IO.read will fail if the position\nis specified.\nThis method is mutually exclusive with getResponseBody.\nCalling other methods that affect the request or disabling fetch\ndomain before body is received results in an undefined behavior.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "$ref": "RequestId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stream",
+ "$ref": "IO.StreamHandle"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "requestPaused",
+ "description": "Issued when the domain is enabled and the request URL matches the\nspecified filter. The request is paused until the client responds\nwith one of continueRequest, failRequest or fulfillRequest.\nThe stage of the request can be determined by presence of responseErrorReason\nand responseStatusCode -- the request is at the response stage if either\nof these fields is present and in the request stage otherwise.\nRedirect responses and subsequent requests are reported similarly to regular\nresponses and requests. Redirect responses may be distinguished by the value\nof `responseStatusCode` (which is one of 301, 302, 303, 307, 308) along with\npresence of the `location` header. Requests resulting from a redirect will\nhave `redirectedRequestId` field set.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Each request the page makes will have a unique id.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "request",
+ "description": "The details of the request.",
+ "$ref": "Network.Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "responseErrorReason",
+ "description": "Response error if intercepted at response stage.",
+ "optional": true,
+ "$ref": "Network.ErrorReason"
+ },
+ {
+ "name": "responseStatusCode",
+ "description": "Response code if intercepted at response stage.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "responseStatusText",
+ "description": "Response status text if intercepted at response stage.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "responseHeaders",
+ "description": "Response headers if intercepted at the response stage.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "HeaderEntry"
+ }
+ },
+ {
+ "name": "networkId",
+ "description": "If the intercepted request had a corresponding Network.requestWillBeSent event fired for it,\nthen this networkId will be the same as the requestId present in the requestWillBeSent event.",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "redirectedRequestId",
+ "description": "If the request is due to a redirect response from the server, the id of the request that\nhas caused the redirect.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "RequestId"
+ }
+ ]
+ },
+ {
+ "name": "authRequired",
+ "description": "Issued when the domain is enabled with handleAuthRequests set to true.\nThe request is paused until client responds with continueWithAuth.",
+ "parameters": [
+ {
+ "name": "requestId",
+ "description": "Each request the page makes will have a unique id.",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "request",
+ "description": "The details of the request.",
+ "$ref": "Network.Request"
+ },
+ {
+ "name": "frameId",
+ "description": "The id of the frame that initiated the request.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "resourceType",
+ "description": "How the requested resource will be used.",
+ "$ref": "Network.ResourceType"
+ },
+ {
+ "name": "authChallenge",
+ "description": "Details of the Authorization Challenge encountered.\nIf this is set, client should respond with continueRequest that\ncontains AuthChallengeResponse.",
+ "$ref": "AuthChallenge"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "WebAudio",
+ "description": "This domain allows inspection of Web Audio API.\nhttps://webaudio.github.io/web-audio-api/",
+ "experimental": true,
+ "types": [
+ {
+ "id": "GraphObjectId",
+ "description": "An unique ID for a graph object (AudioContext, AudioNode, AudioParam) in Web Audio API",
+ "type": "string"
+ },
+ {
+ "id": "ContextType",
+ "description": "Enum of BaseAudioContext types",
+ "type": "string",
+ "enum": [
+ "realtime",
+ "offline"
+ ]
+ },
+ {
+ "id": "ContextState",
+ "description": "Enum of AudioContextState from the spec",
+ "type": "string",
+ "enum": [
+ "suspended",
+ "running",
+ "closed"
+ ]
+ },
+ {
+ "id": "NodeType",
+ "description": "Enum of AudioNode types",
+ "type": "string"
+ },
+ {
+ "id": "ChannelCountMode",
+ "description": "Enum of AudioNode::ChannelCountMode from the spec",
+ "type": "string",
+ "enum": [
+ "clamped-max",
+ "explicit",
+ "max"
+ ]
+ },
+ {
+ "id": "ChannelInterpretation",
+ "description": "Enum of AudioNode::ChannelInterpretation from the spec",
+ "type": "string",
+ "enum": [
+ "discrete",
+ "speakers"
+ ]
+ },
+ {
+ "id": "ParamType",
+ "description": "Enum of AudioParam types",
+ "type": "string"
+ },
+ {
+ "id": "AutomationRate",
+ "description": "Enum of AudioParam::AutomationRate from the spec",
+ "type": "string",
+ "enum": [
+ "a-rate",
+ "k-rate"
+ ]
+ },
+ {
+ "id": "ContextRealtimeData",
+ "description": "Fields in AudioContext that change in real-time.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "currentTime",
+ "description": "The current context time in second in BaseAudioContext.",
+ "type": "number"
+ },
+ {
+ "name": "renderCapacity",
+ "description": "The time spent on rendering graph divided by render quantum duration,\nand multiplied by 100. 100 means the audio renderer reached the full\ncapacity and glitch may occur.",
+ "type": "number"
+ },
+ {
+ "name": "callbackIntervalMean",
+ "description": "A running mean of callback interval.",
+ "type": "number"
+ },
+ {
+ "name": "callbackIntervalVariance",
+ "description": "A running variance of callback interval.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "BaseAudioContext",
+ "description": "Protocol object for BaseAudioContext",
+ "type": "object",
+ "properties": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextType",
+ "$ref": "ContextType"
+ },
+ {
+ "name": "contextState",
+ "$ref": "ContextState"
+ },
+ {
+ "name": "realtimeData",
+ "optional": true,
+ "$ref": "ContextRealtimeData"
+ },
+ {
+ "name": "callbackBufferSize",
+ "description": "Platform-dependent callback buffer size.",
+ "type": "number"
+ },
+ {
+ "name": "maxOutputChannelCount",
+ "description": "Number of output channels supported by audio hardware in use.",
+ "type": "number"
+ },
+ {
+ "name": "sampleRate",
+ "description": "Context sample rate.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "AudioListener",
+ "description": "Protocol object for AudioListener",
+ "type": "object",
+ "properties": [
+ {
+ "name": "listenerId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "id": "AudioNode",
+ "description": "Protocol object for AudioNode",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeType",
+ "$ref": "NodeType"
+ },
+ {
+ "name": "numberOfInputs",
+ "type": "number"
+ },
+ {
+ "name": "numberOfOutputs",
+ "type": "number"
+ },
+ {
+ "name": "channelCount",
+ "type": "number"
+ },
+ {
+ "name": "channelCountMode",
+ "$ref": "ChannelCountMode"
+ },
+ {
+ "name": "channelInterpretation",
+ "$ref": "ChannelInterpretation"
+ }
+ ]
+ },
+ {
+ "id": "AudioParam",
+ "description": "Protocol object for AudioParam",
+ "type": "object",
+ "properties": [
+ {
+ "name": "paramId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "paramType",
+ "$ref": "ParamType"
+ },
+ {
+ "name": "rate",
+ "$ref": "AutomationRate"
+ },
+ {
+ "name": "defaultValue",
+ "type": "number"
+ },
+ {
+ "name": "minValue",
+ "type": "number"
+ },
+ {
+ "name": "maxValue",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enables the WebAudio domain and starts sending context lifetime events."
+ },
+ {
+ "name": "disable",
+ "description": "Disables the WebAudio domain."
+ },
+ {
+ "name": "getRealtimeData",
+ "description": "Fetch the realtime data from the registered contexts.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "realtimeData",
+ "$ref": "ContextRealtimeData"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "contextCreated",
+ "description": "Notifies that a new BaseAudioContext has been created.",
+ "parameters": [
+ {
+ "name": "context",
+ "$ref": "BaseAudioContext"
+ }
+ ]
+ },
+ {
+ "name": "contextWillBeDestroyed",
+ "description": "Notifies that an existing BaseAudioContext will be destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "contextChanged",
+ "description": "Notifies that existing BaseAudioContext has changed some properties (id stays the same)..",
+ "parameters": [
+ {
+ "name": "context",
+ "$ref": "BaseAudioContext"
+ }
+ ]
+ },
+ {
+ "name": "audioListenerCreated",
+ "description": "Notifies that the construction of an AudioListener has finished.",
+ "parameters": [
+ {
+ "name": "listener",
+ "$ref": "AudioListener"
+ }
+ ]
+ },
+ {
+ "name": "audioListenerWillBeDestroyed",
+ "description": "Notifies that a new AudioListener has been created.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "listenerId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "audioNodeCreated",
+ "description": "Notifies that a new AudioNode has been created.",
+ "parameters": [
+ {
+ "name": "node",
+ "$ref": "AudioNode"
+ }
+ ]
+ },
+ {
+ "name": "audioNodeWillBeDestroyed",
+ "description": "Notifies that an existing AudioNode has been destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "audioParamCreated",
+ "description": "Notifies that a new AudioParam has been created.",
+ "parameters": [
+ {
+ "name": "param",
+ "$ref": "AudioParam"
+ }
+ ]
+ },
+ {
+ "name": "audioParamWillBeDestroyed",
+ "description": "Notifies that an existing AudioParam has been destroyed.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "nodeId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "paramId",
+ "$ref": "GraphObjectId"
+ }
+ ]
+ },
+ {
+ "name": "nodesConnected",
+ "description": "Notifies that two AudioNodes are connected.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "destinationInputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodesDisconnected",
+ "description": "Notifies that AudioNodes are disconnected. The destination can be null, and it means all the outgoing connections from the source are disconnected.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "destinationInputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodeParamConnected",
+ "description": "Notifies that an AudioNode is connected to an AudioParam.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "nodeParamDisconnected",
+ "description": "Notifies that an AudioNode is disconnected to an AudioParam.",
+ "parameters": [
+ {
+ "name": "contextId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "destinationId",
+ "$ref": "GraphObjectId"
+ },
+ {
+ "name": "sourceOutputIndex",
+ "optional": true,
+ "type": "number"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "WebAuthn",
+ "description": "This domain allows configuring virtual authenticators to test the WebAuthn\nAPI.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "AuthenticatorId",
+ "type": "string"
+ },
+ {
+ "id": "AuthenticatorProtocol",
+ "type": "string",
+ "enum": [
+ "u2f",
+ "ctap2"
+ ]
+ },
+ {
+ "id": "Ctap2Version",
+ "type": "string",
+ "enum": [
+ "ctap2_0",
+ "ctap2_1"
+ ]
+ },
+ {
+ "id": "AuthenticatorTransport",
+ "type": "string",
+ "enum": [
+ "usb",
+ "nfc",
+ "ble",
+ "cable",
+ "internal"
+ ]
+ },
+ {
+ "id": "VirtualAuthenticatorOptions",
+ "type": "object",
+ "properties": [
+ {
+ "name": "protocol",
+ "$ref": "AuthenticatorProtocol"
+ },
+ {
+ "name": "ctap2Version",
+ "description": "Defaults to ctap2_0. Ignored if |protocol| == u2f.",
+ "optional": true,
+ "$ref": "Ctap2Version"
+ },
+ {
+ "name": "transport",
+ "$ref": "AuthenticatorTransport"
+ },
+ {
+ "name": "hasResidentKey",
+ "description": "Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasUserVerification",
+ "description": "Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasLargeBlob",
+ "description": "If set to true, the authenticator will support the largeBlob extension.\nhttps://w3c.github.io/webauthn#largeBlob\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasCredBlob",
+ "description": "If set to true, the authenticator will support the credBlob extension.\nhttps://fidoalliance.org/specs/fido-v2.1-rd-20201208/fido-client-to-authenticator-protocol-v2.1-rd-20201208.html#sctn-credBlob-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasMinPinLength",
+ "description": "If set to true, the authenticator will support the minPinLength extension.\nhttps://fidoalliance.org/specs/fido-v2.1-ps-20210615/fido-client-to-authenticator-protocol-v2.1-ps-20210615.html#sctn-minpinlength-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "hasPrf",
+ "description": "If set to true, the authenticator will support the prf extension.\nhttps://w3c.github.io/webauthn/#prf-extension\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "automaticPresenceSimulation",
+ "description": "If set to true, tests of user presence will succeed immediately.\nOtherwise, they will not be resolved. Defaults to true.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isUserVerified",
+ "description": "Sets whether User Verification succeeds or fails for an authenticator.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Credential",
+ "type": "object",
+ "properties": [
+ {
+ "name": "credentialId",
+ "type": "string"
+ },
+ {
+ "name": "isResidentCredential",
+ "type": "boolean"
+ },
+ {
+ "name": "rpId",
+ "description": "Relying Party ID the credential is scoped to. Must be set when adding a\ncredential.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "privateKey",
+ "description": "The ECDSA P-256 private key in PKCS#8 format. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ },
+ {
+ "name": "userHandle",
+ "description": "An opaque byte sequence with a maximum size of 64 bytes mapping the\ncredential to a specific user. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "signCount",
+ "description": "Signature counter. This is incremented by one for each successful\nassertion.\nSee https://w3c.github.io/webauthn/#signature-counter",
+ "type": "integer"
+ },
+ {
+ "name": "largeBlob",
+ "description": "The large blob associated with the credential.\nSee https://w3c.github.io/webauthn/#sctn-large-blob-extension (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enable the WebAuthn domain and start intercepting credential storage and\nretrieval with a virtual authenticator.",
+ "parameters": [
+ {
+ "name": "enableUI",
+ "description": "Whether to enable the WebAuthn user interface. Enabling the UI is\nrecommended for debugging and demo purposes, as it is closer to the real\nexperience. Disabling the UI is recommended for automated testing.\nSupported at the embedder's discretion if UI is available.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disable the WebAuthn domain."
+ },
+ {
+ "name": "addVirtualAuthenticator",
+ "description": "Creates and adds a virtual authenticator.",
+ "parameters": [
+ {
+ "name": "options",
+ "$ref": "VirtualAuthenticatorOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "setResponseOverrideBits",
+ "description": "Resets parameters isBogusSignature, isBadUV, isBadUP to false if they are not present.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "isBogusSignature",
+ "description": "If isBogusSignature is set, overrides the signature in the authenticator response to be zero.\nDefaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isBadUV",
+ "description": "If isBadUV is set, overrides the UV bit in the flags in the authenticator response to\nbe zero. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isBadUP",
+ "description": "If isBadUP is set, overrides the UP bit in the flags in the authenticator response to\nbe zero. Defaults to false.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "removeVirtualAuthenticator",
+ "description": "Removes the given authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "addCredential",
+ "description": "Adds the credential to the specified authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "getCredential",
+ "description": "Returns a single credential stored in the given virtual authenticator that\nmatches the credential ID.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credentialId",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "getCredentials",
+ "description": "Returns all the credentials stored in the given virtual authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "credentials",
+ "type": "array",
+ "items": {
+ "$ref": "Credential"
+ }
+ }
+ ]
+ },
+ {
+ "name": "removeCredential",
+ "description": "Removes a credential from the authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credentialId",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "clearCredentials",
+ "description": "Clears all the credentials from the specified device.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ }
+ ]
+ },
+ {
+ "name": "setUserVerified",
+ "description": "Sets whether User Verification succeeds or fails for an authenticator.\nThe default is true.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "isUserVerified",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setAutomaticPresenceSimulation",
+ "description": "Sets whether tests of user presence will succeed immediately (if true) or fail to resolve (if false) for an authenticator.\nThe default is true.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "credentialAdded",
+ "description": "Triggered when a credential is added to an authenticator.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ },
+ {
+ "name": "credentialAsserted",
+ "description": "Triggered when a credential is used in a webauthn assertion.",
+ "parameters": [
+ {
+ "name": "authenticatorId",
+ "$ref": "AuthenticatorId"
+ },
+ {
+ "name": "credential",
+ "$ref": "Credential"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Media",
+ "description": "This domain allows detailed inspection of media elements",
+ "experimental": true,
+ "types": [
+ {
+ "id": "PlayerId",
+ "description": "Players will get an ID that is unique within the agent context.",
+ "type": "string"
+ },
+ {
+ "id": "Timestamp",
+ "type": "number"
+ },
+ {
+ "id": "PlayerMessage",
+ "description": "Have one type per entry in MediaLogRecord::Type\nCorresponds to kMessage",
+ "type": "object",
+ "properties": [
+ {
+ "name": "level",
+ "description": "Keep in sync with MediaLogMessageLevel\nWe are currently keeping the message level 'error' separate from the\nPlayerError type because right now they represent different things,\nthis one being a DVLOG(ERROR) style log message that gets printed\nbased on what log level is selected in the UI, and the other is a\nrepresentation of a media::PipelineStatus object. Soon however we're\ngoing to be moving away from using PipelineStatus for errors and\nintroducing a new error type which should hopefully let us integrate\nthe error log level into the PlayerError type.",
+ "type": "string",
+ "enum": [
+ "error",
+ "warning",
+ "info",
+ "debug"
+ ]
+ },
+ {
+ "name": "message",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerProperty",
+ "description": "Corresponds to kMediaPropertyChange",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerEvent",
+ "description": "Corresponds to kMediaEventTriggered",
+ "type": "object",
+ "properties": [
+ {
+ "name": "timestamp",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "value",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "PlayerErrorSourceLocation",
+ "description": "Represents logged source line numbers reported in an error.\nNOTE: file and line are from chromium c++ implementation code, not js.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "file",
+ "type": "string"
+ },
+ {
+ "name": "line",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "PlayerError",
+ "description": "Corresponds to kMediaError",
+ "type": "object",
+ "properties": [
+ {
+ "name": "errorType",
+ "type": "string"
+ },
+ {
+ "name": "code",
+ "description": "Code is the numeric enum entry for a specific set of error codes, such\nas PipelineStatusCodes in media/base/pipeline_status.h",
+ "type": "integer"
+ },
+ {
+ "name": "stack",
+ "description": "A trace of where this error was caused / where it passed through.",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerErrorSourceLocation"
+ }
+ },
+ {
+ "name": "cause",
+ "description": "Errors potentially have a root cause error, ie, a DecoderError might be\ncaused by an WindowsError",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerError"
+ }
+ },
+ {
+ "name": "data",
+ "description": "Extra data attached to an error, such as an HRESULT, Video Codec, etc.",
+ "type": "object"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "playerPropertiesChanged",
+ "description": "This can be called multiple times, and can be used to set / override /\nremove player properties. A null propValue indicates removal.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "properties",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerProperty"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerEventsAdded",
+ "description": "Send events as a list, allowing them to be batched on the browser for less\ncongestion. If batched, events must ALWAYS be in chronological order.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "events",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerEvent"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerMessagesLogged",
+ "description": "Send a list of any messages that need to be delivered.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "messages",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerMessage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playerErrorsRaised",
+ "description": "Send a list of any errors that need to be delivered.",
+ "parameters": [
+ {
+ "name": "playerId",
+ "$ref": "PlayerId"
+ },
+ {
+ "name": "errors",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerError"
+ }
+ }
+ ]
+ },
+ {
+ "name": "playersCreated",
+ "description": "Called whenever a player is created, or when a new agent joins and receives\na list of active players. If an agent is restored, it will receive the full\nlist of player ids and all events again.",
+ "parameters": [
+ {
+ "name": "players",
+ "type": "array",
+ "items": {
+ "$ref": "PlayerId"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enables the Media domain"
+ },
+ {
+ "name": "disable",
+ "description": "Disables the Media domain."
+ }
+ ]
+ },
+ {
+ "domain": "DeviceAccess",
+ "experimental": true,
+ "types": [
+ {
+ "id": "RequestId",
+ "description": "Device request id.",
+ "type": "string"
+ },
+ {
+ "id": "DeviceId",
+ "description": "A device id.",
+ "type": "string"
+ },
+ {
+ "id": "PromptDevice",
+ "description": "Device information displayed in a user prompt to select a device.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "DeviceId"
+ },
+ {
+ "name": "name",
+ "description": "Display name as it appears in a device request user prompt.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "description": "Enable events in this domain."
+ },
+ {
+ "name": "disable",
+ "description": "Disable events in this domain."
+ },
+ {
+ "name": "selectPrompt",
+ "description": "Select a device in response to a DeviceAccess.deviceRequestPrompted event.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "deviceId",
+ "$ref": "DeviceId"
+ }
+ ]
+ },
+ {
+ "name": "cancelPrompt",
+ "description": "Cancel a prompt in response to a DeviceAccess.deviceRequestPrompted event.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "deviceRequestPrompted",
+ "description": "A device request opened a user prompt to select a device. Respond with the\nselectPrompt or cancelPrompt command.",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RequestId"
+ },
+ {
+ "name": "devices",
+ "type": "array",
+ "items": {
+ "$ref": "PromptDevice"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Preload",
+ "experimental": true,
+ "types": [
+ {
+ "id": "RuleSetId",
+ "description": "Unique id",
+ "type": "string"
+ },
+ {
+ "id": "RuleSet",
+ "description": "Corresponds to SpeculationRuleSet",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "$ref": "RuleSetId"
+ },
+ {
+ "name": "loaderId",
+ "description": "Identifies a document which the rule set is associated with.",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "sourceText",
+ "description": "Source text of JSON representing the rule set. If it comes from\n`<script>` tag, it is the textContent of the node. Note that it is\na JSON for valid case.\n\nSee also:\n- https://wicg.github.io/nav-speculation/speculation-rules.html\n- https://github.com/WICG/nav-speculation/blob/main/triggers.md",
+ "type": "string"
+ },
+ {
+ "name": "backendNodeId",
+ "description": "A speculation rule set is either added through an inline\n`<script>` tag or through an external resource via the\n'Speculation-Rules' HTTP header. For the first case, we include\nthe BackendNodeId of the relevant `<script>` tag. For the second\ncase, we include the external URL where the rule set was loaded\nfrom, and also RequestId if Network domain is enabled.\n\nSee also:\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-script\n- https://wicg.github.io/nav-speculation/speculation-rules.html#speculation-rules-header",
+ "optional": true,
+ "$ref": "DOM.BackendNodeId"
+ },
+ {
+ "name": "url",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "requestId",
+ "optional": true,
+ "$ref": "Network.RequestId"
+ },
+ {
+ "name": "errorType",
+ "description": "Error information\n`errorMessage` is null iff `errorType` is null.",
+ "optional": true,
+ "$ref": "RuleSetErrorType"
+ },
+ {
+ "name": "errorMessage",
+ "description": "TODO(https://crbug.com/1425354): Replace this property with structured error.",
+ "deprecated": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "RuleSetErrorType",
+ "type": "string",
+ "enum": [
+ "SourceIsNotJsonObject",
+ "InvalidRulesSkipped"
+ ]
+ },
+ {
+ "id": "SpeculationAction",
+ "description": "The type of preloading attempted. It corresponds to\nmojom::SpeculationAction (although PrefetchWithSubresources is omitted as it\nisn't being used by clients).",
+ "type": "string",
+ "enum": [
+ "Prefetch",
+ "Prerender"
+ ]
+ },
+ {
+ "id": "SpeculationTargetHint",
+ "description": "Corresponds to mojom::SpeculationTargetHint.\nSee https://github.com/WICG/nav-speculation/blob/main/triggers.md#window-name-targeting-hints",
+ "type": "string",
+ "enum": [
+ "Blank",
+ "Self"
+ ]
+ },
+ {
+ "id": "PreloadingAttemptKey",
+ "description": "A key that identifies a preloading attempt.\n\nThe url used is the url specified by the trigger (i.e. the initial URL), and\nnot the final url that is navigated to. For example, prerendering allows\nsame-origin main frame navigations during the attempt, but the attempt is\nstill keyed with the initial URL.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "action",
+ "$ref": "SpeculationAction"
+ },
+ {
+ "name": "url",
+ "type": "string"
+ },
+ {
+ "name": "targetHint",
+ "optional": true,
+ "$ref": "SpeculationTargetHint"
+ }
+ ]
+ },
+ {
+ "id": "PreloadingAttemptSource",
+ "description": "Lists sources for a preloading attempt, specifically the ids of rule sets\nthat had a speculation rule that triggered the attempt, and the\nBackendNodeIds of <a href> or <area href> elements that triggered the\nattempt (in the case of attempts triggered by a document rule). It is\npossible for mulitple rule sets and links to trigger a single attempt.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "ruleSetIds",
+ "type": "array",
+ "items": {
+ "$ref": "RuleSetId"
+ }
+ },
+ {
+ "name": "nodeIds",
+ "type": "array",
+ "items": {
+ "$ref": "DOM.BackendNodeId"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PrerenderFinalStatus",
+ "description": "List of FinalStatus reasons for Prerender2.",
+ "type": "string",
+ "enum": [
+ "Activated",
+ "Destroyed",
+ "LowEndDevice",
+ "InvalidSchemeRedirect",
+ "InvalidSchemeNavigation",
+ "InProgressNavigation",
+ "NavigationRequestBlockedByCsp",
+ "MainFrameNavigation",
+ "MojoBinderPolicy",
+ "RendererProcessCrashed",
+ "RendererProcessKilled",
+ "Download",
+ "TriggerDestroyed",
+ "NavigationNotCommitted",
+ "NavigationBadHttpStatus",
+ "ClientCertRequested",
+ "NavigationRequestNetworkError",
+ "MaxNumOfRunningPrerendersExceeded",
+ "CancelAllHostsForTesting",
+ "DidFailLoad",
+ "Stop",
+ "SslCertificateError",
+ "LoginAuthRequested",
+ "UaChangeRequiresReload",
+ "BlockedByClient",
+ "AudioOutputDeviceRequested",
+ "MixedContent",
+ "TriggerBackgrounded",
+ "MemoryLimitExceeded",
+ "FailToGetMemoryUsage",
+ "DataSaverEnabled",
+ "HasEffectiveUrl",
+ "ActivatedBeforeStarted",
+ "InactivePageRestriction",
+ "StartFailed",
+ "TimeoutBackgrounded",
+ "CrossSiteRedirectInInitialNavigation",
+ "CrossSiteNavigationInInitialNavigation",
+ "SameSiteCrossOriginRedirectNotOptInInInitialNavigation",
+ "SameSiteCrossOriginNavigationNotOptInInInitialNavigation",
+ "ActivationNavigationParameterMismatch",
+ "ActivatedInBackground",
+ "EmbedderHostDisallowed",
+ "ActivationNavigationDestroyedBeforeSuccess",
+ "TabClosedByUserGesture",
+ "TabClosedWithoutUserGesture",
+ "PrimaryMainFrameRendererProcessCrashed",
+ "PrimaryMainFrameRendererProcessKilled",
+ "ActivationFramePolicyNotCompatible",
+ "PreloadingDisabled",
+ "BatterySaverEnabled",
+ "ActivatedDuringMainFrameNavigation",
+ "PreloadingUnsupportedByWebContents",
+ "CrossSiteRedirectInMainFrameNavigation",
+ "CrossSiteNavigationInMainFrameNavigation",
+ "SameSiteCrossOriginRedirectNotOptInInMainFrameNavigation",
+ "SameSiteCrossOriginNavigationNotOptInInMainFrameNavigation",
+ "MemoryPressureOnTrigger",
+ "MemoryPressureAfterTriggered",
+ "PrerenderingDisabledByDevTools",
+ "ResourceLoadBlockedByClient",
+ "SpeculationRuleRemoved",
+ "ActivatedWithAuxiliaryBrowsingContexts"
+ ]
+ },
+ {
+ "id": "PreloadingStatus",
+ "description": "Preloading status values, see also PreloadingTriggeringOutcome. This\nstatus is shared by prefetchStatusUpdated and prerenderStatusUpdated.",
+ "type": "string",
+ "enum": [
+ "Pending",
+ "Running",
+ "Ready",
+ "Success",
+ "Failure",
+ "NotSupported"
+ ]
+ },
+ {
+ "id": "PrefetchStatus",
+ "description": "TODO(https://crbug.com/1384419): revisit the list of PrefetchStatus and\nfilter out the ones that aren't necessary to the developers.",
+ "type": "string",
+ "enum": [
+ "PrefetchAllowed",
+ "PrefetchFailedIneligibleRedirect",
+ "PrefetchFailedInvalidRedirect",
+ "PrefetchFailedMIMENotSupported",
+ "PrefetchFailedNetError",
+ "PrefetchFailedNon2XX",
+ "PrefetchFailedPerPageLimitExceeded",
+ "PrefetchEvicted",
+ "PrefetchHeldback",
+ "PrefetchIneligibleRetryAfter",
+ "PrefetchIsPrivacyDecoy",
+ "PrefetchIsStale",
+ "PrefetchNotEligibleBrowserContextOffTheRecord",
+ "PrefetchNotEligibleDataSaverEnabled",
+ "PrefetchNotEligibleExistingProxy",
+ "PrefetchNotEligibleHostIsNonUnique",
+ "PrefetchNotEligibleNonDefaultStoragePartition",
+ "PrefetchNotEligibleSameSiteCrossOriginPrefetchRequiredProxy",
+ "PrefetchNotEligibleSchemeIsNotHttps",
+ "PrefetchNotEligibleUserHasCookies",
+ "PrefetchNotEligibleUserHasServiceWorker",
+ "PrefetchNotEligibleBatterySaverEnabled",
+ "PrefetchNotEligiblePreloadingDisabled",
+ "PrefetchNotFinishedInTime",
+ "PrefetchNotStarted",
+ "PrefetchNotUsedCookiesChanged",
+ "PrefetchProxyNotAvailable",
+ "PrefetchResponseUsed",
+ "PrefetchSuccessfulButNotUsed",
+ "PrefetchNotUsedProbeFailed"
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable"
+ },
+ {
+ "name": "disable"
+ }
+ ],
+ "events": [
+ {
+ "name": "ruleSetUpdated",
+ "description": "Upsert. Currently, it is only emitted when a rule set added.",
+ "parameters": [
+ {
+ "name": "ruleSet",
+ "$ref": "RuleSet"
+ }
+ ]
+ },
+ {
+ "name": "ruleSetRemoved",
+ "parameters": [
+ {
+ "name": "id",
+ "$ref": "RuleSetId"
+ }
+ ]
+ },
+ {
+ "name": "prerenderAttemptCompleted",
+ "description": "Fired when a prerender attempt is completed.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "initiatingFrameId",
+ "description": "The frame id of the frame initiating prerendering.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "prerenderingUrl",
+ "type": "string"
+ },
+ {
+ "name": "finalStatus",
+ "$ref": "PrerenderFinalStatus"
+ },
+ {
+ "name": "disallowedApiMethod",
+ "description": "This is used to give users more information about the name of the API call\nthat is incompatible with prerender and has caused the cancellation of the attempt",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preloadEnabledStateUpdated",
+ "description": "Fired when a preload enabled state is updated.",
+ "parameters": [
+ {
+ "name": "disabledByPreference",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByDataSaver",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByBatterySaver",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByHoldbackPrefetchSpeculationRules",
+ "type": "boolean"
+ },
+ {
+ "name": "disabledByHoldbackPrerenderSpeculationRules",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "prefetchStatusUpdated",
+ "description": "Fired when a prefetch attempt is updated.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "initiatingFrameId",
+ "description": "The frame id of the frame initiating prefetch.",
+ "$ref": "Page.FrameId"
+ },
+ {
+ "name": "prefetchUrl",
+ "type": "string"
+ },
+ {
+ "name": "status",
+ "$ref": "PreloadingStatus"
+ },
+ {
+ "name": "prefetchStatus",
+ "$ref": "PrefetchStatus"
+ },
+ {
+ "name": "requestId",
+ "$ref": "Network.RequestId"
+ }
+ ]
+ },
+ {
+ "name": "prerenderStatusUpdated",
+ "description": "Fired when a prerender attempt is updated.",
+ "parameters": [
+ {
+ "name": "key",
+ "$ref": "PreloadingAttemptKey"
+ },
+ {
+ "name": "status",
+ "$ref": "PreloadingStatus"
+ },
+ {
+ "name": "prerenderStatus",
+ "optional": true,
+ "$ref": "PrerenderFinalStatus"
+ },
+ {
+ "name": "disallowedMojoInterface",
+ "description": "This is used to give users more information about the name of Mojo interface\nthat is incompatible with prerender and has caused the cancellation of the attempt.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preloadingAttemptSourcesUpdated",
+ "description": "Send a list of sources for all preloading attempts in a document.",
+ "parameters": [
+ {
+ "name": "loaderId",
+ "$ref": "Network.LoaderId"
+ },
+ {
+ "name": "preloadingAttemptSources",
+ "type": "array",
+ "items": {
+ "$ref": "PreloadingAttemptSource"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "FedCm",
+ "description": "This domain allows interacting with the FedCM dialog.",
+ "experimental": true,
+ "types": [
+ {
+ "id": "LoginState",
+ "description": "Whether this is a sign-up or sign-in action for this account, i.e.\nwhether this account has ever been used to sign in to this RP before.",
+ "type": "string",
+ "enum": [
+ "SignIn",
+ "SignUp"
+ ]
+ },
+ {
+ "id": "DialogType",
+ "description": "Whether the dialog shown is an account chooser or an auto re-authentication dialog.",
+ "type": "string",
+ "enum": [
+ "AccountChooser",
+ "AutoReauthn"
+ ]
+ },
+ {
+ "id": "Account",
+ "description": "Corresponds to IdentityRequestAccount",
+ "type": "object",
+ "properties": [
+ {
+ "name": "accountId",
+ "type": "string"
+ },
+ {
+ "name": "email",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "givenName",
+ "type": "string"
+ },
+ {
+ "name": "pictureUrl",
+ "type": "string"
+ },
+ {
+ "name": "idpConfigUrl",
+ "type": "string"
+ },
+ {
+ "name": "idpSigninUrl",
+ "type": "string"
+ },
+ {
+ "name": "loginState",
+ "$ref": "LoginState"
+ },
+ {
+ "name": "termsOfServiceUrl",
+ "description": "These two are only set if the loginState is signUp",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "privacyPolicyUrl",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "dialogShown",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "dialogType",
+ "$ref": "DialogType"
+ },
+ {
+ "name": "accounts",
+ "type": "array",
+ "items": {
+ "$ref": "Account"
+ }
+ },
+ {
+ "name": "title",
+ "description": "These exist primarily so that the caller can verify the\nRP context was used appropriately.",
+ "type": "string"
+ },
+ {
+ "name": "subtitle",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "enable",
+ "parameters": [
+ {
+ "name": "disableRejectionDelay",
+ "description": "Allows callers to disable the promise rejection delay that would\nnormally happen, if this is unimportant to what's being tested.\n(step 4 of https://fedidcg.github.io/FedCM/#browser-api-rp-sign-in)",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "selectAccount",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "accountIndex",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "dismissDialog",
+ "parameters": [
+ {
+ "name": "dialogId",
+ "type": "string"
+ },
+ {
+ "name": "triggerCooldown",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resetCooldown",
+ "description": "Resets the cooldown time, if any, to allow the next FedCM call to show\na dialog even if one was recently dismissed by the user."
+ }
+ ]
+ },
+ {
+ "domain": "Console",
+ "description": "This domain is deprecated - use Runtime or Log instead.",
+ "deprecated": true,
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "ConsoleMessage",
+ "description": "Console message.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "source",
+ "description": "Message source.",
+ "type": "string",
+ "enum": [
+ "xml",
+ "javascript",
+ "network",
+ "console-api",
+ "storage",
+ "appcache",
+ "rendering",
+ "security",
+ "other",
+ "deprecation",
+ "worker"
+ ]
+ },
+ {
+ "name": "level",
+ "description": "Message severity.",
+ "type": "string",
+ "enum": [
+ "log",
+ "warning",
+ "error",
+ "debug",
+ "info"
+ ]
+ },
+ {
+ "name": "text",
+ "description": "Message text.",
+ "type": "string"
+ },
+ {
+ "name": "url",
+ "description": "URL of the message origin.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "line",
+ "description": "Line number in the resource that generated this message (1-based).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "column",
+ "description": "Column number in the resource that generated this message (1-based).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "clearMessages",
+ "description": "Does nothing."
+ },
+ {
+ "name": "disable",
+ "description": "Disables console domain, prevents further console messages from being reported to the client."
+ },
+ {
+ "name": "enable",
+ "description": "Enables console domain, sends the messages collected so far to the client by means of the\n`messageAdded` notification."
+ }
+ ],
+ "events": [
+ {
+ "name": "messageAdded",
+ "description": "Issued when new console message is added.",
+ "parameters": [
+ {
+ "name": "message",
+ "description": "Console message that has been added.",
+ "$ref": "ConsoleMessage"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Debugger",
+ "description": "Debugger domain exposes JavaScript debugging capabilities. It allows setting and removing\nbreakpoints, stepping through execution, exploring stack traces, etc.",
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "BreakpointId",
+ "description": "Breakpoint identifier.",
+ "type": "string"
+ },
+ {
+ "id": "CallFrameId",
+ "description": "Call frame identifier.",
+ "type": "string"
+ },
+ {
+ "id": "Location",
+ "description": "Location in the source code.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script identifier as reported in the `Debugger.scriptParsed`.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "ScriptPosition",
+ "description": "Location in the source code.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "lineNumber",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "LocationRange",
+ "description": "Location range within one script.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "start",
+ "$ref": "ScriptPosition"
+ },
+ {
+ "name": "end",
+ "$ref": "ScriptPosition"
+ }
+ ]
+ },
+ {
+ "id": "CallFrame",
+ "description": "JavaScript call frame. Array of call frames form the call stack.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier. This identifier is only valid while the virtual machine is paused.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "functionName",
+ "description": "Name of the JavaScript function called on this call frame.",
+ "type": "string"
+ },
+ {
+ "name": "functionLocation",
+ "description": "Location in the source code.",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "location",
+ "description": "Location in the source code.",
+ "$ref": "Location"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.\nDeprecated in favor of using the `location.scriptId` to resolve the URL via a previously\nsent `Debugger.scriptParsed` event.",
+ "deprecated": true,
+ "type": "string"
+ },
+ {
+ "name": "scopeChain",
+ "description": "Scope chain for this call frame.",
+ "type": "array",
+ "items": {
+ "$ref": "Scope"
+ }
+ },
+ {
+ "name": "this",
+ "description": "`this` object for this call frame.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "returnValue",
+ "description": "The value being returned, if the function is at return point.",
+ "optional": true,
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "canBeRestarted",
+ "description": "Valid only while the VM is paused and indicates whether this frame\ncan be restarted or not. Note that a `true` value here does not\nguarantee that Debugger#restartFrame with this CallFrameId will be\nsuccessful, but it is very likely.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "Scope",
+ "description": "Scope description.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Scope type.",
+ "type": "string",
+ "enum": [
+ "global",
+ "local",
+ "with",
+ "closure",
+ "catch",
+ "block",
+ "script",
+ "eval",
+ "module",
+ "wasm-expression-stack"
+ ]
+ },
+ {
+ "name": "object",
+ "description": "Object representing the scope. For `global` and `with` scopes it represents the actual\nobject; for the rest of the scopes, it is artificial transient object enumerating scope\nvariables as its properties.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "name",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "startLocation",
+ "description": "Location in the source code where scope starts",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "endLocation",
+ "description": "Location in the source code where scope ends",
+ "optional": true,
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "id": "SearchMatch",
+ "description": "Search match for resource.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "lineNumber",
+ "description": "Line number in resource content.",
+ "type": "number"
+ },
+ {
+ "name": "lineContent",
+ "description": "Line with match content.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "id": "BreakLocation",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "Script identifier as reported in the `Debugger.scriptParsed`.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number in the script (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number in the script (0-based).",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "type",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "debuggerStatement",
+ "call",
+ "return"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "WasmDisassemblyChunk",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "lines",
+ "description": "The next chunk of disassembled lines.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "bytecodeOffsets",
+ "description": "The bytecode offsets describing the start of each line.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "ScriptLanguage",
+ "description": "Enum of possible script languages.",
+ "type": "string",
+ "enum": [
+ "JavaScript",
+ "WebAssembly"
+ ]
+ },
+ {
+ "id": "DebugSymbols",
+ "description": "Debug symbols available for a wasm script.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Type of the debug symbols.",
+ "type": "string",
+ "enum": [
+ "None",
+ "SourceMap",
+ "EmbeddedDWARF",
+ "ExternalDWARF"
+ ]
+ },
+ {
+ "name": "externalURL",
+ "description": "URL of the external symbol source.",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "continueToLocation",
+ "description": "Continues execution until specific location is reached.",
+ "parameters": [
+ {
+ "name": "location",
+ "description": "Location to continue to.",
+ "$ref": "Location"
+ },
+ {
+ "name": "targetCallFrames",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "any",
+ "current"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables debugger for given page."
+ },
+ {
+ "name": "enable",
+ "description": "Enables debugger for the given page. Clients should not assume that the debugging has been\nenabled until the result for this command is received.",
+ "parameters": [
+ {
+ "name": "maxScriptsCacheSize",
+ "description": "The maximum size in bytes of collected scripts (not referenced by other heap objects)\nthe debugger can hold. Puts no limit if parameter is omitted.",
+ "experimental": true,
+ "optional": true,
+ "type": "number"
+ }
+ ],
+ "returns": [
+ {
+ "name": "debuggerId",
+ "description": "Unique identifier of the debugger.",
+ "experimental": true,
+ "$ref": "Runtime.UniqueDebuggerId"
+ }
+ ]
+ },
+ {
+ "name": "evaluateOnCallFrame",
+ "description": "Evaluates expression on a given call frame.",
+ "parameters": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier to evaluate on.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "expression",
+ "description": "Expression to evaluate.",
+ "type": "string"
+ },
+ {
+ "name": "objectGroup",
+ "description": "String object group name to put result into (allows rapid releasing resulting object handles\nusing `releaseObjectGroup`).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Specifies whether command line API should be available to the evaluated expression, defaults\nto false.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "timeout",
+ "description": "Terminate execution after timing out (number of milliseconds).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.TimeDelta"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Object wrapper for the evaluation result.",
+ "$ref": "Runtime.RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "Runtime.ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "getPossibleBreakpoints",
+ "description": "Returns possible locations for breakpoint. scriptId in start and end range locations should be\nthe same.",
+ "parameters": [
+ {
+ "name": "start",
+ "description": "Start of range to search possible breakpoint locations in.",
+ "$ref": "Location"
+ },
+ {
+ "name": "end",
+ "description": "End of range to search possible breakpoint locations in (excluding). When not specified, end\nof scripts is used as end of range.",
+ "optional": true,
+ "$ref": "Location"
+ },
+ {
+ "name": "restrictToFunction",
+ "description": "Only consider locations which are in the same (non-nested) function as start.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "locations",
+ "description": "List of the possible breakpoint locations.",
+ "type": "array",
+ "items": {
+ "$ref": "BreakLocation"
+ }
+ }
+ ]
+ },
+ {
+ "name": "getScriptSource",
+ "description": "Returns source for the script with given id.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to get source for.",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scriptSource",
+ "description": "Script source (empty in case of Wasm bytecode).",
+ "type": "string"
+ },
+ {
+ "name": "bytecode",
+ "description": "Wasm bytecode. (Encoded as a base64 string when passed over JSON)",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "disassembleWasmModule",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to disassemble",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "streamId",
+ "description": "For large modules, return a stream from which additional chunks of\ndisassembly can be read successively.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "totalNumberOfLines",
+ "description": "The total number of lines in the disassembly text.",
+ "type": "integer"
+ },
+ {
+ "name": "functionBodyOffsets",
+ "description": "The offsets of all function bodies, in the format [start1, end1,\nstart2, end2, ...] where all ends are exclusive.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "chunk",
+ "description": "The first chunk of disassembly.",
+ "$ref": "WasmDisassemblyChunk"
+ }
+ ]
+ },
+ {
+ "name": "nextWasmDisassemblyChunk",
+ "description": "Disassemble the next chunk of lines for the module corresponding to the\nstream. If disassembly is complete, this API will invalidate the streamId\nand return an empty chunk. Any subsequent calls for the now invalid stream\nwill return errors.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "streamId",
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "chunk",
+ "description": "The next chunk of disassembly.",
+ "$ref": "WasmDisassemblyChunk"
+ }
+ ]
+ },
+ {
+ "name": "getWasmBytecode",
+ "description": "This command is deprecated. Use getScriptSource instead.",
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the Wasm script to get source for.",
+ "$ref": "Runtime.ScriptId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "bytecode",
+ "description": "Script source. (Encoded as a base64 string when passed over JSON)",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getStackTrace",
+ "description": "Returns stack trace with given `stackTraceId`.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "stackTraceId",
+ "$ref": "Runtime.StackTraceId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "stackTrace",
+ "$ref": "Runtime.StackTrace"
+ }
+ ]
+ },
+ {
+ "name": "pause",
+ "description": "Stops on the next JavaScript statement."
+ },
+ {
+ "name": "pauseOnAsyncCall",
+ "experimental": true,
+ "deprecated": true,
+ "parameters": [
+ {
+ "name": "parentStackTraceId",
+ "description": "Debugger will pause when async call with given stack trace is started.",
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "removeBreakpoint",
+ "description": "Removes JavaScript breakpoint.",
+ "parameters": [
+ {
+ "name": "breakpointId",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "restartFrame",
+ "description": "Restarts particular call frame from the beginning. The old, deprecated\nbehavior of `restartFrame` is to stay paused and allow further CDP commands\nafter a restart was scheduled. This can cause problems with restarting, so\nwe now continue execution immediatly after it has been scheduled until we\nreach the beginning of the restarted frame.\n\nTo stay back-wards compatible, `restartFrame` now expects a `mode`\nparameter to be present. If the `mode` parameter is missing, `restartFrame`\nerrors out.\n\nThe various return values are deprecated and `callFrames` is always empty.\nUse the call frames from the `Debugger#paused` events instead, that fires\nonce V8 pauses at the beginning of the restarted function.",
+ "parameters": [
+ {
+ "name": "callFrameId",
+ "description": "Call frame identifier to evaluate on.",
+ "$ref": "CallFrameId"
+ },
+ {
+ "name": "mode",
+ "description": "The `mode` parameter must be present and set to 'StepInto', otherwise\n`restartFrame` will error out.",
+ "experimental": true,
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "StepInto"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "callFrames",
+ "description": "New stack trace.",
+ "deprecated": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "resume",
+ "description": "Resumes JavaScript execution.",
+ "parameters": [
+ {
+ "name": "terminateOnResume",
+ "description": "Set to true to terminate execution upon resuming execution. In contrast\nto Runtime.terminateExecution, this will allows to execute further\nJavaScript (i.e. via evaluation) until execution of the paused code\nis actually resumed, at which point termination is triggered.\nIf execution is currently not paused, this parameter has no effect.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "searchInContent",
+ "description": "Searches for given string in script content.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to search in.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "query",
+ "description": "String to search for.",
+ "type": "string"
+ },
+ {
+ "name": "caseSensitive",
+ "description": "If true, search is case sensitive.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isRegex",
+ "description": "If true, treats string parameter as regex.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "List of search matches.",
+ "type": "array",
+ "items": {
+ "$ref": "SearchMatch"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setAsyncCallStackDepth",
+ "description": "Enables or disables async call stacks tracking.",
+ "parameters": [
+ {
+ "name": "maxDepth",
+ "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setBlackboxPatterns",
+ "description": "Replace previous blackbox patterns with passed ones. Forces backend to skip stepping/pausing in\nscripts with url matching one of the patterns. VM will try to leave blackboxed script by\nperforming 'step in' several times, finally resorting to 'step out' if unsuccessful.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "patterns",
+ "description": "Array of regexps that will be used to check script url for blackbox state.",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBlackboxedRanges",
+ "description": "Makes backend skip steps in the script in blackboxed ranges. VM will try leave blacklisted\nscripts by performing 'step in' several times, finally resorting to 'step out' if unsuccessful.\nPositions array contains positions where blackbox state is changed. First interval isn't\nblackboxed. Array should be sorted.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "positions",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptPosition"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBreakpoint",
+ "description": "Sets JavaScript breakpoint at a given location.",
+ "parameters": [
+ {
+ "name": "location",
+ "description": "Location to set breakpoint in.",
+ "$ref": "Location"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "actualLocation",
+ "description": "Location this breakpoint resolved into.",
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "name": "setInstrumentationBreakpoint",
+ "description": "Sets instrumentation breakpoint.",
+ "parameters": [
+ {
+ "name": "instrumentation",
+ "description": "Instrumentation name.",
+ "type": "string",
+ "enum": [
+ "beforeScriptExecution",
+ "beforeScriptWithSourceMapExecution"
+ ]
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointByUrl",
+ "description": "Sets JavaScript breakpoint at given location specified either by URL or URL regex. Once this\ncommand is issued, all existing parsed scripts will have breakpoints resolved and returned in\n`locations` property. Further matching script parsing will result in subsequent\n`breakpointResolved` events issued. This logical breakpoint will survive page reloads.",
+ "parameters": [
+ {
+ "name": "lineNumber",
+ "description": "Line number to set breakpoint at.",
+ "type": "integer"
+ },
+ {
+ "name": "url",
+ "description": "URL of the resources to set breakpoint on.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "urlRegex",
+ "description": "Regex pattern for the URLs of the resources to set breakpoints on. Either `url` or\n`urlRegex` must be specified.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "scriptHash",
+ "description": "Script hash of the resources to set breakpoint on.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Offset in the line to set breakpoint at.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will only stop on the\nbreakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "locations",
+ "description": "List of the locations this breakpoint resolved into upon addition.",
+ "type": "array",
+ "items": {
+ "$ref": "Location"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointOnFunctionCall",
+ "description": "Sets JavaScript breakpoint before each call to the given function.\nIf another function was created from the same source as a given one,\ncalling it will also trigger the breakpoint.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Function object id.",
+ "$ref": "Runtime.RemoteObjectId"
+ },
+ {
+ "name": "condition",
+ "description": "Expression to use as a breakpoint condition. When specified, debugger will\nstop on the breakpoint if this expression evaluates to true.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "breakpointId",
+ "description": "Id of the created breakpoint for further reference.",
+ "$ref": "BreakpointId"
+ }
+ ]
+ },
+ {
+ "name": "setBreakpointsActive",
+ "description": "Activates / deactivates all breakpoints on the page.",
+ "parameters": [
+ {
+ "name": "active",
+ "description": "New value for breakpoints active state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setPauseOnExceptions",
+ "description": "Defines pause on exceptions state. Can be set to stop on all exceptions, uncaught exceptions,\nor caught exceptions, no exceptions. Initial pause on exceptions state is `none`.",
+ "parameters": [
+ {
+ "name": "state",
+ "description": "Pause on exceptions mode.",
+ "type": "string",
+ "enum": [
+ "none",
+ "caught",
+ "uncaught",
+ "all"
+ ]
+ }
+ ]
+ },
+ {
+ "name": "setReturnValue",
+ "description": "Changes return value in top frame. Available only at return break position.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "newValue",
+ "description": "New return value.",
+ "$ref": "Runtime.CallArgument"
+ }
+ ]
+ },
+ {
+ "name": "setScriptSource",
+ "description": "Edits JavaScript source live.\n\nIn general, functions that are currently on the stack can not be edited with\na single exception: If the edited function is the top-most stack frame and\nthat is the only activation of that function on the stack. In this case\nthe live edit will be successful and a `Debugger.restartFrame` for the\ntop-most function is automatically triggered.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to edit.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "scriptSource",
+ "description": "New content of the script.",
+ "type": "string"
+ },
+ {
+ "name": "dryRun",
+ "description": "If true the change will not actually be applied. Dry run may be used to get result\ndescription without actually modifying the code.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowTopFrameEditing",
+ "description": "If true, then `scriptSource` is allowed to change the function on top of the stack\nas long as the top-most stack frame is the only activation of that function.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "callFrames",
+ "description": "New stack trace in case editing has happened while VM was stopped.",
+ "deprecated": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "stackChanged",
+ "description": "Whether current call stack was modified after applying the changes.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ },
+ {
+ "name": "status",
+ "description": "Whether the operation was successful or not. Only `Ok` denotes a\nsuccessful live edit while the other enum variants denote why\nthe live edit failed.",
+ "experimental": true,
+ "type": "string",
+ "enum": [
+ "Ok",
+ "CompileError",
+ "BlockedByActiveGenerator",
+ "BlockedByActiveFunction",
+ "BlockedByTopLevelEsModuleChange"
+ ]
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details if any. Only present when `status` is `CompileError`.",
+ "optional": true,
+ "$ref": "Runtime.ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "setSkipAllPauses",
+ "description": "Makes page not interrupt on any pauses (breakpoint, exception, dom exception etc).",
+ "parameters": [
+ {
+ "name": "skip",
+ "description": "New value for skip pauses state.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setVariableValue",
+ "description": "Changes value of variable in a callframe. Object-based scopes are not supported and must be\nmutated manually.",
+ "parameters": [
+ {
+ "name": "scopeNumber",
+ "description": "0-based number of scope as was listed in scope chain. Only 'local', 'closure' and 'catch'\nscope types are allowed. Other scopes could be manipulated manually.",
+ "type": "integer"
+ },
+ {
+ "name": "variableName",
+ "description": "Variable name.",
+ "type": "string"
+ },
+ {
+ "name": "newValue",
+ "description": "New variable value.",
+ "$ref": "Runtime.CallArgument"
+ },
+ {
+ "name": "callFrameId",
+ "description": "Id of callframe that holds variable.",
+ "$ref": "CallFrameId"
+ }
+ ]
+ },
+ {
+ "name": "stepInto",
+ "description": "Steps into the function call.",
+ "parameters": [
+ {
+ "name": "breakOnAsyncCall",
+ "description": "Debugger will pause on the execution of the first async task which was scheduled\nbefore next pause.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "skipList",
+ "description": "The skipList specifies location ranges that should be skipped on step into.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "LocationRange"
+ }
+ }
+ ]
+ },
+ {
+ "name": "stepOut",
+ "description": "Steps out of the function call."
+ },
+ {
+ "name": "stepOver",
+ "description": "Steps over the statement.",
+ "parameters": [
+ {
+ "name": "skipList",
+ "description": "The skipList specifies location ranges that should be skipped on step over.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "LocationRange"
+ }
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "breakpointResolved",
+ "description": "Fired when breakpoint is resolved to an actual script and location.",
+ "parameters": [
+ {
+ "name": "breakpointId",
+ "description": "Breakpoint unique identifier.",
+ "$ref": "BreakpointId"
+ },
+ {
+ "name": "location",
+ "description": "Actual breakpoint location.",
+ "$ref": "Location"
+ }
+ ]
+ },
+ {
+ "name": "paused",
+ "description": "Fired when the virtual machine stopped on breakpoint or exception or any other stop criteria.",
+ "parameters": [
+ {
+ "name": "callFrames",
+ "description": "Call stack the virtual machine stopped on.",
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "reason",
+ "description": "Pause reason.",
+ "type": "string",
+ "enum": [
+ "ambiguous",
+ "assert",
+ "CSPViolation",
+ "debugCommand",
+ "DOM",
+ "EventListener",
+ "exception",
+ "instrumentation",
+ "OOM",
+ "other",
+ "promiseRejection",
+ "XHR",
+ "step"
+ ]
+ },
+ {
+ "name": "data",
+ "description": "Object containing break-specific auxiliary properties.",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "hitBreakpoints",
+ "description": "Hit breakpoints IDs",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "asyncStackTrace",
+ "description": "Async stack trace, if any.",
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "asyncStackTraceId",
+ "description": "Async stack trace, if any.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ },
+ {
+ "name": "asyncCallStackTraceId",
+ "description": "Never present, will be removed.",
+ "experimental": true,
+ "deprecated": true,
+ "optional": true,
+ "$ref": "Runtime.StackTraceId"
+ }
+ ]
+ },
+ {
+ "name": "resumed",
+ "description": "Fired when the virtual machine resumed execution."
+ },
+ {
+ "name": "scriptFailedToParse",
+ "description": "Fired when virtual machine fails to parse the script.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Identifier of the script parsed.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL or name of the script parsed (if any).",
+ "type": "string"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the script within the resource with given URL (for script tags).",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the script within the resource with given URL.",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "Last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "Length of the last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies script creation context.",
+ "$ref": "Runtime.ExecutionContextId"
+ },
+ {
+ "name": "hash",
+ "description": "Content hash of the script, SHA-256.",
+ "type": "string"
+ },
+ {
+ "name": "executionContextAuxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "True, if this script has sourceURL.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isModule",
+ "description": "True, if this script is ES6 module.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "length",
+ "description": "This script length.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript top stack frame of where the script parsed event was triggered if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "codeOffset",
+ "description": "If the scriptLanguage is WebAssembly, the code section offset in the module.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scriptLanguage",
+ "description": "The language of the script.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.ScriptLanguage"
+ },
+ {
+ "name": "embedderName",
+ "description": "The name the embedder supplied for this script.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "scriptParsed",
+ "description": "Fired when virtual machine parses script. This event is also fired for all known and uncollected\nscripts upon enabling debugger.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Identifier of the script parsed.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL or name of the script parsed (if any).",
+ "type": "string"
+ },
+ {
+ "name": "startLine",
+ "description": "Line offset of the script within the resource with given URL (for script tags).",
+ "type": "integer"
+ },
+ {
+ "name": "startColumn",
+ "description": "Column offset of the script within the resource with given URL.",
+ "type": "integer"
+ },
+ {
+ "name": "endLine",
+ "description": "Last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "endColumn",
+ "description": "Length of the last line of the script.",
+ "type": "integer"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies script creation context.",
+ "$ref": "Runtime.ExecutionContextId"
+ },
+ {
+ "name": "hash",
+ "description": "Content hash of the script, SHA-256.",
+ "type": "string"
+ },
+ {
+ "name": "executionContextAuxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ },
+ {
+ "name": "isLiveEdit",
+ "description": "True, if this script is generated as a result of the live edit operation.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "sourceMapURL",
+ "description": "URL of source map associated with script (if any).",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "hasSourceURL",
+ "description": "True, if this script has sourceURL.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isModule",
+ "description": "True, if this script is ES6 module.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "length",
+ "description": "This script length.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript top stack frame of where the script parsed event was triggered if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Runtime.StackTrace"
+ },
+ {
+ "name": "codeOffset",
+ "description": "If the scriptLanguage is WebAssembly, the code section offset in the module.",
+ "experimental": true,
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "scriptLanguage",
+ "description": "The language of the script.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.ScriptLanguage"
+ },
+ {
+ "name": "debugSymbols",
+ "description": "If the scriptLanguage is WebASsembly, the source of debug symbols for the module.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "Debugger.DebugSymbols"
+ },
+ {
+ "name": "embedderName",
+ "description": "The name the embedder supplied for this script.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "HeapProfiler",
+ "experimental": true,
+ "dependencies": [
+ "Runtime"
+ ],
+ "types": [
+ {
+ "id": "HeapSnapshotObjectId",
+ "description": "Heap snapshot object id.",
+ "type": "string"
+ },
+ {
+ "id": "SamplingHeapProfileNode",
+ "description": "Sampling Heap Profile node. Holds callsite information, allocation statistics and child nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "callFrame",
+ "description": "Function location.",
+ "$ref": "Runtime.CallFrame"
+ },
+ {
+ "name": "selfSize",
+ "description": "Allocations size in bytes for the node excluding children.",
+ "type": "number"
+ },
+ {
+ "name": "id",
+ "description": "Node id. Ids are unique across all profiles collected between startSampling and stopSampling.",
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child nodes.",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingHeapProfileNode"
+ }
+ }
+ ]
+ },
+ {
+ "id": "SamplingHeapProfileSample",
+ "description": "A single sample from a sampling profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "size",
+ "description": "Allocation size in bytes attributed to the sample.",
+ "type": "number"
+ },
+ {
+ "name": "nodeId",
+ "description": "Id of the corresponding profile tree node.",
+ "type": "integer"
+ },
+ {
+ "name": "ordinal",
+ "description": "Time-ordered sample ordinal number. It is unique across all profiles retrieved\nbetween startSampling and stopSampling.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "id": "SamplingHeapProfile",
+ "description": "Sampling profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "head",
+ "$ref": "SamplingHeapProfileNode"
+ },
+ {
+ "name": "samples",
+ "type": "array",
+ "items": {
+ "$ref": "SamplingHeapProfileSample"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "addInspectedHeapObject",
+ "description": "Enables console to refer to the node with given id via $x (see Command Line API for more details\n$x functions).",
+ "parameters": [
+ {
+ "name": "heapObjectId",
+ "description": "Heap snapshot object id to be accessible by means of $x command line API.",
+ "$ref": "HeapSnapshotObjectId"
+ }
+ ]
+ },
+ {
+ "name": "collectGarbage"
+ },
+ {
+ "name": "disable"
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "getHeapObjectId",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to get heap object id for.",
+ "$ref": "Runtime.RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "heapSnapshotObjectId",
+ "description": "Id of the heap snapshot object corresponding to the passed remote object id.",
+ "$ref": "HeapSnapshotObjectId"
+ }
+ ]
+ },
+ {
+ "name": "getObjectByHeapObjectId",
+ "parameters": [
+ {
+ "name": "objectId",
+ "$ref": "HeapSnapshotObjectId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Evaluation result.",
+ "$ref": "Runtime.RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "getSamplingProfile",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Return the sampling profile being collected.",
+ "$ref": "SamplingHeapProfile"
+ }
+ ]
+ },
+ {
+ "name": "startSampling",
+ "parameters": [
+ {
+ "name": "samplingInterval",
+ "description": "Average sample interval in bytes. Poisson distribution is used for the intervals. The\ndefault value is 32768 bytes.",
+ "optional": true,
+ "type": "number"
+ },
+ {
+ "name": "includeObjectsCollectedByMajorGC",
+ "description": "By default, the sampling heap profiler reports only objects which are\nstill alive when the profile is returned via getSamplingProfile or\nstopSampling, which is useful for determining what functions contribute\nthe most to steady-state memory usage. This flag instructs the sampling\nheap profiler to also include information about objects discarded by\nmajor GC, which will show which functions cause large temporary memory\nusage or long GC pauses.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeObjectsCollectedByMinorGC",
+ "description": "By default, the sampling heap profiler reports only objects which are\nstill alive when the profile is returned via getSamplingProfile or\nstopSampling, which is useful for determining what functions contribute\nthe most to steady-state memory usage. This flag instructs the sampling\nheap profiler to also include information about objects discarded by\nminor GC, which is useful when tuning a latency-sensitive application\nfor minimal GC activity.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "startTrackingHeapObjects",
+ "parameters": [
+ {
+ "name": "trackAllocations",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "stopSampling",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Recorded sampling heap profile.",
+ "$ref": "SamplingHeapProfile"
+ }
+ ]
+ },
+ {
+ "name": "stopTrackingHeapObjects",
+ "parameters": [
+ {
+ "name": "reportProgress",
+ "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken\nwhen the tracking is stopped.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "treatGlobalObjectsAsRoots",
+ "description": "Deprecated in favor of `exposeInternals`.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureNumericValue",
+ "description": "If true, numerical values are included in the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "exposeInternals",
+ "description": "If true, exposes internals of the snapshot.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "takeHeapSnapshot",
+ "parameters": [
+ {
+ "name": "reportProgress",
+ "description": "If true 'reportHeapSnapshotProgress' events will be generated while snapshot is being taken.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "treatGlobalObjectsAsRoots",
+ "description": "If true, a raw snapshot without artificial roots will be generated.\nDeprecated in favor of `exposeInternals`.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "captureNumericValue",
+ "description": "If true, numerical values are included in the snapshot",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "exposeInternals",
+ "description": "If true, exposes internals of the snapshot.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "addHeapSnapshotChunk",
+ "parameters": [
+ {
+ "name": "chunk",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "heapStatsUpdate",
+ "description": "If heap objects tracking has been started then backend may send update for one or more fragments",
+ "parameters": [
+ {
+ "name": "statsUpdate",
+ "description": "An array of triplets. Each triplet describes a fragment. The first integer is the fragment\nindex, the second integer is a total count of objects for the fragment, the third integer is\na total size of the objects for the fragment.",
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "name": "lastSeenObjectId",
+ "description": "If heap objects tracking has been started then backend regularly sends a current value for last\nseen object id and corresponding timestamp. If the were changes in the heap since last event\nthen one or more heapStatsUpdate events will be sent before a new lastSeenObjectId event.",
+ "parameters": [
+ {
+ "name": "lastSeenObjectId",
+ "type": "integer"
+ },
+ {
+ "name": "timestamp",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "reportHeapSnapshotProgress",
+ "parameters": [
+ {
+ "name": "done",
+ "type": "integer"
+ },
+ {
+ "name": "total",
+ "type": "integer"
+ },
+ {
+ "name": "finished",
+ "optional": true,
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "resetProfiles"
+ }
+ ]
+ },
+ {
+ "domain": "Profiler",
+ "dependencies": [
+ "Runtime",
+ "Debugger"
+ ],
+ "types": [
+ {
+ "id": "ProfileNode",
+ "description": "Profile node. Holds callsite information, execution statistics and child nodes.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the node.",
+ "type": "integer"
+ },
+ {
+ "name": "callFrame",
+ "description": "Function location.",
+ "$ref": "Runtime.CallFrame"
+ },
+ {
+ "name": "hitCount",
+ "description": "Number of samples where this node was on top of the call stack.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "children",
+ "description": "Child node ids.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "deoptReason",
+ "description": "The reason of being not optimized. The function may be deoptimized or marked as don't\noptimize.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "positionTicks",
+ "description": "An array of source position ticks.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PositionTickInfo"
+ }
+ }
+ ]
+ },
+ {
+ "id": "Profile",
+ "description": "Profile.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "nodes",
+ "description": "The list of profile nodes. First item is the root node.",
+ "type": "array",
+ "items": {
+ "$ref": "ProfileNode"
+ }
+ },
+ {
+ "name": "startTime",
+ "description": "Profiling start timestamp in microseconds.",
+ "type": "number"
+ },
+ {
+ "name": "endTime",
+ "description": "Profiling end timestamp in microseconds.",
+ "type": "number"
+ },
+ {
+ "name": "samples",
+ "description": "Ids of samples top nodes.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ },
+ {
+ "name": "timeDeltas",
+ "description": "Time intervals between adjacent samples in microseconds. The first delta is relative to the\nprofile startTime.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "type": "integer"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PositionTickInfo",
+ "description": "Specifies a number of samples attributed to a certain source position.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "line",
+ "description": "Source line number (1-based).",
+ "type": "integer"
+ },
+ {
+ "name": "ticks",
+ "description": "Number of samples attributed to the source line.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "CoverageRange",
+ "description": "Coverage data for a source range.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "startOffset",
+ "description": "JavaScript script source offset for the range start.",
+ "type": "integer"
+ },
+ {
+ "name": "endOffset",
+ "description": "JavaScript script source offset for the range end.",
+ "type": "integer"
+ },
+ {
+ "name": "count",
+ "description": "Collected execution count of the source range.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "FunctionCoverage",
+ "description": "Coverage data for a JavaScript function.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "functionName",
+ "description": "JavaScript function name.",
+ "type": "string"
+ },
+ {
+ "name": "ranges",
+ "description": "Source ranges inside the function with coverage data.",
+ "type": "array",
+ "items": {
+ "$ref": "CoverageRange"
+ }
+ },
+ {
+ "name": "isBlockCoverage",
+ "description": "Whether coverage data for this function has block granularity.",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "id": "ScriptCoverage",
+ "description": "Coverage data for a JavaScript script.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "scriptId",
+ "description": "JavaScript script id.",
+ "$ref": "Runtime.ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.",
+ "type": "string"
+ },
+ {
+ "name": "functions",
+ "description": "Functions contained in the script that has coverage data.",
+ "type": "array",
+ "items": {
+ "$ref": "FunctionCoverage"
+ }
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "disable"
+ },
+ {
+ "name": "enable"
+ },
+ {
+ "name": "getBestEffortCoverage",
+ "description": "Collect coverage data for the current isolate. The coverage data may be incomplete due to\ngarbage collection.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ }
+ ]
+ },
+ {
+ "name": "setSamplingInterval",
+ "description": "Changes CPU profiler sampling interval. Must be called before CPU profiles recording started.",
+ "parameters": [
+ {
+ "name": "interval",
+ "description": "New sampling interval in microseconds.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "start"
+ },
+ {
+ "name": "startPreciseCoverage",
+ "description": "Enable precise code coverage. Coverage data for JavaScript executed before enabling precise code\ncoverage may be incomplete. Enabling prevents running optimized code and resets execution\ncounters.",
+ "parameters": [
+ {
+ "name": "callCount",
+ "description": "Collect accurate call counts beyond simple 'covered' or 'not covered'.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "detailed",
+ "description": "Collect block-based coverage.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowTriggeredUpdates",
+ "description": "Allow the backend to send updates on its own initiative",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "stop",
+ "returns": [
+ {
+ "name": "profile",
+ "description": "Recorded profile.",
+ "$ref": "Profile"
+ }
+ ]
+ },
+ {
+ "name": "stopPreciseCoverage",
+ "description": "Disable precise code coverage. Disabling releases unnecessary execution count records and allows\nexecuting optimized code."
+ },
+ {
+ "name": "takePreciseCoverage",
+ "description": "Collect coverage data for the current isolate, and resets execution counters. Precise code\ncoverage needs to have started.",
+ "returns": [
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ },
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "consoleProfileFinished",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Location of console.profileEnd().",
+ "$ref": "Debugger.Location"
+ },
+ {
+ "name": "profile",
+ "$ref": "Profile"
+ },
+ {
+ "name": "title",
+ "description": "Profile title passed as an argument to console.profile().",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "consoleProfileStarted",
+ "description": "Sent when new profile recording is started using console.profile() call.",
+ "parameters": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "location",
+ "description": "Location of console.profile().",
+ "$ref": "Debugger.Location"
+ },
+ {
+ "name": "title",
+ "description": "Profile title passed as an argument to console.profile().",
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "preciseCoverageDeltaUpdate",
+ "description": "Reports coverage delta since the last poll (either from an event like this, or from\n`takePreciseCoverage` for the current isolate. May only be sent if precise code\ncoverage has been started. This event can be trigged by the embedder to, for example,\ntrigger collection of coverage data immediately at a certain point in time.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "timestamp",
+ "description": "Monotonically increasing time (in seconds) when the coverage update was taken in the backend.",
+ "type": "number"
+ },
+ {
+ "name": "occasion",
+ "description": "Identifier for distinguishing coverage events.",
+ "type": "string"
+ },
+ {
+ "name": "result",
+ "description": "Coverage data for the current isolate.",
+ "type": "array",
+ "items": {
+ "$ref": "ScriptCoverage"
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Runtime",
+ "description": "Runtime domain exposes JavaScript runtime by means of remote evaluation and mirror objects.\nEvaluation results are returned as mirror object that expose object type, string representation\nand unique identifier that can be used for further object reference. Original objects are\nmaintained in memory unless they are either explicitly released or are released along with the\nother objects in their object group.",
+ "types": [
+ {
+ "id": "ScriptId",
+ "description": "Unique script identifier.",
+ "type": "string"
+ },
+ {
+ "id": "SerializationOptions",
+ "description": "Represents options for serialization. Overrides `generatePreview`, `returnByValue` and\n`generateWebDriverValue`.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "serialization",
+ "type": "string",
+ "enum": [
+ "deep",
+ "json",
+ "idOnly"
+ ]
+ },
+ {
+ "name": "maxDepth",
+ "description": "Deep serialization depth. Default is full depth. Respected only in `deep` serialization mode.",
+ "optional": true,
+ "type": "integer"
+ },
+ {
+ "name": "additionalParameters",
+ "description": "Embedder-specific parameters. For example if connected to V8 in Chrome these control DOM\nserialization via `maxNodeDepth: integer` and `includeShadowTree: \"none\" | \"open\" | \"all\"`.\nValues can be only of type string or integer.",
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "DeepSerializedValue",
+ "description": "Represents deep serialized value.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "type": "string",
+ "enum": [
+ "undefined",
+ "null",
+ "string",
+ "number",
+ "boolean",
+ "bigint",
+ "regexp",
+ "date",
+ "symbol",
+ "array",
+ "object",
+ "function",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "node",
+ "window"
+ ]
+ },
+ {
+ "name": "value",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "objectId",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "weakLocalObjectReference",
+ "description": "Set if value reference met more then once during serialization. In such\ncase, value is provided only to one of the serialized values. Unique\nper value in the scope of one CDP call.",
+ "optional": true,
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "RemoteObjectId",
+ "description": "Unique object identifier.",
+ "type": "string"
+ },
+ {
+ "id": "UnserializableValue",
+ "description": "Primitive value which cannot be JSON-stringified. Includes values `-0`, `NaN`, `Infinity`,\n`-Infinity`, and bigint literals.",
+ "type": "string"
+ },
+ {
+ "id": "RemoteObject",
+ "description": "Mirror object referencing original JavaScript object.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Object type.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "bigint"
+ ]
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.\nNOTE: If you change anything here, make sure to also update\n`subtype` in `ObjectPreview` and `PropertyPreview` below.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ },
+ {
+ "name": "className",
+ "description": "Object class (constructor) name. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "Remote object value in case of primitive values or JSON values (if it was requested).",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "unserializableValue",
+ "description": "Primitive value which can not be JSON-stringified does not have `value`, but gets this\nproperty.",
+ "optional": true,
+ "$ref": "UnserializableValue"
+ },
+ {
+ "name": "description",
+ "description": "String representation of the object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "webDriverValue",
+ "description": "Deprecated. Use `deepSerializedValue` instead. WebDriver BiDi representation of the value.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "DeepSerializedValue"
+ },
+ {
+ "name": "deepSerializedValue",
+ "description": "Deep serialized value.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "DeepSerializedValue"
+ },
+ {
+ "name": "objectId",
+ "description": "Unique object identifier (for non-primitive values).",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "preview",
+ "description": "Preview containing abbreviated property values. Specified for `object` type values only.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "customPreview",
+ "experimental": true,
+ "optional": true,
+ "$ref": "CustomPreview"
+ }
+ ]
+ },
+ {
+ "id": "CustomPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "header",
+ "description": "The JSON-stringified result of formatter.header(object, config) call.\nIt contains json ML array that represents RemoteObject.",
+ "type": "string"
+ },
+ {
+ "name": "bodyGetterId",
+ "description": "If formatter returns true as a result of formatter.hasBody call then bodyGetterId will\ncontain RemoteObjectId for the function that returns result of formatter.body(object, config) call.\nThe result value is json ML array.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "id": "ObjectPreview",
+ "description": "Object containing abbreviated remote object value.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "type",
+ "description": "Object type.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "bigint"
+ ]
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ },
+ {
+ "name": "description",
+ "description": "String representation of the object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "overflow",
+ "description": "True iff some of the properties or entries of the original object did not fit.",
+ "type": "boolean"
+ },
+ {
+ "name": "properties",
+ "description": "List of the properties.",
+ "type": "array",
+ "items": {
+ "$ref": "PropertyPreview"
+ }
+ },
+ {
+ "name": "entries",
+ "description": "List of the entries. Specified for `map` and `set` subtype values only.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "EntryPreview"
+ }
+ }
+ ]
+ },
+ {
+ "id": "PropertyPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Property name.",
+ "type": "string"
+ },
+ {
+ "name": "type",
+ "description": "Object type. Accessor means that the property itself is an accessor property.",
+ "type": "string",
+ "enum": [
+ "object",
+ "function",
+ "undefined",
+ "string",
+ "number",
+ "boolean",
+ "symbol",
+ "accessor",
+ "bigint"
+ ]
+ },
+ {
+ "name": "value",
+ "description": "User-friendly property value string.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "valuePreview",
+ "description": "Nested value preview.",
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "subtype",
+ "description": "Object subtype hint. Specified for `object` type values only.",
+ "optional": true,
+ "type": "string",
+ "enum": [
+ "array",
+ "null",
+ "node",
+ "regexp",
+ "date",
+ "map",
+ "set",
+ "weakmap",
+ "weakset",
+ "iterator",
+ "generator",
+ "error",
+ "proxy",
+ "promise",
+ "typedarray",
+ "arraybuffer",
+ "dataview",
+ "webassemblymemory",
+ "wasmvalue"
+ ]
+ }
+ ]
+ },
+ {
+ "id": "EntryPreview",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "key",
+ "description": "Preview of the key. Specified for map-like collection entries.",
+ "optional": true,
+ "$ref": "ObjectPreview"
+ },
+ {
+ "name": "value",
+ "description": "Preview of the value.",
+ "$ref": "ObjectPreview"
+ }
+ ]
+ },
+ {
+ "id": "PropertyDescriptor",
+ "description": "Object property descriptor.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Property name or symbol description.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "writable",
+ "description": "True if the value associated with the property may be changed (data descriptors only).",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "get",
+ "description": "A function which serves as a getter for the property, or `undefined` if there is no getter\n(accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "set",
+ "description": "A function which serves as a setter for the property, or `undefined` if there is no setter\n(accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "configurable",
+ "description": "True if the type of this property descriptor may be changed and if the property may be\ndeleted from the corresponding object.",
+ "type": "boolean"
+ },
+ {
+ "name": "enumerable",
+ "description": "True if this property shows up during enumeration of the properties on the corresponding\nobject.",
+ "type": "boolean"
+ },
+ {
+ "name": "wasThrown",
+ "description": "True if the result was thrown during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "isOwn",
+ "description": "True if the property is owned for the object.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "symbol",
+ "description": "Property symbol object, if the property is of the `symbol` type.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "InternalPropertyDescriptor",
+ "description": "Object internal property descriptor. This property isn't normally visible in JavaScript code.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Conventional property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "PrivatePropertyDescriptor",
+ "description": "Object private field descriptor.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Private property name.",
+ "type": "string"
+ },
+ {
+ "name": "value",
+ "description": "The value associated with the private property.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "get",
+ "description": "A function which serves as a getter for the private property,\nor `undefined` if there is no getter (accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "set",
+ "description": "A function which serves as a setter for the private property,\nor `undefined` if there is no setter (accessor descriptors only).",
+ "optional": true,
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "id": "CallArgument",
+ "description": "Represents function call argument. Either remote object id `objectId`, primitive `value`,\nunserializable primitive value or neither of (for undefined) them should be specified.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "value",
+ "description": "Primitive value or serializable javascript object.",
+ "optional": true,
+ "type": "any"
+ },
+ {
+ "name": "unserializableValue",
+ "description": "Primitive value which can not be JSON-stringified.",
+ "optional": true,
+ "$ref": "UnserializableValue"
+ },
+ {
+ "name": "objectId",
+ "description": "Remote object handle.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "id": "ExecutionContextId",
+ "description": "Id of an execution context.",
+ "type": "integer"
+ },
+ {
+ "id": "ExecutionContextDescription",
+ "description": "Description of an isolated world.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "description": "Unique id of the execution context. It can be used to specify in which execution context\nscript evaluation should be performed.",
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "origin",
+ "description": "Execution context origin.",
+ "type": "string"
+ },
+ {
+ "name": "name",
+ "description": "Human readable name describing given context.",
+ "type": "string"
+ },
+ {
+ "name": "uniqueId",
+ "description": "A system-unique execution context identifier. Unlike the id, this is unique across\nmultiple processes, so can be reliably used to identify specific context while backend\nperforms a cross-process navigation.",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "name": "auxData",
+ "description": "Embedder-specific auxiliary data likely matching {isDefault: boolean, type: 'default'|'isolated'|'worker', frameId: string}",
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "ExceptionDetails",
+ "description": "Detailed information about exception (or error) that was thrown during script compilation or\nexecution.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "exceptionId",
+ "description": "Exception id.",
+ "type": "integer"
+ },
+ {
+ "name": "text",
+ "description": "Exception text, which should be used together with exception object when available.",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "Line number of the exception location (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "Column number of the exception location (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "scriptId",
+ "description": "Script ID of the exception location.",
+ "optional": true,
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "URL of the exception location, to be used when the script was not reported.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "stackTrace",
+ "description": "JavaScript stack trace if available.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "exception",
+ "description": "Exception object if available.",
+ "optional": true,
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where exception happened.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "exceptionMetaData",
+ "description": "Dictionary with entries of meta data that the client associated\nwith this exception, such as information about associated network\nrequests, etc.",
+ "experimental": true,
+ "optional": true,
+ "type": "object"
+ }
+ ]
+ },
+ {
+ "id": "Timestamp",
+ "description": "Number of milliseconds since epoch.",
+ "type": "number"
+ },
+ {
+ "id": "TimeDelta",
+ "description": "Number of milliseconds.",
+ "type": "number"
+ },
+ {
+ "id": "CallFrame",
+ "description": "Stack entry for runtime errors and assertions.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "functionName",
+ "description": "JavaScript function name.",
+ "type": "string"
+ },
+ {
+ "name": "scriptId",
+ "description": "JavaScript script id.",
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "url",
+ "description": "JavaScript script name or url.",
+ "type": "string"
+ },
+ {
+ "name": "lineNumber",
+ "description": "JavaScript script line number (0-based).",
+ "type": "integer"
+ },
+ {
+ "name": "columnNumber",
+ "description": "JavaScript script column number (0-based).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "id": "StackTrace",
+ "description": "Call frames for assertions or error messages.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "description",
+ "description": "String label of this stack trace. For async traces this may be a name of the function that\ninitiated the async call.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "callFrames",
+ "description": "JavaScript function name.",
+ "type": "array",
+ "items": {
+ "$ref": "CallFrame"
+ }
+ },
+ {
+ "name": "parent",
+ "description": "Asynchronous JavaScript stack trace that preceded this stack, if available.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "parentId",
+ "description": "Asynchronous JavaScript stack trace that preceded this stack, if available.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "StackTraceId"
+ }
+ ]
+ },
+ {
+ "id": "UniqueDebuggerId",
+ "description": "Unique identifier of current debugger.",
+ "experimental": true,
+ "type": "string"
+ },
+ {
+ "id": "StackTraceId",
+ "description": "If `debuggerId` is set stack trace comes from another debugger and can be resolved there. This\nallows to track cross-debugger calls. See `Runtime.StackTrace` and `Debugger.paused` for usages.",
+ "experimental": true,
+ "type": "object",
+ "properties": [
+ {
+ "name": "id",
+ "type": "string"
+ },
+ {
+ "name": "debuggerId",
+ "optional": true,
+ "$ref": "UniqueDebuggerId"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "awaitPromise",
+ "description": "Add handler to promise with given promise object id.",
+ "parameters": [
+ {
+ "name": "promiseObjectId",
+ "description": "Identifier of the promise.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Promise result. Will contain rejected value if promise was rejected.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details if stack strace is available.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "callFunctionOn",
+ "description": "Calls function with given declaration on the given object. Object group of the result is\ninherited from the target object.",
+ "parameters": [
+ {
+ "name": "functionDeclaration",
+ "description": "Declaration of the function to call.",
+ "type": "string"
+ },
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to call function on. Either objectId or executionContextId should\nbe specified.",
+ "optional": true,
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "arguments",
+ "description": "Call arguments. All call arguments must belong to the same JavaScript world as the target\nobject.",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "CallArgument"
+ }
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object which should be sent by value.\nCan be overriden by `serializationOptions`.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether execution should be treated as initiated by user in the UI.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies execution context which global object will be used to call function on. Either\nexecutionContextId or objectId should be specified.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects. If objectGroup is not\nspecified and objectId is, objectGroup will be inherited from object.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "uniqueContextId",
+ "description": "An alternative way to specify the execution context to call function on.\nCompared to contextId that may be reused across processes, this is guaranteed to be\nsystem-unique, so it can be used to prevent accidental function call\nin context different than intended (e.g. as a result of navigation across process\nboundaries).\nThis is mutually exclusive with `executionContextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "generateWebDriverValue",
+ "description": "Deprecated. Use `serializationOptions: {serialization:\"deep\"}` instead.\nWhether the result should contain `webDriverValue`, serialized according to\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\nresulting `objectId` is still provided.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "serializationOptions",
+ "description": "Specifies the result serialization. If provided, overrides\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "SerializationOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Call result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "compileScript",
+ "description": "Compiles expression.",
+ "parameters": [
+ {
+ "name": "expression",
+ "description": "Expression to compile.",
+ "type": "string"
+ },
+ {
+ "name": "sourceURL",
+ "description": "Source url to be set for the script.",
+ "type": "string"
+ },
+ {
+ "name": "persistScript",
+ "description": "Specifies whether the compiled script should be persisted.",
+ "type": "boolean"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script.",
+ "optional": true,
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "disable",
+ "description": "Disables reporting of execution contexts creation."
+ },
+ {
+ "name": "discardConsoleEntries",
+ "description": "Discards collected exceptions and console API calls."
+ },
+ {
+ "name": "enable",
+ "description": "Enables reporting of execution contexts creation by means of `executionContextCreated` event.\nWhen the reporting gets enabled the event will be sent immediately for each existing execution\ncontext."
+ },
+ {
+ "name": "evaluate",
+ "description": "Evaluates expression on global object.",
+ "parameters": [
+ {
+ "name": "expression",
+ "description": "Expression to evaluate.",
+ "type": "string"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Determines whether Command Line API should be available during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "contextId",
+ "description": "Specifies in which execution context to perform evaluation. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.\nThis is mutually exclusive with `uniqueContextId`, which offers an\nalternative way to identify the execution context that is more reliable\nin a multi-process environment.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object that should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "userGesture",
+ "description": "Whether execution should be treated as initiated by user in the UI.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "throwOnSideEffect",
+ "description": "Whether to throw an exception if side effect cannot be ruled out during evaluation.\nThis implies `disableBreaks` below.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "timeout",
+ "description": "Terminate execution after timing out (number of milliseconds).",
+ "experimental": true,
+ "optional": true,
+ "$ref": "TimeDelta"
+ },
+ {
+ "name": "disableBreaks",
+ "description": "Disable breakpoints during execution.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "replMode",
+ "description": "Setting this flag to true enables `let` re-declaration and top-level `await`.\nNote that `let` variables can only be re-declared if they originate from\n`replMode` themselves.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "allowUnsafeEvalBlockedByCSP",
+ "description": "The Content Security Policy (CSP) for the target might block 'unsafe-eval'\nwhich includes eval(), Function(), setTimeout() and setInterval()\nwhen called with non-callable arguments. This flag bypasses CSP for this\nevaluation and allows unsafe-eval. Defaults to true.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "uniqueContextId",
+ "description": "An alternative way to specify the execution context to evaluate in.\nCompared to contextId that may be reused across processes, this is guaranteed to be\nsystem-unique, so it can be used to prevent accidental evaluation of the expression\nin context different than intended (e.g. as a result of navigation across process\nboundaries).\nThis is mutually exclusive with `contextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "generateWebDriverValue",
+ "description": "Deprecated. Use `serializationOptions: {serialization:\"deep\"}` instead.\nWhether the result should contain `webDriverValue`, serialized\naccording to\nhttps://w3c.github.io/webdriver-bidi. This is mutually exclusive with `returnByValue`, but\nresulting `objectId` is still provided.",
+ "deprecated": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "serializationOptions",
+ "description": "Specifies the result serialization. If provided, overrides\n`generatePreview`, `returnByValue` and `generateWebDriverValue`.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "SerializationOptions"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Evaluation result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "getIsolateId",
+ "description": "Returns the isolate id.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "id",
+ "description": "The isolate id.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getHeapUsage",
+ "description": "Returns the JavaScript heap usage.\nIt is the total usage of the corresponding isolate not scoped to a particular Runtime.",
+ "experimental": true,
+ "returns": [
+ {
+ "name": "usedSize",
+ "description": "Used heap size in bytes.",
+ "type": "number"
+ },
+ {
+ "name": "totalSize",
+ "description": "Allocated heap size in bytes.",
+ "type": "number"
+ }
+ ]
+ },
+ {
+ "name": "getProperties",
+ "description": "Returns properties of a given object. Object group of the result is inherited from the target\nobject.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to return properties for.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "ownProperties",
+ "description": "If true, returns properties belonging only to the element itself, not to its prototype\nchain.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "accessorPropertiesOnly",
+ "description": "If true, returns accessor properties (with getter/setter) only; internal properties are not\nreturned either.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the results.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "nonIndexedPropertiesOnly",
+ "description": "If true, returns non-indexed properties only.",
+ "experimental": true,
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Object properties.",
+ "type": "array",
+ "items": {
+ "$ref": "PropertyDescriptor"
+ }
+ },
+ {
+ "name": "internalProperties",
+ "description": "Internal object properties (only of the element itself).",
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "InternalPropertyDescriptor"
+ }
+ },
+ {
+ "name": "privateProperties",
+ "description": "Object private properties.",
+ "experimental": true,
+ "optional": true,
+ "type": "array",
+ "items": {
+ "$ref": "PrivatePropertyDescriptor"
+ }
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "globalLexicalScopeNames",
+ "description": "Returns all let, const and class variables from global scope.",
+ "parameters": [
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to lookup global scope variables.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "names",
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ ]
+ },
+ {
+ "name": "queryObjects",
+ "parameters": [
+ {
+ "name": "prototypeObjectId",
+ "description": "Identifier of the prototype to return objects for.",
+ "$ref": "RemoteObjectId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release the results.",
+ "optional": true,
+ "type": "string"
+ }
+ ],
+ "returns": [
+ {
+ "name": "objects",
+ "description": "Array with objects.",
+ "$ref": "RemoteObject"
+ }
+ ]
+ },
+ {
+ "name": "releaseObject",
+ "description": "Releases remote object with given id.",
+ "parameters": [
+ {
+ "name": "objectId",
+ "description": "Identifier of the object to release.",
+ "$ref": "RemoteObjectId"
+ }
+ ]
+ },
+ {
+ "name": "releaseObjectGroup",
+ "description": "Releases all remote objects that belong to a given group.",
+ "parameters": [
+ {
+ "name": "objectGroup",
+ "description": "Symbolic object group name.",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "runIfWaitingForDebugger",
+ "description": "Tells inspected instance to run if it was waiting for debugger to attach."
+ },
+ {
+ "name": "runScript",
+ "description": "Runs script with given id in a given context.",
+ "parameters": [
+ {
+ "name": "scriptId",
+ "description": "Id of the script to run.",
+ "$ref": "ScriptId"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Specifies in which execution context to perform script run. If the parameter is omitted the\nevaluation will be performed in the context of the inspected page.",
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "objectGroup",
+ "description": "Symbolic group name that can be used to release multiple objects.",
+ "optional": true,
+ "type": "string"
+ },
+ {
+ "name": "silent",
+ "description": "In silent mode exceptions thrown during evaluation are not reported and do not pause\nexecution. Overrides `setPauseOnException` state.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "includeCommandLineAPI",
+ "description": "Determines whether Command Line API should be available during the evaluation.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "returnByValue",
+ "description": "Whether the result is expected to be a JSON object which should be sent by value.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "generatePreview",
+ "description": "Whether preview should be generated for the result.",
+ "optional": true,
+ "type": "boolean"
+ },
+ {
+ "name": "awaitPromise",
+ "description": "Whether execution should `await` for resulting value and return once awaited promise is\nresolved.",
+ "optional": true,
+ "type": "boolean"
+ }
+ ],
+ "returns": [
+ {
+ "name": "result",
+ "description": "Run result.",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "exceptionDetails",
+ "description": "Exception details.",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "setAsyncCallStackDepth",
+ "description": "Enables or disables async call stacks tracking.",
+ "redirect": "Debugger",
+ "parameters": [
+ {
+ "name": "maxDepth",
+ "description": "Maximum depth of async call stacks. Setting to `0` will effectively disable collecting async\ncall stacks (default).",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "setCustomObjectFormatterEnabled",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "enabled",
+ "type": "boolean"
+ }
+ ]
+ },
+ {
+ "name": "setMaxCallStackSizeToCapture",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "size",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "terminateExecution",
+ "description": "Terminate current or next JavaScript execution.\nWill cancel the termination when the outer-most script execution ends.",
+ "experimental": true
+ },
+ {
+ "name": "addBinding",
+ "description": "If executionContextId is empty, adds binding with the given name on the\nglobal objects of all inspected contexts, including those created later,\nbindings survive reloads.\nBinding function takes exactly one argument, this argument should be string,\nin case of any other input, function throws an exception.\nEach binding function call produces Runtime.bindingCalled notification.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "If specified, the binding would only be exposed to the specified\nexecution context. If omitted and `executionContextName` is not set,\nthe binding is exposed to all execution contexts of the target.\nThis parameter is mutually exclusive with `executionContextName`.\nDeprecated in favor of `executionContextName` due to an unclear use case\nand bugs in implementation (crbug.com/1169639). `executionContextId` will be\nremoved in the future.",
+ "deprecated": true,
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "executionContextName",
+ "description": "If specified, the binding is exposed to the executionContext with\nmatching name, even for contexts created after the binding is added.\nSee also `ExecutionContext.name` and `worldName` parameter to\n`Page.addScriptToEvaluateOnNewDocument`.\nThis parameter is mutually exclusive with `executionContextId`.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "removeBinding",
+ "description": "This method does not remove binding function from global object but\nunsubscribes current runtime agent from Runtime.bindingCalled notifications.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "getExceptionDetails",
+ "description": "This method tries to lookup and populate exception details for a\nJavaScript Error object.\nNote that the stackTrace portion of the resulting exceptionDetails will\nonly be populated if the Runtime domain was enabled at the time when the\nError was thrown.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "errorObjectId",
+ "description": "The error object for which to resolve the exception details.",
+ "$ref": "RemoteObjectId"
+ }
+ ],
+ "returns": [
+ {
+ "name": "exceptionDetails",
+ "optional": true,
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ }
+ ],
+ "events": [
+ {
+ "name": "bindingCalled",
+ "description": "Notification is issued every time when binding is called.",
+ "experimental": true,
+ "parameters": [
+ {
+ "name": "name",
+ "type": "string"
+ },
+ {
+ "name": "payload",
+ "type": "string"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "$ref": "ExecutionContextId"
+ }
+ ]
+ },
+ {
+ "name": "consoleAPICalled",
+ "description": "Issued when console API was called.",
+ "parameters": [
+ {
+ "name": "type",
+ "description": "Type of the call.",
+ "type": "string",
+ "enum": [
+ "log",
+ "debug",
+ "info",
+ "error",
+ "warning",
+ "dir",
+ "dirxml",
+ "table",
+ "trace",
+ "clear",
+ "startGroup",
+ "startGroupCollapsed",
+ "endGroup",
+ "assert",
+ "profile",
+ "profileEnd",
+ "count",
+ "timeEnd"
+ ]
+ },
+ {
+ "name": "args",
+ "description": "Call arguments.",
+ "type": "array",
+ "items": {
+ "$ref": "RemoteObject"
+ }
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "timestamp",
+ "description": "Call timestamp.",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "stackTrace",
+ "description": "Stack trace captured when the call was made. The async stack chain is automatically reported for\nthe following call types: `assert`, `error`, `trace`, `warning`. For other types the async call\nchain can be retrieved using `Debugger.getStackTrace` and `stackTrace.parentId` field.",
+ "optional": true,
+ "$ref": "StackTrace"
+ },
+ {
+ "name": "context",
+ "description": "Console context descriptor for calls on non-default console context (not console.*):\n'anonymous#unique-logger-id' for call on unnamed context, 'name#unique-logger-id' for call\non named context.",
+ "experimental": true,
+ "optional": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "exceptionRevoked",
+ "description": "Issued when unhandled exception was revoked.",
+ "parameters": [
+ {
+ "name": "reason",
+ "description": "Reason describing why exception was revoked.",
+ "type": "string"
+ },
+ {
+ "name": "exceptionId",
+ "description": "The id of revoked exception, as reported in `exceptionThrown`.",
+ "type": "integer"
+ }
+ ]
+ },
+ {
+ "name": "exceptionThrown",
+ "description": "Issued when exception was thrown and unhandled.",
+ "parameters": [
+ {
+ "name": "timestamp",
+ "description": "Timestamp of the exception.",
+ "$ref": "Timestamp"
+ },
+ {
+ "name": "exceptionDetails",
+ "$ref": "ExceptionDetails"
+ }
+ ]
+ },
+ {
+ "name": "executionContextCreated",
+ "description": "Issued when new execution context is created.",
+ "parameters": [
+ {
+ "name": "context",
+ "description": "A newly created execution context.",
+ "$ref": "ExecutionContextDescription"
+ }
+ ]
+ },
+ {
+ "name": "executionContextDestroyed",
+ "description": "Issued when execution context is destroyed.",
+ "parameters": [
+ {
+ "name": "executionContextId",
+ "description": "Id of the destroyed context",
+ "deprecated": true,
+ "$ref": "ExecutionContextId"
+ },
+ {
+ "name": "executionContextUniqueId",
+ "description": "Unique Id of the destroyed context",
+ "experimental": true,
+ "type": "string"
+ }
+ ]
+ },
+ {
+ "name": "executionContextsCleared",
+ "description": "Issued when all executionContexts were cleared in browser"
+ },
+ {
+ "name": "inspectRequested",
+ "description": "Issued when object should be inspected (for example, as a result of inspect() command line API\ncall).",
+ "parameters": [
+ {
+ "name": "object",
+ "$ref": "RemoteObject"
+ },
+ {
+ "name": "hints",
+ "type": "object"
+ },
+ {
+ "name": "executionContextId",
+ "description": "Identifier of the context where the call was made.",
+ "experimental": true,
+ "optional": true,
+ "$ref": "ExecutionContextId"
+ }
+ ]
+ }
+ ]
+ },
+ {
+ "domain": "Schema",
+ "description": "This domain is deprecated.",
+ "deprecated": true,
+ "types": [
+ {
+ "id": "Domain",
+ "description": "Description of the protocol domain.",
+ "type": "object",
+ "properties": [
+ {
+ "name": "name",
+ "description": "Domain name.",
+ "type": "string"
+ },
+ {
+ "name": "version",
+ "description": "Domain version.",
+ "type": "string"
+ }
+ ]
+ }
+ ],
+ "commands": [
+ {
+ "name": "getDomains",
+ "description": "Returns supported domains.",
+ "returns": [
+ {
+ "name": "domains",
+ "description": "List of supported domains.",
+ "type": "array",
+ "items": {
+ "$ref": "Domain"
+ }
+ }
+ ]
+ }
+ ]
+ }
+ ]
+}
diff --git a/node_modules/chrome-remote-interface/lib/websocket-wrapper.js b/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
new file mode 100644
index 0000000..8321446
--- /dev/null
+++ b/node_modules/chrome-remote-interface/lib/websocket-wrapper.js
@@ -0,0 +1,39 @@
+'use strict';
+
+const EventEmitter = require('events');
+
+// wrapper around the Node.js ws module
+// for use in browsers
+class WebSocketWrapper extends EventEmitter {
+ constructor(url) {
+ super();
+ this._ws = new WebSocket(url); // eslint-disable-line no-undef
+ this._ws.onopen = () => {
+ this.emit('open');
+ };
+ this._ws.onclose = () => {
+ this.emit('close');
+ };
+ this._ws.onmessage = (event) => {
+ this.emit('message', event.data);
+ };
+ this._ws.onerror = () => {
+ this.emit('error', new Error('WebSocket error'));
+ };
+ }
+
+ close() {
+ this._ws.close();
+ }
+
+ send(data, callback) {
+ try {
+ this._ws.send(data);
+ callback();
+ } catch (err) {
+ callback(err);
+ }
+ }
+}
+
+module.exports = WebSocketWrapper;
diff --git a/node_modules/chrome-remote-interface/package.json b/node_modules/chrome-remote-interface/package.json
new file mode 100644
index 0000000..dc01bf4
--- /dev/null
+++ b/node_modules/chrome-remote-interface/package.json
@@ -0,0 +1,64 @@
+{
+ "name": "chrome-remote-interface",
+ "author": "Andrea Cardaci <cyrus.and@gmail.com>",
+ "license": "MIT",
+ "contributors": [
+ "Andrey Sidorov <sidoares@yandex.ru>",
+ "Greg Cochard <greg@gregcochard.com>"
+ ],
+ "description": "Chrome Debugging Protocol interface",
+ "keywords": [
+ "chrome",
+ "debug",
+ "protocol",
+ "remote",
+ "interface"
+ ],
+ "homepage": "https://github.com/cyrus-and/chrome-remote-interface",
+ "version": "0.33.0",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/cyrus-and/chrome-remote-interface.git"
+ },
+ "bugs": {
+ "url": "http://github.com/cyrus-and/chrome-remote-interface/issues"
+ },
+ "engine-strict": {
+ "node": ">=8"
+ },
+ "dependencies": {
+ "commander": "2.11.x",
+ "ws": "^7.2.0"
+ },
+ "files": [
+ "lib",
+ "bin",
+ "index.js",
+ "chrome-remote-interface.js",
+ "webpack.config.js"
+ ],
+ "bin": {
+ "chrome-remote-interface": "bin/client.js"
+ },
+ "main": "index.js",
+ "browser": "chrome-remote-interface.js",
+ "devDependencies": {
+ "babel-core": "^6.26.3",
+ "babel-loader": "8.x.x",
+ "babel-polyfill": "^6.26.0",
+ "babel-preset-env": "^1.7.0",
+ "eslint": "^8.8.0",
+ "json-loader": "^0.5.4",
+ "mocha": "^9.2.0",
+ "process": "^0.11.10",
+ "url": "^0.11.0",
+ "util": "^0.12.4",
+ "webpack": "^5.39.0",
+ "webpack-cli": "^4.7.2"
+ },
+ "scripts": {
+ "test": "./scripts/run-tests.sh",
+ "webpack": "webpack",
+ "prepare": "webpack"
+ }
+}
diff --git a/node_modules/chrome-remote-interface/webpack.config.js b/node_modules/chrome-remote-interface/webpack.config.js
new file mode 100644
index 0000000..0b7da3c
--- /dev/null
+++ b/node_modules/chrome-remote-interface/webpack.config.js
@@ -0,0 +1,48 @@
+'use strict';
+
+const TerserPlugin = require('terser-webpack-plugin');
+const webpack = require('webpack');
+
+function criWrapper(_, options, callback) {
+ window.criRequest(options, callback); // eslint-disable-line no-undef
+}
+
+module.exports = {
+ mode: 'production',
+ resolve: {
+ fallback: {
+ 'util': require.resolve('util/'),
+ 'url': require.resolve('url/'),
+ 'http': false,
+ 'https': false,
+ 'dns': false
+ },
+ alias: {
+ 'ws': './websocket-wrapper.js'
+ }
+ },
+ externals: [
+ {
+ './external-request.js': `var (${criWrapper})`
+ }
+ ],
+ plugins: [
+ new webpack.ProvidePlugin({
+ process: 'process/browser',
+ }),
+ ],
+ optimization: {
+ minimizer: [
+ new TerserPlugin({
+ extractComments: false,
+ })
+ ],
+ },
+ entry: ['babel-polyfill', './index.js'],
+ output: {
+ path: __dirname,
+ filename: 'chrome-remote-interface.js',
+ libraryTarget: process.env.TARGET || 'commonjs2',
+ library: 'CDP'
+ }
+};
diff --git a/node_modules/commander/History.md b/node_modules/commander/History.md
new file mode 100644
index 0000000..d60418e
--- /dev/null
+++ b/node_modules/commander/History.md
@@ -0,0 +1,298 @@
+
+2.11.0 / 2017-07-03
+==================
+
+ * Fix help section order and padding (#652)
+ * feature: support for signals to subcommands (#632)
+ * Fixed #37, --help should not display first (#447)
+ * Fix translation errors. (#570)
+ * Add package-lock.json
+ * Remove engines
+ * Upgrade package version
+ * Prefix events to prevent conflicts between commands and options (#494)
+ * Removing dependency on graceful-readlink
+ * Support setting name in #name function and make it chainable
+ * Add .vscode directory to .gitignore (Visual Studio Code metadata)
+ * Updated link to ruby commander in readme files
+
+2.10.0 / 2017-06-19
+==================
+
+ * Update .travis.yml. drop support for older node.js versions.
+ * Fix require arguments in README.md
+ * On SemVer you do not start from 0.0.1
+ * Add missing semi colon in readme
+ * Add save param to npm install
+ * node v6 travis test
+ * Update Readme_zh-CN.md
+ * Allow literal '--' to be passed-through as an argument
+ * Test subcommand alias help
+ * link build badge to master branch
+ * Support the alias of Git style sub-command
+ * added keyword commander for better search result on npm
+ * Fix Sub-Subcommands
+ * test node.js stable
+ * Fixes TypeError when a command has an option called `--description`
+ * Update README.md to make it beginner friendly and elaborate on the difference between angled and square brackets.
+ * Add chinese Readme file
+
+2.9.0 / 2015-10-13
+==================
+
+ * Add option `isDefault` to set default subcommand #415 @Qix-
+ * Add callback to allow filtering or post-processing of help text #434 @djulien
+ * Fix `undefined` text in help information close #414 #416 @zhiyelee
+
+2.8.1 / 2015-04-22
+==================
+
+ * Back out `support multiline description` Close #396 #397
+
+2.8.0 / 2015-04-07
+==================
+
+ * Add `process.execArg` support, execution args like `--harmony` will be passed to sub-commands #387 @DigitalIO @zhiyelee
+ * Fix bug in Git-style sub-commands #372 @zhiyelee
+ * Allow commands to be hidden from help #383 @tonylukasavage
+ * When git-style sub-commands are in use, yet none are called, display help #382 @claylo
+ * Add ability to specify arguments syntax for top-level command #258 @rrthomas
+ * Support multiline descriptions #208 @zxqfox
+
+2.7.1 / 2015-03-11
+==================
+
+ * Revert #347 (fix collisions when option and first arg have same name) which causes a bug in #367.
+
+2.7.0 / 2015-03-09
+==================
+
+ * Fix git-style bug when installed globally. Close #335 #349 @zhiyelee
+ * Fix collisions when option and first arg have same name. Close #346 #347 @tonylukasavage
+ * Add support for camelCase on `opts()`. Close #353 @nkzawa
+ * Add node.js 0.12 and io.js to travis.yml
+ * Allow RegEx options. #337 @palanik
+ * Fixes exit code when sub-command failing. Close #260 #332 @pirelenito
+ * git-style `bin` files in $PATH make sense. Close #196 #327 @zhiyelee
+
+2.6.0 / 2014-12-30
+==================
+
+ * added `Command#allowUnknownOption` method. Close #138 #318 @doozr @zhiyelee
+ * Add application description to the help msg. Close #112 @dalssoft
+
+2.5.1 / 2014-12-15
+==================
+
+ * fixed two bugs incurred by variadic arguments. Close #291 @Quentin01 #302 @zhiyelee
+
+2.5.0 / 2014-10-24
+==================
+
+ * add support for variadic arguments. Closes #277 @whitlockjc
+
+2.4.0 / 2014-10-17
+==================
+
+ * fixed a bug on executing the coercion function of subcommands option. Closes #270
+ * added `Command.prototype.name` to retrieve command name. Closes #264 #266 @tonylukasavage
+ * added `Command.prototype.opts` to retrieve all the options as a simple object of key-value pairs. Closes #262 @tonylukasavage
+ * fixed a bug on subcommand name. Closes #248 @jonathandelgado
+ * fixed function normalize doesn’t honor option terminator. Closes #216 @abbr
+
+2.3.0 / 2014-07-16
+==================
+
+ * add command alias'. Closes PR #210
+ * fix: Typos. Closes #99
+ * fix: Unused fs module. Closes #217
+
+2.2.0 / 2014-03-29
+==================
+
+ * add passing of previous option value
+ * fix: support subcommands on windows. Closes #142
+ * Now the defaultValue passed as the second argument of the coercion function.
+
+2.1.0 / 2013-11-21
+==================
+
+ * add: allow cflag style option params, unit test, fixes #174
+
+2.0.0 / 2013-07-18
+==================
+
+ * remove input methods (.prompt, .confirm, etc)
+
+1.3.2 / 2013-07-18
+==================
+
+ * add support for sub-commands to co-exist with the original command
+
+1.3.1 / 2013-07-18
+==================
+
+ * add quick .runningCommand hack so you can opt-out of other logic when running a sub command
+
+1.3.0 / 2013-07-09
+==================
+
+ * add EACCES error handling
+ * fix sub-command --help
+
+1.2.0 / 2013-06-13
+==================
+
+ * allow "-" hyphen as an option argument
+ * support for RegExp coercion
+
+1.1.1 / 2012-11-20
+==================
+
+ * add more sub-command padding
+ * fix .usage() when args are present. Closes #106
+
+1.1.0 / 2012-11-16
+==================
+
+ * add git-style executable subcommand support. Closes #94
+
+1.0.5 / 2012-10-09
+==================
+
+ * fix `--name` clobbering. Closes #92
+ * fix examples/help. Closes #89
+
+1.0.4 / 2012-09-03
+==================
+
+ * add `outputHelp()` method.
+
+1.0.3 / 2012-08-30
+==================
+
+ * remove invalid .version() defaulting
+
+1.0.2 / 2012-08-24
+==================
+
+ * add `--foo=bar` support [arv]
+ * fix password on node 0.8.8. Make backward compatible with 0.6 [focusaurus]
+
+1.0.1 / 2012-08-03
+==================
+
+ * fix issue #56
+ * fix tty.setRawMode(mode) was moved to tty.ReadStream#setRawMode() (i.e. process.stdin.setRawMode())
+
+1.0.0 / 2012-07-05
+==================
+
+ * add support for optional option descriptions
+ * add defaulting of `.version()` to package.json's version
+
+0.6.1 / 2012-06-01
+==================
+
+ * Added: append (yes or no) on confirmation
+ * Added: allow node.js v0.7.x
+
+0.6.0 / 2012-04-10
+==================
+
+ * Added `.prompt(obj, callback)` support. Closes #49
+ * Added default support to .choose(). Closes #41
+ * Fixed the choice example
+
+0.5.1 / 2011-12-20
+==================
+
+ * Fixed `password()` for recent nodes. Closes #36
+
+0.5.0 / 2011-12-04
+==================
+
+ * Added sub-command option support [itay]
+
+0.4.3 / 2011-12-04
+==================
+
+ * Fixed custom help ordering. Closes #32
+
+0.4.2 / 2011-11-24
+==================
+
+ * Added travis support
+ * Fixed: line-buffered input automatically trimmed. Closes #31
+
+0.4.1 / 2011-11-18
+==================
+
+ * Removed listening for "close" on --help
+
+0.4.0 / 2011-11-15
+==================
+
+ * Added support for `--`. Closes #24
+
+0.3.3 / 2011-11-14
+==================
+
+ * Fixed: wait for close event when writing help info [Jerry Hamlet]
+
+0.3.2 / 2011-11-01
+==================
+
+ * Fixed long flag definitions with values [felixge]
+
+0.3.1 / 2011-10-31
+==================
+
+ * Changed `--version` short flag to `-V` from `-v`
+ * Changed `.version()` so it's configurable [felixge]
+
+0.3.0 / 2011-10-31
+==================
+
+ * Added support for long flags only. Closes #18
+
+0.2.1 / 2011-10-24
+==================
+
+ * "node": ">= 0.4.x < 0.7.0". Closes #20
+
+0.2.0 / 2011-09-26
+==================
+
+ * Allow for defaults that are not just boolean. Default peassignment only occurs for --no-*, optional, and required arguments. [Jim Isaacs]
+
+0.1.0 / 2011-08-24
+==================
+
+ * Added support for custom `--help` output
+
+0.0.5 / 2011-08-18
+==================
+
+ * Changed: when the user enters nothing prompt for password again
+ * Fixed issue with passwords beginning with numbers [NuckChorris]
+
+0.0.4 / 2011-08-15
+==================
+
+ * Fixed `Commander#args`
+
+0.0.3 / 2011-08-15
+==================
+
+ * Added default option value support
+
+0.0.2 / 2011-08-15
+==================
+
+ * Added mask support to `Command#password(str[, mask], fn)`
+ * Added `Command#password(str, fn)`
+
+0.0.1 / 2010-01-03
+==================
+
+ * Initial release
diff --git a/node_modules/commander/LICENSE b/node_modules/commander/LICENSE
new file mode 100644
index 0000000..10f997a
--- /dev/null
+++ b/node_modules/commander/LICENSE
@@ -0,0 +1,22 @@
+(The MIT License)
+
+Copyright (c) 2011 TJ Holowaychuk <tj@vision-media.ca>
+
+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.
diff --git a/node_modules/commander/Readme.md b/node_modules/commander/Readme.md
new file mode 100644
index 0000000..3135a94
--- /dev/null
+++ b/node_modules/commander/Readme.md
@@ -0,0 +1,351 @@
+# Commander.js
+
+
+[![Build Status](https://api.travis-ci.org/tj/commander.js.svg?branch=master)](http://travis-ci.org/tj/commander.js)
+[![NPM Version](http://img.shields.io/npm/v/commander.svg?style=flat)](https://www.npmjs.org/package/commander)
+[![NPM Downloads](https://img.shields.io/npm/dm/commander.svg?style=flat)](https://www.npmjs.org/package/commander)
+[![Join the chat at https://gitter.im/tj/commander.js](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/tj/commander.js?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
+
+ The complete solution for [node.js](http://nodejs.org) command-line interfaces, inspired by Ruby's [commander](https://github.com/commander-rb/commander).
+ [API documentation](http://tj.github.com/commander.js/)
+
+
+## Installation
+
+ $ npm install commander --save
+
+## Option parsing
+
+ Options with commander are defined with the `.option()` method, also serving as documentation for the options. The example below parses args and options from `process.argv`, leaving remaining args as the `program.args` array which were not consumed by options.
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-p, --peppers', 'Add peppers')
+ .option('-P, --pineapple', 'Add pineapple')
+ .option('-b, --bbq-sauce', 'Add bbq sauce')
+ .option('-c, --cheese [type]', 'Add the specified type of cheese [marble]', 'marble')
+ .parse(process.argv);
+
+console.log('you ordered a pizza with:');
+if (program.peppers) console.log(' - peppers');
+if (program.pineapple) console.log(' - pineapple');
+if (program.bbqSauce) console.log(' - bbq');
+console.log(' - %s cheese', program.cheese);
+```
+
+ Short flags may be passed as a single arg, for example `-abc` is equivalent to `-a -b -c`. Multi-word options such as "--template-engine" are camel-cased, becoming `program.templateEngine` etc.
+
+
+## Coercion
+
+```js
+function range(val) {
+ return val.split('..').map(Number);
+}
+
+function list(val) {
+ return val.split(',');
+}
+
+function collect(val, memo) {
+ memo.push(val);
+ return memo;
+}
+
+function increaseVerbosity(v, total) {
+ return total + 1;
+}
+
+program
+ .version('0.1.0')
+ .usage('[options] <file ...>')
+ .option('-i, --integer <n>', 'An integer argument', parseInt)
+ .option('-f, --float <n>', 'A float argument', parseFloat)
+ .option('-r, --range <a>..<b>', 'A range', range)
+ .option('-l, --list <items>', 'A list', list)
+ .option('-o, --optional [value]', 'An optional value')
+ .option('-c, --collect [value]', 'A repeatable value', collect, [])
+ .option('-v, --verbose', 'A value that can be increased', increaseVerbosity, 0)
+ .parse(process.argv);
+
+console.log(' int: %j', program.integer);
+console.log(' float: %j', program.float);
+console.log(' optional: %j', program.optional);
+program.range = program.range || [];
+console.log(' range: %j..%j', program.range[0], program.range[1]);
+console.log(' list: %j', program.list);
+console.log(' collect: %j', program.collect);
+console.log(' verbosity: %j', program.verbose);
+console.log(' args: %j', program.args);
+```
+
+## Regular Expression
+```js
+program
+ .version('0.1.0')
+ .option('-s --size <size>', 'Pizza size', /^(large|medium|small)$/i, 'medium')
+ .option('-d --drink [drink]', 'Drink', /^(coke|pepsi|izze)$/i)
+ .parse(process.argv);
+
+console.log(' size: %j', program.size);
+console.log(' drink: %j', program.drink);
+```
+
+## Variadic arguments
+
+ The last argument of a command can be variadic, and only the last argument. To make an argument variadic you have to
+ append `...` to the argument name. Here is an example:
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .command('rmdir <dir> [otherDirs...]')
+ .action(function (dir, otherDirs) {
+ console.log('rmdir %s', dir);
+ if (otherDirs) {
+ otherDirs.forEach(function (oDir) {
+ console.log('rmdir %s', oDir);
+ });
+ }
+ });
+
+program.parse(process.argv);
+```
+
+ An `Array` is used for the value of a variadic argument. This applies to `program.args` as well as the argument passed
+ to your action as demonstrated above.
+
+## Specify the argument syntax
+
+```js
+#!/usr/bin/env node
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .arguments('<cmd> [env]')
+ .action(function (cmd, env) {
+ cmdValue = cmd;
+ envValue = env;
+ });
+
+program.parse(process.argv);
+
+if (typeof cmdValue === 'undefined') {
+ console.error('no command given!');
+ process.exit(1);
+}
+console.log('command:', cmdValue);
+console.log('environment:', envValue || "no environment given");
+```
+Angled brackets (e.g. `<cmd>`) indicate required input. Square brackets (e.g. `[env]`) indicate optional input.
+
+## Git-style sub-commands
+
+```js
+// file: ./examples/pm
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .command('install [name]', 'install one or more packages')
+ .command('search [query]', 'search with optional query')
+ .command('list', 'list packages installed', {isDefault: true})
+ .parse(process.argv);
+```
+
+When `.command()` is invoked with a description argument, no `.action(callback)` should be called to handle sub-commands, otherwise there will be an error. This tells commander that you're going to use separate executables for sub-commands, much like `git(1)` and other popular tools.
+The commander will try to search the executables in the directory of the entry script (like `./examples/pm`) with the name `program-command`, like `pm-install`, `pm-search`.
+
+Options can be passed with the call to `.command()`. Specifying `true` for `opts.noHelp` will remove the option from the generated help output. Specifying `true` for `opts.isDefault` will run the subcommand if no other subcommand is specified.
+
+If the program is designed to be installed globally, make sure the executables have proper modes, like `755`.
+
+### `--harmony`
+
+You can enable `--harmony` option in two ways:
+* Use `#! /usr/bin/env node --harmony` in the sub-commands scripts. Note some os version don’t support this pattern.
+* Use the `--harmony` option when call the command, like `node --harmony examples/pm publish`. The `--harmony` option will be preserved when spawning sub-command process.
+
+## Automated --help
+
+ The help information is auto-generated based on the information commander already knows about your program, so the following `--help` info is for free:
+
+```
+ $ ./examples/pizza --help
+
+ Usage: pizza [options]
+
+ An application for pizzas ordering
+
+ Options:
+
+ -h, --help output usage information
+ -V, --version output the version number
+ -p, --peppers Add peppers
+ -P, --pineapple Add pineapple
+ -b, --bbq Add bbq sauce
+ -c, --cheese <type> Add the specified type of cheese [marble]
+ -C, --no-cheese You do not want any cheese
+
+```
+
+## Custom help
+
+ You can display arbitrary `-h, --help` information
+ by listening for "--help". Commander will automatically
+ exit once you are done so that the remainder of your program
+ does not execute causing undesired behaviours, for example
+ in the following executable "stuff" will not output when
+ `--help` is used.
+
+```js
+#!/usr/bin/env node
+
+/**
+ * Module dependencies.
+ */
+
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-f, --foo', 'enable some foo')
+ .option('-b, --bar', 'enable some bar')
+ .option('-B, --baz', 'enable some baz');
+
+// must be before .parse() since
+// node's emit() is immediate
+
+program.on('--help', function(){
+ console.log(' Examples:');
+ console.log('');
+ console.log(' $ custom-help --help');
+ console.log(' $ custom-help -h');
+ console.log('');
+});
+
+program.parse(process.argv);
+
+console.log('stuff');
+```
+
+Yields the following help output when `node script-name.js -h` or `node script-name.js --help` are run:
+
+```
+
+Usage: custom-help [options]
+
+Options:
+
+ -h, --help output usage information
+ -V, --version output the version number
+ -f, --foo enable some foo
+ -b, --bar enable some bar
+ -B, --baz enable some baz
+
+Examples:
+
+ $ custom-help --help
+ $ custom-help -h
+
+```
+
+## .outputHelp(cb)
+
+Output help information without exiting.
+Optional callback cb allows post-processing of help text before it is displayed.
+
+If you want to display help by default (e.g. if no command was provided), you can use something like:
+
+```js
+var program = require('commander');
+var colors = require('colors');
+
+program
+ .version('0.1.0')
+ .command('getstream [url]', 'get stream URL')
+ .parse(process.argv);
+
+ if (!process.argv.slice(2).length) {
+ program.outputHelp(make_red);
+ }
+
+function make_red(txt) {
+ return colors.red(txt); //display the help text in red on the console
+}
+```
+
+## .help(cb)
+
+ Output help information and exit immediately.
+ Optional callback cb allows post-processing of help text before it is displayed.
+
+## Examples
+
+```js
+var program = require('commander');
+
+program
+ .version('0.1.0')
+ .option('-C, --chdir <path>', 'change the working directory')
+ .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
+ .option('-T, --no-tests', 'ignore test hook');
+
+program
+ .command('setup [env]')
+ .description('run setup commands for all envs')
+ .option("-s, --setup_mode [mode]", "Which setup mode to use")
+ .action(function(env, options){
+ var mode = options.setup_mode || "normal";
+ env = env || 'all';
+ console.log('setup for %s env(s) with %s mode', env, mode);
+ });
+
+program
+ .command('exec <cmd>')
+ .alias('ex')
+ .description('execute the given remote cmd')
+ .option("-e, --exec_mode <mode>", "Which exec mode to use")
+ .action(function(cmd, options){
+ console.log('exec "%s" using %s mode', cmd, options.exec_mode);
+ }).on('--help', function() {
+ console.log(' Examples:');
+ console.log();
+ console.log(' $ deploy exec sequential');
+ console.log(' $ deploy exec async');
+ console.log();
+ });
+
+program
+ .command('*')
+ .action(function(env){
+ console.log('deploying "%s"', env);
+ });
+
+program.parse(process.argv);
+```
+
+More Demos can be found in the [examples](https://github.com/tj/commander.js/tree/master/examples) directory.
+
+## License
+
+MIT
diff --git a/node_modules/commander/index.js b/node_modules/commander/index.js
new file mode 100644
index 0000000..d5dbe18
--- /dev/null
+++ b/node_modules/commander/index.js
@@ -0,0 +1,1137 @@
+/**
+ * Module dependencies.
+ */
+
+var EventEmitter = require('events').EventEmitter;
+var spawn = require('child_process').spawn;
+var path = require('path');
+var dirname = path.dirname;
+var basename = path.basename;
+var fs = require('fs');
+
+/**
+ * Expose the root command.
+ */
+
+exports = module.exports = new Command();
+
+/**
+ * Expose `Command`.
+ */
+
+exports.Command = Command;
+
+/**
+ * Expose `Option`.
+ */
+
+exports.Option = Option;
+
+/**
+ * Initialize a new `Option` with the given `flags` and `description`.
+ *
+ * @param {String} flags
+ * @param {String} description
+ * @api public
+ */
+
+function Option(flags, description) {
+ this.flags = flags;
+ this.required = ~flags.indexOf('<');
+ this.optional = ~flags.indexOf('[');
+ this.bool = !~flags.indexOf('-no-');
+ flags = flags.split(/[ ,|]+/);
+ if (flags.length > 1 && !/^[[<]/.test(flags[1])) this.short = flags.shift();
+ this.long = flags.shift();
+ this.description = description || '';
+}
+
+/**
+ * Return option name.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Option.prototype.name = function() {
+ return this.long
+ .replace('--', '')
+ .replace('no-', '');
+};
+
+/**
+ * Check if `arg` matches the short or long flag.
+ *
+ * @param {String} arg
+ * @return {Boolean}
+ * @api private
+ */
+
+Option.prototype.is = function(arg) {
+ return arg == this.short || arg == this.long;
+};
+
+/**
+ * Initialize a new `Command`.
+ *
+ * @param {String} name
+ * @api public
+ */
+
+function Command(name) {
+ this.commands = [];
+ this.options = [];
+ this._execs = {};
+ this._allowUnknownOption = false;
+ this._args = [];
+ this._name = name || '';
+}
+
+/**
+ * Inherit from `EventEmitter.prototype`.
+ */
+
+Command.prototype.__proto__ = EventEmitter.prototype;
+
+/**
+ * Add command `name`.
+ *
+ * The `.action()` callback is invoked when the
+ * command `name` is specified via __ARGV__,
+ * and the remaining arguments are applied to the
+ * function for access.
+ *
+ * When the `name` is "*" an un-matched command
+ * will be passed as the first arg, followed by
+ * the rest of __ARGV__ remaining.
+ *
+ * Examples:
+ *
+ * program
+ * .version('0.0.1')
+ * .option('-C, --chdir <path>', 'change the working directory')
+ * .option('-c, --config <path>', 'set config path. defaults to ./deploy.conf')
+ * .option('-T, --no-tests', 'ignore test hook')
+ *
+ * program
+ * .command('setup')
+ * .description('run remote setup commands')
+ * .action(function() {
+ * console.log('setup');
+ * });
+ *
+ * program
+ * .command('exec <cmd>')
+ * .description('run the given remote command')
+ * .action(function(cmd) {
+ * console.log('exec "%s"', cmd);
+ * });
+ *
+ * program
+ * .command('teardown <dir> [otherDirs...]')
+ * .description('run teardown commands')
+ * .action(function(dir, otherDirs) {
+ * console.log('dir "%s"', dir);
+ * if (otherDirs) {
+ * otherDirs.forEach(function (oDir) {
+ * console.log('dir "%s"', oDir);
+ * });
+ * }
+ * });
+ *
+ * program
+ * .command('*')
+ * .description('deploy the given env')
+ * .action(function(env) {
+ * console.log('deploying "%s"', env);
+ * });
+ *
+ * program.parse(process.argv);
+ *
+ * @param {String} name
+ * @param {String} [desc] for git-style sub-commands
+ * @return {Command} the new command
+ * @api public
+ */
+
+Command.prototype.command = function(name, desc, opts) {
+ opts = opts || {};
+ var args = name.split(/ +/);
+ var cmd = new Command(args.shift());
+
+ if (desc) {
+ cmd.description(desc);
+ this.executables = true;
+ this._execs[cmd._name] = true;
+ if (opts.isDefault) this.defaultExecutable = cmd._name;
+ }
+
+ cmd._noHelp = !!opts.noHelp;
+ this.commands.push(cmd);
+ cmd.parseExpectedArgs(args);
+ cmd.parent = this;
+
+ if (desc) return this;
+ return cmd;
+};
+
+/**
+ * Define argument syntax for the top-level command.
+ *
+ * @api public
+ */
+
+Command.prototype.arguments = function (desc) {
+ return this.parseExpectedArgs(desc.split(/ +/));
+};
+
+/**
+ * Add an implicit `help [cmd]` subcommand
+ * which invokes `--help` for the given command.
+ *
+ * @api private
+ */
+
+Command.prototype.addImplicitHelpCommand = function() {
+ this.command('help [cmd]', 'display help for [cmd]');
+};
+
+/**
+ * Parse expected `args`.
+ *
+ * For example `["[type]"]` becomes `[{ required: false, name: 'type' }]`.
+ *
+ * @param {Array} args
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.parseExpectedArgs = function(args) {
+ if (!args.length) return;
+ var self = this;
+ args.forEach(function(arg) {
+ var argDetails = {
+ required: false,
+ name: '',
+ variadic: false
+ };
+
+ switch (arg[0]) {
+ case '<':
+ argDetails.required = true;
+ argDetails.name = arg.slice(1, -1);
+ break;
+ case '[':
+ argDetails.name = arg.slice(1, -1);
+ break;
+ }
+
+ if (argDetails.name.length > 3 && argDetails.name.slice(-3) === '...') {
+ argDetails.variadic = true;
+ argDetails.name = argDetails.name.slice(0, -3);
+ }
+ if (argDetails.name) {
+ self._args.push(argDetails);
+ }
+ });
+ return this;
+};
+
+/**
+ * Register callback `fn` for the command.
+ *
+ * Examples:
+ *
+ * program
+ * .command('help')
+ * .description('display verbose help')
+ * .action(function() {
+ * // output help here
+ * });
+ *
+ * @param {Function} fn
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.action = function(fn) {
+ var self = this;
+ var listener = function(args, unknown) {
+ // Parse any so-far unknown options
+ args = args || [];
+ unknown = unknown || [];
+
+ var parsed = self.parseOptions(unknown);
+
+ // Output help if necessary
+ outputHelpIfNecessary(self, parsed.unknown);
+
+ // If there are still any unknown options, then we simply
+ // die, unless someone asked for help, in which case we give it
+ // to them, and then we die.
+ if (parsed.unknown.length > 0) {
+ self.unknownOption(parsed.unknown[0]);
+ }
+
+ // Leftover arguments need to be pushed back. Fixes issue #56
+ if (parsed.args.length) args = parsed.args.concat(args);
+
+ self._args.forEach(function(arg, i) {
+ if (arg.required && null == args[i]) {
+ self.missingArgument(arg.name);
+ } else if (arg.variadic) {
+ if (i !== self._args.length - 1) {
+ self.variadicArgNotLast(arg.name);
+ }
+
+ args[i] = args.splice(i);
+ }
+ });
+
+ // Always append ourselves to the end of the arguments,
+ // to make sure we match the number of arguments the user
+ // expects
+ if (self._args.length) {
+ args[self._args.length] = self;
+ } else {
+ args.push(self);
+ }
+
+ fn.apply(self, args);
+ };
+ var parent = this.parent || this;
+ var name = parent === this ? '*' : this._name;
+ parent.on('command:' + name, listener);
+ if (this._alias) parent.on('command:' + this._alias, listener);
+ return this;
+};
+
+/**
+ * Define option with `flags`, `description` and optional
+ * coercion `fn`.
+ *
+ * The `flags` string should contain both the short and long flags,
+ * separated by comma, a pipe or space. The following are all valid
+ * all will output this way when `--help` is used.
+ *
+ * "-p, --pepper"
+ * "-p|--pepper"
+ * "-p --pepper"
+ *
+ * Examples:
+ *
+ * // simple boolean defaulting to false
+ * program.option('-p, --pepper', 'add pepper');
+ *
+ * --pepper
+ * program.pepper
+ * // => Boolean
+ *
+ * // simple boolean defaulting to true
+ * program.option('-C, --no-cheese', 'remove cheese');
+ *
+ * program.cheese
+ * // => true
+ *
+ * --no-cheese
+ * program.cheese
+ * // => false
+ *
+ * // required argument
+ * program.option('-C, --chdir <path>', 'change the working directory');
+ *
+ * --chdir /tmp
+ * program.chdir
+ * // => "/tmp"
+ *
+ * // optional argument
+ * program.option('-c, --cheese [type]', 'add cheese [marble]');
+ *
+ * @param {String} flags
+ * @param {String} description
+ * @param {Function|*} [fn] or default
+ * @param {*} [defaultValue]
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.option = function(flags, description, fn, defaultValue) {
+ var self = this
+ , option = new Option(flags, description)
+ , oname = option.name()
+ , name = camelcase(oname);
+
+ // default as 3rd arg
+ if (typeof fn != 'function') {
+ if (fn instanceof RegExp) {
+ var regex = fn;
+ fn = function(val, def) {
+ var m = regex.exec(val);
+ return m ? m[0] : def;
+ }
+ }
+ else {
+ defaultValue = fn;
+ fn = null;
+ }
+ }
+
+ // preassign default value only for --no-*, [optional], or <required>
+ if (false == option.bool || option.optional || option.required) {
+ // when --no-* we make sure default is true
+ if (false == option.bool) defaultValue = true;
+ // preassign only if we have a default
+ if (undefined !== defaultValue) self[name] = defaultValue;
+ }
+
+ // register the option
+ this.options.push(option);
+
+ // when it's passed assign the value
+ // and conditionally invoke the callback
+ this.on('option:' + oname, function(val) {
+ // coercion
+ if (null !== val && fn) val = fn(val, undefined === self[name]
+ ? defaultValue
+ : self[name]);
+
+ // unassigned or bool
+ if ('boolean' == typeof self[name] || 'undefined' == typeof self[name]) {
+ // if no value, bool true, and we have a default, then use it!
+ if (null == val) {
+ self[name] = option.bool
+ ? defaultValue || true
+ : false;
+ } else {
+ self[name] = val;
+ }
+ } else if (null !== val) {
+ // reassign
+ self[name] = val;
+ }
+ });
+
+ return this;
+};
+
+/**
+ * Allow unknown options on the command line.
+ *
+ * @param {Boolean} arg if `true` or omitted, no error will be thrown
+ * for unknown options.
+ * @api public
+ */
+Command.prototype.allowUnknownOption = function(arg) {
+ this._allowUnknownOption = arguments.length === 0 || arg;
+ return this;
+};
+
+/**
+ * Parse `argv`, settings options and invoking commands when defined.
+ *
+ * @param {Array} argv
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.parse = function(argv) {
+ // implicit help
+ if (this.executables) this.addImplicitHelpCommand();
+
+ // store raw args
+ this.rawArgs = argv;
+
+ // guess name
+ this._name = this._name || basename(argv[1], '.js');
+
+ // github-style sub-commands with no sub-command
+ if (this.executables && argv.length < 3 && !this.defaultExecutable) {
+ // this user needs help
+ argv.push('--help');
+ }
+
+ // process argv
+ var parsed = this.parseOptions(this.normalize(argv.slice(2)));
+ var args = this.args = parsed.args;
+
+ var result = this.parseArgs(this.args, parsed.unknown);
+
+ // executable sub-commands
+ var name = result.args[0];
+
+ var aliasCommand = null;
+ // check alias of sub commands
+ if (name) {
+ aliasCommand = this.commands.filter(function(command) {
+ return command.alias() === name;
+ })[0];
+ }
+
+ if (this._execs[name] && typeof this._execs[name] != "function") {
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ } else if (aliasCommand) {
+ // is alias of a subCommand
+ args[0] = aliasCommand._name;
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ } else if (this.defaultExecutable) {
+ // use the default subcommand
+ args.unshift(this.defaultExecutable);
+ return this.executeSubCommand(argv, args, parsed.unknown);
+ }
+
+ return result;
+};
+
+/**
+ * Execute a sub-command executable.
+ *
+ * @param {Array} argv
+ * @param {Array} args
+ * @param {Array} unknown
+ * @api private
+ */
+
+Command.prototype.executeSubCommand = function(argv, args, unknown) {
+ args = args.concat(unknown);
+
+ if (!args.length) this.help();
+ if ('help' == args[0] && 1 == args.length) this.help();
+
+ // <cmd> --help
+ if ('help' == args[0]) {
+ args[0] = args[1];
+ args[1] = '--help';
+ }
+
+ // executable
+ var f = argv[1];
+ // name of the subcommand, link `pm-install`
+ var bin = basename(f, '.js') + '-' + args[0];
+
+
+ // In case of globally installed, get the base dir where executable
+ // subcommand file should be located at
+ var baseDir
+ , link = fs.lstatSync(f).isSymbolicLink() ? fs.readlinkSync(f) : f;
+
+ // when symbolink is relative path
+ if (link !== f && link.charAt(0) !== '/') {
+ link = path.join(dirname(f), link)
+ }
+ baseDir = dirname(link);
+
+ // prefer local `./<bin>` to bin in the $PATH
+ var localBin = path.join(baseDir, bin);
+
+ // whether bin file is a js script with explicit `.js` extension
+ var isExplicitJS = false;
+ if (exists(localBin + '.js')) {
+ bin = localBin + '.js';
+ isExplicitJS = true;
+ } else if (exists(localBin)) {
+ bin = localBin;
+ }
+
+ args = args.slice(1);
+
+ var proc;
+ if (process.platform !== 'win32') {
+ if (isExplicitJS) {
+ args.unshift(bin);
+ // add executable arguments to spawn
+ args = (process.execArgv || []).concat(args);
+
+ proc = spawn('node', args, { stdio: 'inherit', customFds: [0, 1, 2] });
+ } else {
+ proc = spawn(bin, args, { stdio: 'inherit', customFds: [0, 1, 2] });
+ }
+ } else {
+ args.unshift(bin);
+ proc = spawn(process.execPath, args, { stdio: 'inherit'});
+ }
+
+ var signals = ['SIGUSR1', 'SIGUSR2', 'SIGTERM', 'SIGINT', 'SIGHUP'];
+ signals.forEach(function(signal) {
+ process.on(signal, function(){
+ if ((proc.killed === false) && (proc.exitCode === null)){
+ proc.kill(signal);
+ }
+ });
+ });
+ proc.on('close', process.exit.bind(process));
+ proc.on('error', function(err) {
+ if (err.code == "ENOENT") {
+ console.error('\n %s(1) does not exist, try --help\n', bin);
+ } else if (err.code == "EACCES") {
+ console.error('\n %s(1) not executable. try chmod or run with root\n', bin);
+ }
+ process.exit(1);
+ });
+
+ // Store the reference to the child process
+ this.runningCommand = proc;
+};
+
+/**
+ * Normalize `args`, splitting joined short flags. For example
+ * the arg "-abc" is equivalent to "-a -b -c".
+ * This also normalizes equal sign and splits "--abc=def" into "--abc def".
+ *
+ * @param {Array} args
+ * @return {Array}
+ * @api private
+ */
+
+Command.prototype.normalize = function(args) {
+ var ret = []
+ , arg
+ , lastOpt
+ , index;
+
+ for (var i = 0, len = args.length; i < len; ++i) {
+ arg = args[i];
+ if (i > 0) {
+ lastOpt = this.optionFor(args[i-1]);
+ }
+
+ if (arg === '--') {
+ // Honor option terminator
+ ret = ret.concat(args.slice(i));
+ break;
+ } else if (lastOpt && lastOpt.required) {
+ ret.push(arg);
+ } else if (arg.length > 1 && '-' == arg[0] && '-' != arg[1]) {
+ arg.slice(1).split('').forEach(function(c) {
+ ret.push('-' + c);
+ });
+ } else if (/^--/.test(arg) && ~(index = arg.indexOf('='))) {
+ ret.push(arg.slice(0, index), arg.slice(index + 1));
+ } else {
+ ret.push(arg);
+ }
+ }
+
+ return ret;
+};
+
+/**
+ * Parse command `args`.
+ *
+ * When listener(s) are available those
+ * callbacks are invoked, otherwise the "*"
+ * event is emitted and those actions are invoked.
+ *
+ * @param {Array} args
+ * @return {Command} for chaining
+ * @api private
+ */
+
+Command.prototype.parseArgs = function(args, unknown) {
+ var name;
+
+ if (args.length) {
+ name = args[0];
+ if (this.listeners('command:' + name).length) {
+ this.emit('command:' + args.shift(), args, unknown);
+ } else {
+ this.emit('command:*', args);
+ }
+ } else {
+ outputHelpIfNecessary(this, unknown);
+
+ // If there were no args and we have unknown options,
+ // then they are extraneous and we need to error.
+ if (unknown.length > 0) {
+ this.unknownOption(unknown[0]);
+ }
+ }
+
+ return this;
+};
+
+/**
+ * Return an option matching `arg` if any.
+ *
+ * @param {String} arg
+ * @return {Option}
+ * @api private
+ */
+
+Command.prototype.optionFor = function(arg) {
+ for (var i = 0, len = this.options.length; i < len; ++i) {
+ if (this.options[i].is(arg)) {
+ return this.options[i];
+ }
+ }
+};
+
+/**
+ * Parse options from `argv` returning `argv`
+ * void of these options.
+ *
+ * @param {Array} argv
+ * @return {Array}
+ * @api public
+ */
+
+Command.prototype.parseOptions = function(argv) {
+ var args = []
+ , len = argv.length
+ , literal
+ , option
+ , arg;
+
+ var unknownOptions = [];
+
+ // parse options
+ for (var i = 0; i < len; ++i) {
+ arg = argv[i];
+
+ // literal args after --
+ if (literal) {
+ args.push(arg);
+ continue;
+ }
+
+ if ('--' == arg) {
+ literal = true;
+ continue;
+ }
+
+ // find matching Option
+ option = this.optionFor(arg);
+
+ // option is defined
+ if (option) {
+ // requires arg
+ if (option.required) {
+ arg = argv[++i];
+ if (null == arg) return this.optionMissingArgument(option);
+ this.emit('option:' + option.name(), arg);
+ // optional arg
+ } else if (option.optional) {
+ arg = argv[i+1];
+ if (null == arg || ('-' == arg[0] && '-' != arg)) {
+ arg = null;
+ } else {
+ ++i;
+ }
+ this.emit('option:' + option.name(), arg);
+ // bool
+ } else {
+ this.emit('option:' + option.name());
+ }
+ continue;
+ }
+
+ // looks like an option
+ if (arg.length > 1 && '-' == arg[0]) {
+ unknownOptions.push(arg);
+
+ // If the next argument looks like it might be
+ // an argument for this option, we pass it on.
+ // If it isn't, then it'll simply be ignored
+ if (argv[i+1] && '-' != argv[i+1][0]) {
+ unknownOptions.push(argv[++i]);
+ }
+ continue;
+ }
+
+ // arg
+ args.push(arg);
+ }
+
+ return { args: args, unknown: unknownOptions };
+};
+
+/**
+ * Return an object containing options as key-value pairs
+ *
+ * @return {Object}
+ * @api public
+ */
+Command.prototype.opts = function() {
+ var result = {}
+ , len = this.options.length;
+
+ for (var i = 0 ; i < len; i++) {
+ var key = camelcase(this.options[i].name());
+ result[key] = key === 'version' ? this._version : this[key];
+ }
+ return result;
+};
+
+/**
+ * Argument `name` is missing.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+Command.prototype.missingArgument = function(name) {
+ console.error();
+ console.error(" error: missing required argument `%s'", name);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * `Option` is missing an argument, but received `flag` or nothing.
+ *
+ * @param {String} option
+ * @param {String} flag
+ * @api private
+ */
+
+Command.prototype.optionMissingArgument = function(option, flag) {
+ console.error();
+ if (flag) {
+ console.error(" error: option `%s' argument missing, got `%s'", option.flags, flag);
+ } else {
+ console.error(" error: option `%s' argument missing", option.flags);
+ }
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Unknown option `flag`.
+ *
+ * @param {String} flag
+ * @api private
+ */
+
+Command.prototype.unknownOption = function(flag) {
+ if (this._allowUnknownOption) return;
+ console.error();
+ console.error(" error: unknown option `%s'", flag);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Variadic argument with `name` is not the last argument as required.
+ *
+ * @param {String} name
+ * @api private
+ */
+
+Command.prototype.variadicArgNotLast = function(name) {
+ console.error();
+ console.error(" error: variadic arguments must be last `%s'", name);
+ console.error();
+ process.exit(1);
+};
+
+/**
+ * Set the program version to `str`.
+ *
+ * This method auto-registers the "-V, --version" flag
+ * which will print the version number when passed.
+ *
+ * @param {String} str
+ * @param {String} [flags]
+ * @return {Command} for chaining
+ * @api public
+ */
+
+Command.prototype.version = function(str, flags) {
+ if (0 == arguments.length) return this._version;
+ this._version = str;
+ flags = flags || '-V, --version';
+ this.option(flags, 'output the version number');
+ this.on('option:version', function() {
+ process.stdout.write(str + '\n');
+ process.exit(0);
+ });
+ return this;
+};
+
+/**
+ * Set the description to `str`.
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.description = function(str) {
+ if (0 === arguments.length) return this._description;
+ this._description = str;
+ return this;
+};
+
+/**
+ * Set an alias for the command
+ *
+ * @param {String} alias
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.alias = function(alias) {
+ var command = this;
+ if(this.commands.length !== 0) {
+ command = this.commands[this.commands.length - 1]
+ }
+
+ if (arguments.length === 0) return command._alias;
+
+ command._alias = alias;
+ return this;
+};
+
+/**
+ * Set / get the command usage `str`.
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.usage = function(str) {
+ var args = this._args.map(function(arg) {
+ return humanReadableArgName(arg);
+ });
+
+ var usage = '[options]'
+ + (this.commands.length ? ' [command]' : '')
+ + (this._args.length ? ' ' + args.join(' ') : '');
+
+ if (0 == arguments.length) return this._usage || usage;
+ this._usage = str;
+
+ return this;
+};
+
+/**
+ * Get or set the name of the command
+ *
+ * @param {String} str
+ * @return {String|Command}
+ * @api public
+ */
+
+Command.prototype.name = function(str) {
+ if (0 === arguments.length) return this._name;
+ this._name = str;
+ return this;
+};
+
+/**
+ * Return the largest option length.
+ *
+ * @return {Number}
+ * @api private
+ */
+
+Command.prototype.largestOptionLength = function() {
+ return this.options.reduce(function(max, option) {
+ return Math.max(max, option.flags.length);
+ }, 0);
+};
+
+/**
+ * Return help for options.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.optionHelp = function() {
+ var width = this.largestOptionLength();
+
+ // Append the help information
+ return this.options.map(function(option) {
+ return pad(option.flags, width) + ' ' + option.description;
+ }).concat([pad('-h, --help', width) + ' ' + 'output usage information'])
+ .join('\n');
+};
+
+/**
+ * Return command help documentation.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.commandHelp = function() {
+ if (!this.commands.length) return '';
+
+ var commands = this.commands.filter(function(cmd) {
+ return !cmd._noHelp;
+ }).map(function(cmd) {
+ var args = cmd._args.map(function(arg) {
+ return humanReadableArgName(arg);
+ }).join(' ');
+
+ return [
+ cmd._name
+ + (cmd._alias ? '|' + cmd._alias : '')
+ + (cmd.options.length ? ' [options]' : '')
+ + ' ' + args
+ , cmd._description
+ ];
+ });
+
+ var width = commands.reduce(function(max, command) {
+ return Math.max(max, command[0].length);
+ }, 0);
+
+ return [
+ ''
+ , ' Commands:'
+ , ''
+ , commands.map(function(cmd) {
+ var desc = cmd[1] ? ' ' + cmd[1] : '';
+ return pad(cmd[0], width) + desc;
+ }).join('\n').replace(/^/gm, ' ')
+ , ''
+ ].join('\n');
+};
+
+/**
+ * Return program help documentation.
+ *
+ * @return {String}
+ * @api private
+ */
+
+Command.prototype.helpInformation = function() {
+ var desc = [];
+ if (this._description) {
+ desc = [
+ ' ' + this._description
+ , ''
+ ];
+ }
+
+ var cmdName = this._name;
+ if (this._alias) {
+ cmdName = cmdName + '|' + this._alias;
+ }
+ var usage = [
+ ''
+ ,' Usage: ' + cmdName + ' ' + this.usage()
+ , ''
+ ];
+
+ var cmds = [];
+ var commandHelp = this.commandHelp();
+ if (commandHelp) cmds = [commandHelp];
+
+ var options = [
+ ''
+ , ' Options:'
+ , ''
+ , '' + this.optionHelp().replace(/^/gm, ' ')
+ , ''
+ ];
+
+ return usage
+ .concat(desc)
+ .concat(options)
+ .concat(cmds)
+ .join('\n');
+};
+
+/**
+ * Output help information for this command
+ *
+ * @api public
+ */
+
+Command.prototype.outputHelp = function(cb) {
+ if (!cb) {
+ cb = function(passthru) {
+ return passthru;
+ }
+ }
+ process.stdout.write(cb(this.helpInformation()));
+ this.emit('--help');
+};
+
+/**
+ * Output help information and exit.
+ *
+ * @api public
+ */
+
+Command.prototype.help = function(cb) {
+ this.outputHelp(cb);
+ process.exit();
+};
+
+/**
+ * Camel-case the given `flag`
+ *
+ * @param {String} flag
+ * @return {String}
+ * @api private
+ */
+
+function camelcase(flag) {
+ return flag.split('-').reduce(function(str, word) {
+ return str + word[0].toUpperCase() + word.slice(1);
+ });
+}
+
+/**
+ * Pad `str` to `width`.
+ *
+ * @param {String} str
+ * @param {Number} width
+ * @return {String}
+ * @api private
+ */
+
+function pad(str, width) {
+ var len = Math.max(0, width - str.length);
+ return str + Array(len + 1).join(' ');
+}
+
+/**
+ * Output help information if necessary
+ *
+ * @param {Command} command to output help for
+ * @param {Array} array of options to search for -h or --help
+ * @api private
+ */
+
+function outputHelpIfNecessary(cmd, options) {
+ options = options || [];
+ for (var i = 0; i < options.length; i++) {
+ if (options[i] == '--help' || options[i] == '-h') {
+ cmd.outputHelp();
+ process.exit(0);
+ }
+ }
+}
+
+/**
+ * Takes an argument an returns its human readable equivalent for help usage.
+ *
+ * @param {Object} arg
+ * @return {String}
+ * @api private
+ */
+
+function humanReadableArgName(arg) {
+ var nameOutput = arg.name + (arg.variadic === true ? '...' : '');
+
+ return arg.required
+ ? '<' + nameOutput + '>'
+ : '[' + nameOutput + ']'
+}
+
+// for versions before node v0.8 when there weren't `fs.existsSync`
+function exists(file) {
+ try {
+ if (fs.statSync(file).isFile()) {
+ return true;
+ }
+ } catch (e) {
+ return false;
+ }
+}
+
diff --git a/node_modules/commander/package.json b/node_modules/commander/package.json
new file mode 100644
index 0000000..708f223
--- /dev/null
+++ b/node_modules/commander/package.json
@@ -0,0 +1,29 @@
+{
+ "name": "commander",
+ "version": "2.11.0",
+ "description": "the complete solution for node.js command-line programs",
+ "keywords": [
+ "commander",
+ "command",
+ "option",
+ "parser"
+ ],
+ "author": "TJ Holowaychuk <tj@vision-media.ca>",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/tj/commander.js.git"
+ },
+ "devDependencies": {
+ "should": "^11.2.1",
+ "sinon": "^2.3.5"
+ },
+ "scripts": {
+ "test": "make test"
+ },
+ "main": "index",
+ "files": [
+ "index.js"
+ ],
+ "dependencies": {}
+}
diff --git a/node_modules/sizzle/AUTHORS.txt b/node_modules/sizzle/AUTHORS.txt
new file mode 100644
index 0000000..d510ad0
--- /dev/null
+++ b/node_modules/sizzle/AUTHORS.txt
@@ -0,0 +1,67 @@
+Authors ordered by first contribution
+
+John Resig <jeresig@gmail.com>
+Cheah Chu Yeow <chuyeow@gmail.com>
+Andrew Chalkley <andrew@chalkley.org>
+Fabio Buffoni <fabio.buffoni@bitmaster.it>
+Stefan Bauckmeier  <stefan@bauckmeier.de>
+Brandon Aaron <brandon.aaron@gmail.com>
+Anton Kovalyov <anton@kovalyov.net>
+Dušan B. Jovanovic <dbjdbj@gmail.com>
+Riccardo De Agostini <rdeago@gmail.com>
+Fabian Jakobs <fabian.jakobs@web.de>
+Karl Swedberg <kswedberg@gmail.com>
+Jake Archibald <jake.archibald@bbc.co.uk>
+Colin Snover <github.com@zetafleet.com>
+Anton Matzneller <obhvsbypqghgc@gmail.com>
+Dave Methvin <dave.methvin@gmail.com>
+Corey Frang <gnarf@gnarf.net>
+Mathias Bynens <mathias@qiwi.be>
+John Firebaugh <john_firebaugh@us.ibm.com>
+Timmy Willison <timmywillisn@gmail.com>
+Mike Sherov <mike.sherov@gmail.com>
+Rock Hymas <rock@fogcreek.com>
+Yehuda Katz <wycats@gmail.com>
+Jörn Zaefferer <joern.zaefferer@gmail.com>
+Richard Gibson <richard.gibson@gmail.com>
+Vitya Muhachev <vic99999@yandex.ru>
+Henri Wiechers <hwiechers@gmail.com>
+Alan Plum <github@ap.apsq.de>
+Chad Killingsworth <chadkillingsworth@missouristate.edu>
+Markus Staab <markus.staab@redaxo.de>
+Timo Tijhof <krinklemail@gmail.com>
+Diego Tres <diegotres@gmail.com>
+Jonathan Sampson <jjdsampson@gmail.com>
+Pascal Borreli <pascal@borreli.com>
+Daniel Herman <daniel.c.herman@gmail.com>
+Frederic Junod <frederic.junod@camptocamp.com>
+Mitch Foley <mitch@thefoley.net>
+Scott González <scott.gonzalez@gmail.com>
+Oleg Gaidarenko <markelog@gmail.com>
+Dan Burzo <danburzo@gmail.com>
+Goare Mao <mygoare@gmail.com>
+Dongseok Paeng <dongseok83.paeng@lge.com>
+Michał Gołębiowski-Owczarek <m.goleb@gmail.com>
+Philip Jägenstedt <philip@foolip.org>
+Chris Antaki <ChrisAntaki@gmail.com>
+Benjamin Tan <demoneaux@gmail.com>
+T.J. Crowder <tj.crowder@farsightsoftware.com>
+Anne-Gaelle Colom <coloma@westminster.ac.uk>
+Neftaly Hernandez <neftaly.hernandez@gmail.com>
+Jörn Wagner <joern.wagner@explicatis.com>
+Chris Rebert <code@rebertia.com>
+Colin Frick <colin@bash.li>
+John-David Dalton <john.david.dalton@gmail.com>
+Kevin Kirsche <Kev.Kirsche+GitHub@gmail.com>
+Steve Mao <maochenyan@gmail.com>
+Tom von Clef <thomas.vonclef@gmail.com>
+Josh Soref <apache@soref.com>
+Saptak Sengupta <saptak013@gmail.com>
+Jon Dufresne <jon.dufresne@gmail.com>
+Andrey Meshkov <ay.meshkov@gmail.com>
+Sébastien Règne <regseb@users.noreply.github.com>
+Andrey Meshkov <am@adguard.com>
+Siddharth Dungarwal <sd5869@gmail.com>
+wartmanm <3869625+wartmanm@users.noreply.github.com>
+Hoang <dangkyokhoang@gmail.com>
+JuanMa Ruiz <ruizjuanma@gmail.com>
diff --git a/node_modules/sizzle/LICENSE.txt b/node_modules/sizzle/LICENSE.txt
new file mode 100644
index 0000000..88fcd17
--- /dev/null
+++ b/node_modules/sizzle/LICENSE.txt
@@ -0,0 +1,36 @@
+Copyright JS Foundation and other contributors, https://js.foundation/
+
+This software consists of voluntary contributions made by many
+individuals. For exact contribution history, see the revision history
+available at https://github.com/jquery/sizzle
+
+The following license applies to all parts of this software except as
+documented below:
+
+====
+
+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.
+
+====
+
+All files located in the node_modules and external directories are
+externally maintained libraries used by this software which have their
+own licenses; we recommend you read them, as their terms may differ from
+the terms above.
diff --git a/node_modules/sizzle/README.md b/node_modules/sizzle/README.md
new file mode 100644
index 0000000..8ca764f
--- /dev/null
+++ b/node_modules/sizzle/README.md
@@ -0,0 +1,55 @@
+# Sizzle
+
+__A pure-JavaScript CSS selector engine designed to be easily dropped in to a host library.__
+
+- [More information](https://sizzlejs.com/)
+- [Documentation](https://github.com/jquery/sizzle/wiki/)
+- [Browser support](https://github.com/jquery/sizzle/wiki/#wiki-browsers)
+
+Contribution Guides
+---------------------------
+
+In the spirit of open source software development, jQuery always encourages community code contribution. To help you get started and before you jump into writing code, be sure to read these important contribution guidelines thoroughly:
+
+1. [Getting Involved](https://contribute.jquery.org/)
+2. [JavaScript Style Guide](https://contribute.jquery.org/style-guide/js/)
+3. [Writing Code for jQuery Organization Projects](https://contribute.jquery.org/code/)
+
+What you need to build Sizzle
+---------------------------
+
+In order to build Sizzle, you should have Node.js/npm latest and git 1.7 or later (earlier versions might work OK, but are not tested).
+
+For Windows you have to download and install [git](http://git-scm.com/downloads) and [Node.js](https://nodejs.org/download/).
+
+Mac OS users should install [Homebrew](http://mxcl.github.com/homebrew/). Once Homebrew is installed, run `brew install git` to install git,
+and `brew install node` to install Node.js.
+
+Linux/BSD users should use their appropriate package managers to install git and Node.js, or build from source
+if you swing that way. Easy-peasy.
+
+
+How to build Sizzle
+----------------------------
+
+Clone a copy of the main Sizzle git repo by running:
+
+```bash
+git clone git://github.com/jquery/sizzle.git
+```
+
+In the `sizzle/dist` folder you will find build version of sizzle along with the minified copy and associated map file.
+
+Testing
+----------------------------
+
+- Run `npm install`, it's also preferable (but not necessarily) to globally install `grunt-cli` package – `npm install -g grunt-cli`
+- Open `test/index.html` in the browser. Or run `npm test`/`grunt test` on the command line, if environment variables `BROWSER_STACK_USERNAME` and `BROWSER_STACK_ACCESS_KEY` are set up, it will attempt to use [Browserstack](https://www.browserstack.com/) service (you will need to install java on your machine so browserstack could connect to your local server), otherwise [PhantomJS](http://phantomjs.org/) will be used.
+- The actual unit tests are in the `test/unit` directory.
+
+Developing with [grunt](http://gruntjs.com)
+----------------------------
+
+- `npm run build` or `grunt` will lint, build, test, and compare the sizes of the built files.
+- `npm start` or `grunt start` can be run to re-lint, re-build, and re-test files as you change them.
+- `grunt -help` will show other available commands.
diff --git a/node_modules/sizzle/dist/sizzle.js b/node_modules/sizzle/dist/sizzle.js
new file mode 100644
index 0000000..456b233
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.js
@@ -0,0 +1,2514 @@
+/*!
+ * Sizzle CSS Selector Engine v2.3.10
+ * https://sizzlejs.com/
+ *
+ * Copyright JS Foundation and other contributors
+ * Released under the MIT license
+ * https://js.foundation/
+ *
+ * Date: 2023-02-14
+ */
+( function( window ) {
+var i,
+ support,
+ Expr,
+ getText,
+ isXML,
+ tokenize,
+ compile,
+ select,
+ outermostContext,
+ sortInput,
+ hasDuplicate,
+
+ // Local document vars
+ setDocument,
+ document,
+ docElem,
+ documentIsHTML,
+ rbuggyQSA,
+ rbuggyMatches,
+ matches,
+ contains,
+
+ // Instance-specific data
+ expando = "sizzle" + 1 * new Date(),
+ preferredDoc = window.document,
+ dirruns = 0,
+ done = 0,
+ classCache = createCache(),
+ tokenCache = createCache(),
+ compilerCache = createCache(),
+ nonnativeSelectorCache = createCache(),
+ sortOrder = function( a, b ) {
+ if ( a === b ) {
+ hasDuplicate = true;
+ }
+ return 0;
+ },
+
+ // Instance methods
+ hasOwn = ( {} ).hasOwnProperty,
+ arr = [],
+ pop = arr.pop,
+ pushNative = arr.push,
+ push = arr.push,
+ slice = arr.slice,
+
+ // Use a stripped-down indexOf as it's faster than native
+ // https://jsperf.com/thor-indexof-vs-for/5
+ indexOf = function( list, elem ) {
+ var i = 0,
+ len = list.length;
+ for ( ; i < len; i++ ) {
+ if ( list[ i ] === elem ) {
+ return i;
+ }
+ }
+ return -1;
+ },
+
+ booleans = "checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|" +
+ "ismap|loop|multiple|open|readonly|required|scoped",
+
+ // Regular expressions
+
+ // http://www.w3.org/TR/css3-selectors/#whitespace
+ whitespace = "[\\x20\\t\\r\\n\\f]",
+
+ // https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ identifier = "(?:\\\\[\\da-fA-F]{1,6}" + whitespace +
+ "?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",
+
+ // Attribute selectors: http://www.w3.org/TR/selectors/#attribute-selectors
+ attributes = "\\[" + whitespace + "*(" + identifier + ")(?:" + whitespace +
+
+ // Operator (capture 2)
+ "*([*^$|!~]?=)" + whitespace +
+
+ // "Attribute values must be CSS identifiers [capture 5]
+ // or strings [capture 3 or capture 4]"
+ "*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|(" + identifier + "))|)" +
+ whitespace + "*\\]",
+
+ pseudos = ":(" + identifier + ")(?:\\((" +
+
+ // To reduce the number of selectors needing tokenize in the preFilter, prefer arguments:
+ // 1. quoted (capture 3; capture 4 or capture 5)
+ "('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|" +
+
+ // 2. simple (capture 6)
+ "((?:\\\\.|[^\\\\()[\\]]|" + attributes + ")*)|" +
+
+ // 3. anything else (capture 2)
+ ".*" +
+ ")\\)|)",
+
+ // Leading and non-escaped trailing whitespace, capturing some non-whitespace characters preceding the latter
+ rwhitespace = new RegExp( whitespace + "+", "g" ),
+ rtrim = new RegExp( "^" + whitespace + "+|((?:^|[^\\\\])(?:\\\\.)*)" +
+ whitespace + "+$", "g" ),
+
+ rcomma = new RegExp( "^" + whitespace + "*," + whitespace + "*" ),
+ rleadingCombinator = new RegExp( "^" + whitespace + "*([>+~]|" + whitespace + ")" + whitespace +
+ "*" ),
+ rdescend = new RegExp( whitespace + "|>" ),
+
+ rpseudo = new RegExp( pseudos ),
+ ridentifier = new RegExp( "^" + identifier + "$" ),
+
+ matchExpr = {
+ "ID": new RegExp( "^#(" + identifier + ")" ),
+ "CLASS": new RegExp( "^\\.(" + identifier + ")" ),
+ "TAG": new RegExp( "^(" + identifier + "|[*])" ),
+ "ATTR": new RegExp( "^" + attributes ),
+ "PSEUDO": new RegExp( "^" + pseudos ),
+ "CHILD": new RegExp( "^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\(" +
+ whitespace + "*(even|odd|(([+-]|)(\\d*)n|)" + whitespace + "*(?:([+-]|)" +
+ whitespace + "*(\\d+)|))" + whitespace + "*\\)|)", "i" ),
+ "bool": new RegExp( "^(?:" + booleans + ")$", "i" ),
+
+ // For use in libraries implementing .is()
+ // We use this for POS matching in `select`
+ "needsContext": new RegExp( "^" + whitespace +
+ "*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\(" + whitespace +
+ "*((?:-\\d)?\\d*)" + whitespace + "*\\)|)(?=[^-]|$)", "i" )
+ },
+
+ rhtml = /HTML$/i,
+ rinputs = /^(?:input|select|textarea|button)$/i,
+ rheader = /^h\d$/i,
+
+ rnative = /^[^{]+\{\s*\[native \w/,
+
+ // Easily-parseable/retrievable ID or TAG or CLASS selectors
+ rquickExpr = /^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,
+
+ rsibling = /[+~]/,
+
+ // CSS escapes
+ // http://www.w3.org/TR/CSS21/syndata.html#escaped-characters
+ runescape = new RegExp( "\\\\[\\da-fA-F]{1,6}" + whitespace + "?|\\\\([^\\r\\n\\f])", "g" ),
+ funescape = function( escape, nonHex ) {
+ var high = "0x" + escape.slice( 1 ) - 0x10000;
+
+ return nonHex ?
+
+ // Strip the backslash prefix from a non-hex escape sequence
+ nonHex :
+
+ // Replace a hexadecimal escape sequence with the encoded Unicode code point
+ // Support: IE <=11+
+ // For values outside the Basic Multilingual Plane (BMP), manually construct a
+ // surrogate pair
+ high < 0 ?
+ String.fromCharCode( high + 0x10000 ) :
+ String.fromCharCode( high >> 10 | 0xD800, high & 0x3FF | 0xDC00 );
+ },
+
+ // CSS string/identifier serialization
+ // https://drafts.csswg.org/cssom/#common-serializing-idioms
+ rcssescape = /([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,
+ fcssescape = function( ch, asCodePoint ) {
+ if ( asCodePoint ) {
+
+ // U+0000 NULL becomes U+FFFD REPLACEMENT CHARACTER
+ if ( ch === "\0" ) {
+ return "\uFFFD";
+ }
+
+ // Control characters and (dependent upon position) numbers get escaped as code points
+ return ch.slice( 0, -1 ) + "\\" +
+ ch.charCodeAt( ch.length - 1 ).toString( 16 ) + " ";
+ }
+
+ // Other potentially-special ASCII characters get backslash-escaped
+ return "\\" + ch;
+ },
+
+ // Used for iframes
+ // See setDocument()
+ // Removing the function wrapper causes a "Permission Denied"
+ // error in IE
+ unloadHandler = function() {
+ setDocument();
+ },
+
+ inDisabledFieldset = addCombinator(
+ function( elem ) {
+ return elem.disabled === true && elem.nodeName.toLowerCase() === "fieldset";
+ },
+ { dir: "parentNode", next: "legend" }
+ );
+
+// Optimize for push.apply( _, NodeList )
+try {
+ push.apply(
+ ( arr = slice.call( preferredDoc.childNodes ) ),
+ preferredDoc.childNodes
+ );
+
+ // Support: Android<4.0
+ // Detect silently failing push.apply
+ // eslint-disable-next-line no-unused-expressions
+ arr[ preferredDoc.childNodes.length ].nodeType;
+} catch ( e ) {
+ push = { apply: arr.length ?
+
+ // Leverage slice if possible
+ function( target, els ) {
+ pushNative.apply( target, slice.call( els ) );
+ } :
+
+ // Support: IE<9
+ // Otherwise append directly
+ function( target, els ) {
+ var j = target.length,
+ i = 0;
+
+ // Can't trust NodeList.length
+ while ( ( target[ j++ ] = els[ i++ ] ) ) {}
+ target.length = j - 1;
+ }
+ };
+}
+
+function Sizzle( selector, context, results, seed ) {
+ var m, i, elem, nid, match, groups, newSelector,
+ newContext = context && context.ownerDocument,
+
+ // nodeType defaults to 9, since context defaults to document
+ nodeType = context ? context.nodeType : 9;
+
+ results = results || [];
+
+ // Return early from calls with invalid selector or context
+ if ( typeof selector !== "string" || !selector ||
+ nodeType !== 1 && nodeType !== 9 && nodeType !== 11 ) {
+
+ return results;
+ }
+
+ // Try to shortcut find operations (as opposed to filters) in HTML documents
+ if ( !seed ) {
+ setDocument( context );
+ context = context || document;
+
+ if ( documentIsHTML ) {
+
+ // If the selector is sufficiently simple, try using a "get*By*" DOM method
+ // (excepting DocumentFragment context, where the methods don't exist)
+ if ( nodeType !== 11 && ( match = rquickExpr.exec( selector ) ) ) {
+
+ // ID selector
+ if ( ( m = match[ 1 ] ) ) {
+
+ // Document context
+ if ( nodeType === 9 ) {
+ if ( ( elem = context.getElementById( m ) ) ) {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( elem.id === m ) {
+ results.push( elem );
+ return results;
+ }
+ } else {
+ return results;
+ }
+
+ // Element context
+ } else {
+
+ // Support: IE, Opera, Webkit
+ // TODO: identify versions
+ // getElementById can match elements by name instead of ID
+ if ( newContext && ( elem = newContext.getElementById( m ) ) &&
+ contains( context, elem ) &&
+ elem.id === m ) {
+
+ results.push( elem );
+ return results;
+ }
+ }
+
+ // Type selector
+ } else if ( match[ 2 ] ) {
+ push.apply( results, context.getElementsByTagName( selector ) );
+ return results;
+
+ // Class selector
+ } else if ( ( m = match[ 3 ] ) && support.getElementsByClassName &&
+ context.getElementsByClassName ) {
+
+ push.apply( results, context.getElementsByClassName( m ) );
+ return results;
+ }
+ }
+
+ // Take advantage of querySelectorAll
+ if ( support.qsa &&
+ !nonnativeSelectorCache[ selector + " " ] &&
+ ( !rbuggyQSA || !rbuggyQSA.test( selector ) ) &&
+
+ // Support: IE 8 only
+ // Exclude object elements
+ ( nodeType !== 1 || context.nodeName.toLowerCase() !== "object" ) ) {
+
+ newSelector = selector;
+ newContext = context;
+
+ // qSA considers elements outside a scoping root when evaluating child or
+ // descendant combinators, which is not what we want.
+ // In such cases, we work around the behavior by prefixing every selector in the
+ // list with an ID selector referencing the scope context.
+ // The technique has to be used as well when a leading combinator is used
+ // as such selectors are not recognized by querySelectorAll.
+ // Thanks to Andrew Dupont for this technique.
+ if ( nodeType === 1 &&
+ ( rdescend.test( selector ) || rleadingCombinator.test( selector ) ) ) {
+
+ // Expand context for sibling selectors
+ newContext = rsibling.test( selector ) && testContext( context.parentNode ) ||
+ context;
+
+ // We can use :scope instead of the ID hack if the browser
+ // supports it & if we're not changing the context.
+ if ( newContext !== context || !support.scope ) {
+
+ // Capture the context ID, setting it first if necessary
+ if ( ( nid = context.getAttribute( "id" ) ) ) {
+ nid = nid.replace( rcssescape, fcssescape );
+ } else {
+ context.setAttribute( "id", ( nid = expando ) );
+ }
+ }
+
+ // Prefix every selector in the list
+ groups = tokenize( selector );
+ i = groups.length;
+ while ( i-- ) {
+ groups[ i ] = ( nid ? "#" + nid : ":scope" ) + " " +
+ toSelector( groups[ i ] );
+ }
+ newSelector = groups.join( "," );
+ }
+
+ try {
+ push.apply( results,
+ newContext.querySelectorAll( newSelector )
+ );
+ return results;
+ } catch ( qsaError ) {
+ nonnativeSelectorCache( selector, true );
+ } finally {
+ if ( nid === expando ) {
+ context.removeAttribute( "id" );
+ }
+ }
+ }
+ }
+ }
+
+ // All others
+ return select( selector.replace( rtrim, "$1" ), context, results, seed );
+}
+
+/**
+ * Create key-value caches of limited size
+ * @returns {function(string, object)} Returns the Object data after storing it on itself with
+ * property name the (space-suffixed) string and (if the cache is larger than Expr.cacheLength)
+ * deleting the oldest entry
+ */
+function createCache() {
+ var keys = [];
+
+ function cache( key, value ) {
+
+ // Use (key + " ") to avoid collision with native prototype properties (see Issue #157)
+ if ( keys.push( key + " " ) > Expr.cacheLength ) {
+
+ // Only keep the most recent entries
+ delete cache[ keys.shift() ];
+ }
+ return ( cache[ key + " " ] = value );
+ }
+ return cache;
+}
+
+/**
+ * Mark a function for special use by Sizzle
+ * @param {Function} fn The function to mark
+ */
+function markFunction( fn ) {
+ fn[ expando ] = true;
+ return fn;
+}
+
+/**
+ * Support testing using an element
+ * @param {Function} fn Passed the created element and returns a boolean result
+ */
+function assert( fn ) {
+ var el = document.createElement( "fieldset" );
+
+ try {
+ return !!fn( el );
+ } catch ( e ) {
+ return false;
+ } finally {
+
+ // Remove from its parent by default
+ if ( el.parentNode ) {
+ el.parentNode.removeChild( el );
+ }
+
+ // release memory in IE
+ el = null;
+ }
+}
+
+/**
+ * Adds the same handler for all of the specified attrs
+ * @param {String} attrs Pipe-separated list of attributes
+ * @param {Function} handler The method that will be applied
+ */
+function addHandle( attrs, handler ) {
+ var arr = attrs.split( "|" ),
+ i = arr.length;
+
+ while ( i-- ) {
+ Expr.attrHandle[ arr[ i ] ] = handler;
+ }
+}
+
+/**
+ * Checks document order of two siblings
+ * @param {Element} a
+ * @param {Element} b
+ * @returns {Number} Returns less than 0 if a precedes b, greater than 0 if a follows b
+ */
+function siblingCheck( a, b ) {
+ var cur = b && a,
+ diff = cur && a.nodeType === 1 && b.nodeType === 1 &&
+ a.sourceIndex - b.sourceIndex;
+
+ // Use IE sourceIndex if available on both nodes
+ if ( diff ) {
+ return diff;
+ }
+
+ // Check if b follows a
+ if ( cur ) {
+ while ( ( cur = cur.nextSibling ) ) {
+ if ( cur === b ) {
+ return -1;
+ }
+ }
+ }
+
+ return a ? 1 : -1;
+}
+
+/**
+ * Returns a function to use in pseudos for input types
+ * @param {String} type
+ */
+function createInputPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for buttons
+ * @param {String} type
+ */
+function createButtonPseudo( type ) {
+ return function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return ( name === "input" || name === "button" ) && elem.type === type;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for :enabled/:disabled
+ * @param {Boolean} disabled true for :disabled; false for :enabled
+ */
+function createDisabledPseudo( disabled ) {
+
+ // Known :disabled false positives: fieldset[disabled] > legend:nth-of-type(n+2) :can-disable
+ return function( elem ) {
+
+ // Only certain elements can match :enabled or :disabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-enabled
+ // https://html.spec.whatwg.org/multipage/scripting.html#selector-disabled
+ if ( "form" in elem ) {
+
+ // Check for inherited disabledness on relevant non-disabled elements:
+ // * listed form-associated elements in a disabled fieldset
+ // https://html.spec.whatwg.org/multipage/forms.html#category-listed
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-fe-disabled
+ // * option elements in a disabled optgroup
+ // https://html.spec.whatwg.org/multipage/forms.html#concept-option-disabled
+ // All such elements have a "form" property.
+ if ( elem.parentNode && elem.disabled === false ) {
+
+ // Option elements defer to a parent optgroup if present
+ if ( "label" in elem ) {
+ if ( "label" in elem.parentNode ) {
+ return elem.parentNode.disabled === disabled;
+ } else {
+ return elem.disabled === disabled;
+ }
+ }
+
+ // Support: IE 6 - 11
+ // Use the isDisabled shortcut property to check for disabled fieldset ancestors
+ return elem.isDisabled === disabled ||
+
+ // Where there is no isDisabled, check manually
+ /* jshint -W018 */
+ elem.isDisabled !== !disabled &&
+ inDisabledFieldset( elem ) === disabled;
+ }
+
+ return elem.disabled === disabled;
+
+ // Try to winnow out elements that can't be disabled before trusting the disabled property.
+ // Some victims get caught in our net (label, legend, menu, track), but it shouldn't
+ // even exist on them, let alone have a boolean value.
+ } else if ( "label" in elem ) {
+ return elem.disabled === disabled;
+ }
+
+ // Remaining elements are neither :enabled nor :disabled
+ return false;
+ };
+}
+
+/**
+ * Returns a function to use in pseudos for positionals
+ * @param {Function} fn
+ */
+function createPositionalPseudo( fn ) {
+ return markFunction( function( argument ) {
+ argument = +argument;
+ return markFunction( function( seed, matches ) {
+ var j,
+ matchIndexes = fn( [], seed.length, argument ),
+ i = matchIndexes.length;
+
+ // Match elements found at the specified indexes
+ while ( i-- ) {
+ if ( seed[ ( j = matchIndexes[ i ] ) ] ) {
+ seed[ j ] = !( matches[ j ] = seed[ j ] );
+ }
+ }
+ } );
+ } );
+}
+
+/**
+ * Checks a node for validity as a Sizzle context
+ * @param {Element|Object=} context
+ * @returns {Element|Object|Boolean} The input node if acceptable, otherwise a falsy value
+ */
+function testContext( context ) {
+ return context && typeof context.getElementsByTagName !== "undefined" && context;
+}
+
+// Expose support vars for convenience
+support = Sizzle.support = {};
+
+/**
+ * Detects XML nodes
+ * @param {Element|Object} elem An element or a document
+ * @returns {Boolean} True iff elem is a non-HTML XML node
+ */
+isXML = Sizzle.isXML = function( elem ) {
+ var namespace = elem && elem.namespaceURI,
+ docElem = elem && ( elem.ownerDocument || elem ).documentElement;
+
+ // Support: IE <=8
+ // Assume HTML when documentElement doesn't yet exist, such as inside loading iframes
+ // https://bugs.jquery.com/ticket/4833
+ return !rhtml.test( namespace || docElem && docElem.nodeName || "HTML" );
+};
+
+/**
+ * Sets document-related variables once based on the current document
+ * @param {Element|Object} [doc] An element or document object to use to set the document
+ * @returns {Object} Returns the current document
+ */
+setDocument = Sizzle.setDocument = function( node ) {
+ var hasCompare, subWindow,
+ doc = node ? node.ownerDocument || node : preferredDoc;
+
+ // Return early if doc is invalid or already selected
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( doc == document || doc.nodeType !== 9 || !doc.documentElement ) {
+ return document;
+ }
+
+ // Update global variables
+ document = doc;
+ docElem = document.documentElement;
+ documentIsHTML = !isXML( document );
+
+ // Support: IE 9 - 11+, Edge 12 - 18+
+ // Accessing iframe documents after unload throws "permission denied" errors (jQuery #13936)
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( preferredDoc != document &&
+ ( subWindow = document.defaultView ) && subWindow.top !== subWindow ) {
+
+ // Support: IE 11, Edge
+ if ( subWindow.addEventListener ) {
+ subWindow.addEventListener( "unload", unloadHandler, false );
+
+ // Support: IE 9 - 10 only
+ } else if ( subWindow.attachEvent ) {
+ subWindow.attachEvent( "onunload", unloadHandler );
+ }
+ }
+
+ // Support: IE 8 - 11+, Edge 12 - 18+, Chrome <=16 - 25 only, Firefox <=3.6 - 31 only,
+ // Safari 4 - 5 only, Opera <=11.6 - 12.x only
+ // IE/Edge & older browsers don't support the :scope pseudo-class.
+ // Support: Safari 6.0 only
+ // Safari 6.0 supports :scope but it's an alias of :root there.
+ support.scope = assert( function( el ) {
+ docElem.appendChild( el ).appendChild( document.createElement( "div" ) );
+ return typeof el.querySelectorAll !== "undefined" &&
+ !el.querySelectorAll( ":scope fieldset div" ).length;
+ } );
+
+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
+ // Make sure the the `:has()` argument is parsed unforgivingly.
+ // We include `*` in the test to detect buggy implementations that are
+ // _selectively_ forgiving (specifically when the list includes at least
+ // one valid selector).
+ // Note that we treat complete lack of support for `:has()` as if it were
+ // spec-compliant support, which is fine because use of `:has()` in such
+ // environments will fail in the qSA path and fall back to jQuery traversal
+ // anyway.
+ support.cssHas = assert( function() {
+ try {
+ document.querySelector( ":has(*,:jqfake)" );
+ return false;
+ } catch ( e ) {
+ return true;
+ }
+ } );
+
+ /* Attributes
+ ---------------------------------------------------------------------- */
+
+ // Support: IE<8
+ // Verify that getAttribute really returns attributes and not properties
+ // (excepting IE8 booleans)
+ support.attributes = assert( function( el ) {
+ el.className = "i";
+ return !el.getAttribute( "className" );
+ } );
+
+ /* getElement(s)By*
+ ---------------------------------------------------------------------- */
+
+ // Check if getElementsByTagName("*") returns only elements
+ support.getElementsByTagName = assert( function( el ) {
+ el.appendChild( document.createComment( "" ) );
+ return !el.getElementsByTagName( "*" ).length;
+ } );
+
+ // Support: IE<9
+ support.getElementsByClassName = rnative.test( document.getElementsByClassName );
+
+ // Support: IE<10
+ // Check if getElementById returns elements by name
+ // The broken getElementById methods don't pick up programmatically-set names,
+ // so use a roundabout getElementsByName test
+ support.getById = assert( function( el ) {
+ docElem.appendChild( el ).id = expando;
+ return !document.getElementsByName || !document.getElementsByName( expando ).length;
+ } );
+
+ // ID filter and find
+ if ( support.getById ) {
+ Expr.filter[ "ID" ] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ return elem.getAttribute( "id" ) === attrId;
+ };
+ };
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var elem = context.getElementById( id );
+ return elem ? [ elem ] : [];
+ }
+ };
+ } else {
+ Expr.filter[ "ID" ] = function( id ) {
+ var attrId = id.replace( runescape, funescape );
+ return function( elem ) {
+ var node = typeof elem.getAttributeNode !== "undefined" &&
+ elem.getAttributeNode( "id" );
+ return node && node.value === attrId;
+ };
+ };
+
+ // Support: IE 6 - 7 only
+ // getElementById is not reliable as a find shortcut
+ Expr.find[ "ID" ] = function( id, context ) {
+ if ( typeof context.getElementById !== "undefined" && documentIsHTML ) {
+ var node, i, elems,
+ elem = context.getElementById( id );
+
+ if ( elem ) {
+
+ // Verify the id attribute
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+
+ // Fall back on getElementsByName
+ elems = context.getElementsByName( id );
+ i = 0;
+ while ( ( elem = elems[ i++ ] ) ) {
+ node = elem.getAttributeNode( "id" );
+ if ( node && node.value === id ) {
+ return [ elem ];
+ }
+ }
+ }
+
+ return [];
+ }
+ };
+ }
+
+ // Tag
+ Expr.find[ "TAG" ] = support.getElementsByTagName ?
+ function( tag, context ) {
+ if ( typeof context.getElementsByTagName !== "undefined" ) {
+ return context.getElementsByTagName( tag );
+
+ // DocumentFragment nodes don't have gEBTN
+ } else if ( support.qsa ) {
+ return context.querySelectorAll( tag );
+ }
+ } :
+
+ function( tag, context ) {
+ var elem,
+ tmp = [],
+ i = 0,
+
+ // By happy coincidence, a (broken) gEBTN appears on DocumentFragment nodes too
+ results = context.getElementsByTagName( tag );
+
+ // Filter out possible comments
+ if ( tag === "*" ) {
+ while ( ( elem = results[ i++ ] ) ) {
+ if ( elem.nodeType === 1 ) {
+ tmp.push( elem );
+ }
+ }
+
+ return tmp;
+ }
+ return results;
+ };
+
+ // Class
+ Expr.find[ "CLASS" ] = support.getElementsByClassName && function( className, context ) {
+ if ( typeof context.getElementsByClassName !== "undefined" && documentIsHTML ) {
+ return context.getElementsByClassName( className );
+ }
+ };
+
+ /* QSA/matchesSelector
+ ---------------------------------------------------------------------- */
+
+ // QSA and matchesSelector support
+
+ // matchesSelector(:active) reports false when true (IE9/Opera 11.5)
+ rbuggyMatches = [];
+
+ // qSa(:focus) reports false when true (Chrome 21)
+ // We allow this because of a bug in IE8/9 that throws an error
+ // whenever `document.activeElement` is accessed on an iframe
+ // So, we allow :focus to pass through QSA all the time to avoid the IE error
+ // See https://bugs.jquery.com/ticket/13378
+ rbuggyQSA = [];
+
+ if ( ( support.qsa = rnative.test( document.querySelectorAll ) ) ) {
+
+ // Build QSA regex
+ // Regex strategy adopted from Diego Perini
+ assert( function( el ) {
+
+ var input;
+
+ // Select is set to empty string on purpose
+ // This is to test IE's treatment of not explicitly
+ // setting a boolean content attribute,
+ // since its presence should be enough
+ // https://bugs.jquery.com/ticket/12359
+ docElem.appendChild( el ).innerHTML = "<a id='" + expando + "'></a>" +
+ "<select id='" + expando + "-\r\\' msallowcapture=''>" +
+ "<option selected=''></option></select>";
+
+ // Support: IE8, Opera 11-12.16
+ // Nothing should be selected when empty strings follow ^= or $= or *=
+ // The test attribute must be unknown in Opera but "safe" for WinRT
+ // https://msdn.microsoft.com/en-us/library/ie/hh465388.aspx#attribute_section
+ if ( el.querySelectorAll( "[msallowcapture^='']" ).length ) {
+ rbuggyQSA.push( "[*^$]=" + whitespace + "*(?:''|\"\")" );
+ }
+
+ // Support: IE8
+ // Boolean attributes and "value" are not treated correctly
+ if ( !el.querySelectorAll( "[selected]" ).length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*(?:value|" + booleans + ")" );
+ }
+
+ // Support: Chrome<29, Android<4.4, Safari<7.0+, iOS<7.0+, PhantomJS<1.9.8+
+ if ( !el.querySelectorAll( "[id~=" + expando + "-]" ).length ) {
+ rbuggyQSA.push( "~=" );
+ }
+
+ // Support: IE 11+, Edge 15 - 18+
+ // IE 11/Edge don't find elements on a `[name='']` query in some cases.
+ // Adding a temporary attribute to the document before the selection works
+ // around the issue.
+ // Interestingly, IE 10 & older don't seem to have the issue.
+ input = document.createElement( "input" );
+ input.setAttribute( "name", "" );
+ el.appendChild( input );
+ if ( !el.querySelectorAll( "[name='']" ).length ) {
+ rbuggyQSA.push( "\\[" + whitespace + "*name" + whitespace + "*=" +
+ whitespace + "*(?:''|\"\")" );
+ }
+
+ // Webkit/Opera - :checked should return selected option elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ // IE8 throws error here and will not see later tests
+ if ( !el.querySelectorAll( ":checked" ).length ) {
+ rbuggyQSA.push( ":checked" );
+ }
+
+ // Support: Safari 8+, iOS 8+
+ // https://bugs.webkit.org/show_bug.cgi?id=136851
+ // In-page `selector#id sibling-combinator selector` fails
+ if ( !el.querySelectorAll( "a#" + expando + "+*" ).length ) {
+ rbuggyQSA.push( ".#.+[+~]" );
+ }
+
+ // Support: Firefox <=3.6 - 5 only
+ // Old Firefox doesn't throw on a badly-escaped identifier.
+ el.querySelectorAll( "\\\f" );
+ rbuggyQSA.push( "[\\r\\n\\f]" );
+ } );
+
+ assert( function( el ) {
+ el.innerHTML = "<a href='' disabled='disabled'></a>" +
+ "<select disabled='disabled'><option/></select>";
+
+ // Support: Windows 8 Native Apps
+ // The type and name attributes are restricted during .innerHTML assignment
+ var input = document.createElement( "input" );
+ input.setAttribute( "type", "hidden" );
+ el.appendChild( input ).setAttribute( "name", "D" );
+
+ // Support: IE8
+ // Enforce case-sensitivity of name attribute
+ if ( el.querySelectorAll( "[name=d]" ).length ) {
+ rbuggyQSA.push( "name" + whitespace + "*[*^$|!~]?=" );
+ }
+
+ // FF 3.5 - :enabled/:disabled and hidden elements (hidden elements are still enabled)
+ // IE8 throws error here and will not see later tests
+ if ( el.querySelectorAll( ":enabled" ).length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: IE9-11+
+ // IE's :disabled selector does not pick up the children of disabled fieldsets
+ docElem.appendChild( el ).disabled = true;
+ if ( el.querySelectorAll( ":disabled" ).length !== 2 ) {
+ rbuggyQSA.push( ":enabled", ":disabled" );
+ }
+
+ // Support: Opera 10 - 11 only
+ // Opera 10-11 does not throw on post-comma invalid pseudos
+ el.querySelectorAll( "*,:x" );
+ rbuggyQSA.push( ",.*:" );
+ } );
+ }
+
+ if ( ( support.matchesSelector = rnative.test( ( matches = docElem.matches ||
+ docElem.webkitMatchesSelector ||
+ docElem.mozMatchesSelector ||
+ docElem.oMatchesSelector ||
+ docElem.msMatchesSelector ) ) ) ) {
+
+ assert( function( el ) {
+
+ // Check to see if it's possible to do matchesSelector
+ // on a disconnected node (IE 9)
+ support.disconnectedMatch = matches.call( el, "*" );
+
+ // This should fail with an exception
+ // Gecko does not error, returns false instead
+ matches.call( el, "[s!='']:x" );
+ rbuggyMatches.push( "!=", pseudos );
+ } );
+ }
+
+ if ( !support.cssHas ) {
+
+ // Support: Chrome 105 - 110+, Safari 15.4 - 16.3+
+ // Our regular `try-catch` mechanism fails to detect natively-unsupported
+ // pseudo-classes inside `:has()` (such as `:has(:contains("Foo"))`)
+ // in browsers that parse the `:has()` argument as a forgiving selector list.
+ // https://drafts.csswg.org/selectors/#relational now requires the argument
+ // to be parsed unforgivingly, but browsers have not yet fully adjusted.
+ rbuggyQSA.push( ":has" );
+ }
+
+ rbuggyQSA = rbuggyQSA.length && new RegExp( rbuggyQSA.join( "|" ) );
+ rbuggyMatches = rbuggyMatches.length && new RegExp( rbuggyMatches.join( "|" ) );
+
+ /* Contains
+ ---------------------------------------------------------------------- */
+ hasCompare = rnative.test( docElem.compareDocumentPosition );
+
+ // Element contains another
+ // Purposefully self-exclusive
+ // As in, an element does not contain itself
+ contains = hasCompare || rnative.test( docElem.contains ) ?
+ function( a, b ) {
+
+ // Support: IE <9 only
+ // IE doesn't have `contains` on `document` so we need to check for
+ // `documentElement` presence.
+ // We need to fall back to `a` when `documentElement` is missing
+ // as `ownerDocument` of elements within `<template/>` may have
+ // a null one - a default behavior of all modern browsers.
+ var adown = a.nodeType === 9 && a.documentElement || a,
+ bup = b && b.parentNode;
+ return a === bup || !!( bup && bup.nodeType === 1 && (
+ adown.contains ?
+ adown.contains( bup ) :
+ a.compareDocumentPosition && a.compareDocumentPosition( bup ) & 16
+ ) );
+ } :
+ function( a, b ) {
+ if ( b ) {
+ while ( ( b = b.parentNode ) ) {
+ if ( b === a ) {
+ return true;
+ }
+ }
+ }
+ return false;
+ };
+
+ /* Sorting
+ ---------------------------------------------------------------------- */
+
+ // Document order sorting
+ sortOrder = hasCompare ?
+ function( a, b ) {
+
+ // Flag for duplicate removal
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ // Sort on method existence if only one input has compareDocumentPosition
+ var compare = !a.compareDocumentPosition - !b.compareDocumentPosition;
+ if ( compare ) {
+ return compare;
+ }
+
+ // Calculate position if both inputs belong to the same document
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ compare = ( a.ownerDocument || a ) == ( b.ownerDocument || b ) ?
+ a.compareDocumentPosition( b ) :
+
+ // Otherwise we know they are disconnected
+ 1;
+
+ // Disconnected nodes
+ if ( compare & 1 ||
+ ( !support.sortDetached && b.compareDocumentPosition( a ) === compare ) ) {
+
+ // Choose the first element that is related to our preferred document
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( a == document || a.ownerDocument == preferredDoc &&
+ contains( preferredDoc, a ) ) {
+ return -1;
+ }
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( b == document || b.ownerDocument == preferredDoc &&
+ contains( preferredDoc, b ) ) {
+ return 1;
+ }
+
+ // Maintain original order
+ return sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+ }
+
+ return compare & 4 ? -1 : 1;
+ } :
+ function( a, b ) {
+
+ // Exit early if the nodes are identical
+ if ( a === b ) {
+ hasDuplicate = true;
+ return 0;
+ }
+
+ var cur,
+ i = 0,
+ aup = a.parentNode,
+ bup = b.parentNode,
+ ap = [ a ],
+ bp = [ b ];
+
+ // Parentless nodes are either documents or disconnected
+ if ( !aup || !bup ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ return a == document ? -1 :
+ b == document ? 1 :
+ /* eslint-enable eqeqeq */
+ aup ? -1 :
+ bup ? 1 :
+ sortInput ?
+ ( indexOf( sortInput, a ) - indexOf( sortInput, b ) ) :
+ 0;
+
+ // If the nodes are siblings, we can do a quick check
+ } else if ( aup === bup ) {
+ return siblingCheck( a, b );
+ }
+
+ // Otherwise we need full lists of their ancestors for comparison
+ cur = a;
+ while ( ( cur = cur.parentNode ) ) {
+ ap.unshift( cur );
+ }
+ cur = b;
+ while ( ( cur = cur.parentNode ) ) {
+ bp.unshift( cur );
+ }
+
+ // Walk down the tree looking for a discrepancy
+ while ( ap[ i ] === bp[ i ] ) {
+ i++;
+ }
+
+ return i ?
+
+ // Do a sibling check if the nodes have a common ancestor
+ siblingCheck( ap[ i ], bp[ i ] ) :
+
+ // Otherwise nodes in our document sort first
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ /* eslint-disable eqeqeq */
+ ap[ i ] == preferredDoc ? -1 :
+ bp[ i ] == preferredDoc ? 1 :
+ /* eslint-enable eqeqeq */
+ 0;
+ };
+
+ return document;
+};
+
+Sizzle.matches = function( expr, elements ) {
+ return Sizzle( expr, null, null, elements );
+};
+
+Sizzle.matchesSelector = function( elem, expr ) {
+ setDocument( elem );
+
+ if ( support.matchesSelector && documentIsHTML &&
+ !nonnativeSelectorCache[ expr + " " ] &&
+ ( !rbuggyMatches || !rbuggyMatches.test( expr ) ) &&
+ ( !rbuggyQSA || !rbuggyQSA.test( expr ) ) ) {
+
+ try {
+ var ret = matches.call( elem, expr );
+
+ // IE 9's matchesSelector returns false on disconnected nodes
+ if ( ret || support.disconnectedMatch ||
+
+ // As well, disconnected nodes are said to be in a document
+ // fragment in IE 9
+ elem.document && elem.document.nodeType !== 11 ) {
+ return ret;
+ }
+ } catch ( e ) {
+ nonnativeSelectorCache( expr, true );
+ }
+ }
+
+ return Sizzle( expr, document, null, [ elem ] ).length > 0;
+};
+
+Sizzle.contains = function( context, elem ) {
+
+ // Set document vars if needed
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( context.ownerDocument || context ) != document ) {
+ setDocument( context );
+ }
+ return contains( context, elem );
+};
+
+Sizzle.attr = function( elem, name ) {
+
+ // Set document vars if needed
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( ( elem.ownerDocument || elem ) != document ) {
+ setDocument( elem );
+ }
+
+ var fn = Expr.attrHandle[ name.toLowerCase() ],
+
+ // Don't get fooled by Object.prototype properties (jQuery #13807)
+ val = fn && hasOwn.call( Expr.attrHandle, name.toLowerCase() ) ?
+ fn( elem, name, !documentIsHTML ) :
+ undefined;
+
+ return val !== undefined ?
+ val :
+ support.attributes || !documentIsHTML ?
+ elem.getAttribute( name ) :
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
+ val.value :
+ null;
+};
+
+Sizzle.escape = function( sel ) {
+ return ( sel + "" ).replace( rcssescape, fcssescape );
+};
+
+Sizzle.error = function( msg ) {
+ throw new Error( "Syntax error, unrecognized expression: " + msg );
+};
+
+/**
+ * Document sorting and removing duplicates
+ * @param {ArrayLike} results
+ */
+Sizzle.uniqueSort = function( results ) {
+ var elem,
+ duplicates = [],
+ j = 0,
+ i = 0;
+
+ // Unless we *know* we can detect duplicates, assume their presence
+ hasDuplicate = !support.detectDuplicates;
+ sortInput = !support.sortStable && results.slice( 0 );
+ results.sort( sortOrder );
+
+ if ( hasDuplicate ) {
+ while ( ( elem = results[ i++ ] ) ) {
+ if ( elem === results[ i ] ) {
+ j = duplicates.push( i );
+ }
+ }
+ while ( j-- ) {
+ results.splice( duplicates[ j ], 1 );
+ }
+ }
+
+ // Clear input after sorting to release objects
+ // See https://github.com/jquery/sizzle/pull/225
+ sortInput = null;
+
+ return results;
+};
+
+/**
+ * Utility function for retrieving the text value of an array of DOM nodes
+ * @param {Array|Element} elem
+ */
+getText = Sizzle.getText = function( elem ) {
+ var node,
+ ret = "",
+ i = 0,
+ nodeType = elem.nodeType;
+
+ if ( !nodeType ) {
+
+ // If no nodeType, this is expected to be an array
+ while ( ( node = elem[ i++ ] ) ) {
+
+ // Do not traverse comment nodes
+ ret += getText( node );
+ }
+ } else if ( nodeType === 1 || nodeType === 9 || nodeType === 11 ) {
+
+ // Use textContent for elements
+ // innerText usage removed for consistency of new lines (jQuery #11153)
+ if ( typeof elem.textContent === "string" ) {
+ return elem.textContent;
+ } else {
+
+ // Traverse its children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ ret += getText( elem );
+ }
+ }
+ } else if ( nodeType === 3 || nodeType === 4 ) {
+ return elem.nodeValue;
+ }
+
+ // Do not include comment or processing instruction nodes
+
+ return ret;
+};
+
+Expr = Sizzle.selectors = {
+
+ // Can be adjusted by the user
+ cacheLength: 50,
+
+ createPseudo: markFunction,
+
+ match: matchExpr,
+
+ attrHandle: {},
+
+ find: {},
+
+ relative: {
+ ">": { dir: "parentNode", first: true },
+ " ": { dir: "parentNode" },
+ "+": { dir: "previousSibling", first: true },
+ "~": { dir: "previousSibling" }
+ },
+
+ preFilter: {
+ "ATTR": function( match ) {
+ match[ 1 ] = match[ 1 ].replace( runescape, funescape );
+
+ // Move the given value to match[3] whether quoted or unquoted
+ match[ 3 ] = ( match[ 3 ] || match[ 4 ] ||
+ match[ 5 ] || "" ).replace( runescape, funescape );
+
+ if ( match[ 2 ] === "~=" ) {
+ match[ 3 ] = " " + match[ 3 ] + " ";
+ }
+
+ return match.slice( 0, 4 );
+ },
+
+ "CHILD": function( match ) {
+
+ /* matches from matchExpr["CHILD"]
+ 1 type (only|nth|...)
+ 2 what (child|of-type)
+ 3 argument (even|odd|\d*|\d*n([+-]\d+)?|...)
+ 4 xn-component of xn+y argument ([+-]?\d*n|)
+ 5 sign of xn-component
+ 6 x of xn-component
+ 7 sign of y-component
+ 8 y of y-component
+ */
+ match[ 1 ] = match[ 1 ].toLowerCase();
+
+ if ( match[ 1 ].slice( 0, 3 ) === "nth" ) {
+
+ // nth-* requires argument
+ if ( !match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
+ }
+
+ // numeric x and y parameters for Expr.filter.CHILD
+ // remember that false/true cast respectively to 0/1
+ match[ 4 ] = +( match[ 4 ] ?
+ match[ 5 ] + ( match[ 6 ] || 1 ) :
+ 2 * ( match[ 3 ] === "even" || match[ 3 ] === "odd" ) );
+ match[ 5 ] = +( ( match[ 7 ] + match[ 8 ] ) || match[ 3 ] === "odd" );
+
+ // other types prohibit arguments
+ } else if ( match[ 3 ] ) {
+ Sizzle.error( match[ 0 ] );
+ }
+
+ return match;
+ },
+
+ "PSEUDO": function( match ) {
+ var excess,
+ unquoted = !match[ 6 ] && match[ 2 ];
+
+ if ( matchExpr[ "CHILD" ].test( match[ 0 ] ) ) {
+ return null;
+ }
+
+ // Accept quoted arguments as-is
+ if ( match[ 3 ] ) {
+ match[ 2 ] = match[ 4 ] || match[ 5 ] || "";
+
+ // Strip excess characters from unquoted arguments
+ } else if ( unquoted && rpseudo.test( unquoted ) &&
+
+ // Get excess from tokenize (recursively)
+ ( excess = tokenize( unquoted, true ) ) &&
+
+ // advance to the next closing parenthesis
+ ( excess = unquoted.indexOf( ")", unquoted.length - excess ) - unquoted.length ) ) {
+
+ // excess is a negative index
+ match[ 0 ] = match[ 0 ].slice( 0, excess );
+ match[ 2 ] = unquoted.slice( 0, excess );
+ }
+
+ // Return only captures needed by the pseudo filter method (type and argument)
+ return match.slice( 0, 3 );
+ }
+ },
+
+ filter: {
+
+ "TAG": function( nodeNameSelector ) {
+ var nodeName = nodeNameSelector.replace( runescape, funescape ).toLowerCase();
+ return nodeNameSelector === "*" ?
+ function() {
+ return true;
+ } :
+ function( elem ) {
+ return elem.nodeName && elem.nodeName.toLowerCase() === nodeName;
+ };
+ },
+
+ "CLASS": function( className ) {
+ var pattern = classCache[ className + " " ];
+
+ return pattern ||
+ ( pattern = new RegExp( "(^|" + whitespace +
+ ")" + className + "(" + whitespace + "|$)" ) ) && classCache(
+ className, function( elem ) {
+ return pattern.test(
+ typeof elem.className === "string" && elem.className ||
+ typeof elem.getAttribute !== "undefined" &&
+ elem.getAttribute( "class" ) ||
+ ""
+ );
+ } );
+ },
+
+ "ATTR": function( name, operator, check ) {
+ return function( elem ) {
+ var result = Sizzle.attr( elem, name );
+
+ if ( result == null ) {
+ return operator === "!=";
+ }
+ if ( !operator ) {
+ return true;
+ }
+
+ result += "";
+
+ /* eslint-disable max-len */
+
+ return operator === "=" ? result === check :
+ operator === "!=" ? result !== check :
+ operator === "^=" ? check && result.indexOf( check ) === 0 :
+ operator === "*=" ? check && result.indexOf( check ) > -1 :
+ operator === "$=" ? check && result.slice( -check.length ) === check :
+ operator === "~=" ? ( " " + result.replace( rwhitespace, " " ) + " " ).indexOf( check ) > -1 :
+ operator === "|=" ? result === check || result.slice( 0, check.length + 1 ) === check + "-" :
+ false;
+ /* eslint-enable max-len */
+
+ };
+ },
+
+ "CHILD": function( type, what, _argument, first, last ) {
+ var simple = type.slice( 0, 3 ) !== "nth",
+ forward = type.slice( -4 ) !== "last",
+ ofType = what === "of-type";
+
+ return first === 1 && last === 0 ?
+
+ // Shortcut for :nth-*(n)
+ function( elem ) {
+ return !!elem.parentNode;
+ } :
+
+ function( elem, _context, xml ) {
+ var cache, uniqueCache, outerCache, node, nodeIndex, start,
+ dir = simple !== forward ? "nextSibling" : "previousSibling",
+ parent = elem.parentNode,
+ name = ofType && elem.nodeName.toLowerCase(),
+ useCache = !xml && !ofType,
+ diff = false;
+
+ if ( parent ) {
+
+ // :(first|last|only)-(child|of-type)
+ if ( simple ) {
+ while ( dir ) {
+ node = elem;
+ while ( ( node = node[ dir ] ) ) {
+ if ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) {
+
+ return false;
+ }
+ }
+
+ // Reverse direction for :only-* (if we haven't yet done so)
+ start = dir = type === "only" && !start && "nextSibling";
+ }
+ return true;
+ }
+
+ start = [ forward ? parent.firstChild : parent.lastChild ];
+
+ // non-xml :nth-child(...) stores cache data on `parent`
+ if ( forward && useCache ) {
+
+ // Seek `elem` from a previously-cached index
+
+ // ...in a gzip-friendly way
+ node = parent;
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex && cache[ 2 ];
+ node = nodeIndex && parent.childNodes[ nodeIndex ];
+
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
+
+ // Fallback to seeking `elem` from the start
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
+
+ // When found, cache indexes on `parent` and break
+ if ( node.nodeType === 1 && ++diff && node === elem ) {
+ uniqueCache[ type ] = [ dirruns, nodeIndex, diff ];
+ break;
+ }
+ }
+
+ } else {
+
+ // Use previously-cached element index if available
+ if ( useCache ) {
+
+ // ...in a gzip-friendly way
+ node = elem;
+ outerCache = node[ expando ] || ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ cache = uniqueCache[ type ] || [];
+ nodeIndex = cache[ 0 ] === dirruns && cache[ 1 ];
+ diff = nodeIndex;
+ }
+
+ // xml :nth-child(...)
+ // or :nth-last-child(...) or :nth(-last)?-of-type(...)
+ if ( diff === false ) {
+
+ // Use the same loop as above to seek `elem` from the start
+ while ( ( node = ++nodeIndex && node && node[ dir ] ||
+ ( diff = nodeIndex = 0 ) || start.pop() ) ) {
+
+ if ( ( ofType ?
+ node.nodeName.toLowerCase() === name :
+ node.nodeType === 1 ) &&
+ ++diff ) {
+
+ // Cache the index of each encountered element
+ if ( useCache ) {
+ outerCache = node[ expando ] ||
+ ( node[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ node.uniqueID ] ||
+ ( outerCache[ node.uniqueID ] = {} );
+
+ uniqueCache[ type ] = [ dirruns, diff ];
+ }
+
+ if ( node === elem ) {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ // Incorporate the offset, then check against cycle size
+ diff -= last;
+ return diff === first || ( diff % first === 0 && diff / first >= 0 );
+ }
+ };
+ },
+
+ "PSEUDO": function( pseudo, argument ) {
+
+ // pseudo-class names are case-insensitive
+ // http://www.w3.org/TR/selectors/#pseudo-classes
+ // Prioritize by case sensitivity in case custom pseudos are added with uppercase letters
+ // Remember that setFilters inherits from pseudos
+ var args,
+ fn = Expr.pseudos[ pseudo ] || Expr.setFilters[ pseudo.toLowerCase() ] ||
+ Sizzle.error( "unsupported pseudo: " + pseudo );
+
+ // The user may use createPseudo to indicate that
+ // arguments are needed to create the filter function
+ // just as Sizzle does
+ if ( fn[ expando ] ) {
+ return fn( argument );
+ }
+
+ // But maintain support for old signatures
+ if ( fn.length > 1 ) {
+ args = [ pseudo, pseudo, "", argument ];
+ return Expr.setFilters.hasOwnProperty( pseudo.toLowerCase() ) ?
+ markFunction( function( seed, matches ) {
+ var idx,
+ matched = fn( seed, argument ),
+ i = matched.length;
+ while ( i-- ) {
+ idx = indexOf( seed, matched[ i ] );
+ seed[ idx ] = !( matches[ idx ] = matched[ i ] );
+ }
+ } ) :
+ function( elem ) {
+ return fn( elem, 0, args );
+ };
+ }
+
+ return fn;
+ }
+ },
+
+ pseudos: {
+
+ // Potentially complex pseudos
+ "not": markFunction( function( selector ) {
+
+ // Trim the selector passed to compile
+ // to avoid treating leading and trailing
+ // spaces as combinators
+ var input = [],
+ results = [],
+ matcher = compile( selector.replace( rtrim, "$1" ) );
+
+ return matcher[ expando ] ?
+ markFunction( function( seed, matches, _context, xml ) {
+ var elem,
+ unmatched = matcher( seed, null, xml, [] ),
+ i = seed.length;
+
+ // Match elements unmatched by `matcher`
+ while ( i-- ) {
+ if ( ( elem = unmatched[ i ] ) ) {
+ seed[ i ] = !( matches[ i ] = elem );
+ }
+ }
+ } ) :
+ function( elem, _context, xml ) {
+ input[ 0 ] = elem;
+ matcher( input, null, xml, results );
+
+ // Don't keep the element (issue #299)
+ input[ 0 ] = null;
+ return !results.pop();
+ };
+ } ),
+
+ "has": markFunction( function( selector ) {
+ return function( elem ) {
+ return Sizzle( selector, elem ).length > 0;
+ };
+ } ),
+
+ "contains": markFunction( function( text ) {
+ text = text.replace( runescape, funescape );
+ return function( elem ) {
+ return ( elem.textContent || getText( elem ) ).indexOf( text ) > -1;
+ };
+ } ),
+
+ // "Whether an element is represented by a :lang() selector
+ // is based solely on the element's language value
+ // being equal to the identifier C,
+ // or beginning with the identifier C immediately followed by "-".
+ // The matching of C against the element's language value is performed case-insensitively.
+ // The identifier C does not have to be a valid language name."
+ // http://www.w3.org/TR/selectors/#lang-pseudo
+ "lang": markFunction( function( lang ) {
+
+ // lang value must be a valid identifier
+ if ( !ridentifier.test( lang || "" ) ) {
+ Sizzle.error( "unsupported lang: " + lang );
+ }
+ lang = lang.replace( runescape, funescape ).toLowerCase();
+ return function( elem ) {
+ var elemLang;
+ do {
+ if ( ( elemLang = documentIsHTML ?
+ elem.lang :
+ elem.getAttribute( "xml:lang" ) || elem.getAttribute( "lang" ) ) ) {
+
+ elemLang = elemLang.toLowerCase();
+ return elemLang === lang || elemLang.indexOf( lang + "-" ) === 0;
+ }
+ } while ( ( elem = elem.parentNode ) && elem.nodeType === 1 );
+ return false;
+ };
+ } ),
+
+ // Miscellaneous
+ "target": function( elem ) {
+ var hash = window.location && window.location.hash;
+ return hash && hash.slice( 1 ) === elem.id;
+ },
+
+ "root": function( elem ) {
+ return elem === docElem;
+ },
+
+ "focus": function( elem ) {
+ return elem === document.activeElement &&
+ ( !document.hasFocus || document.hasFocus() ) &&
+ !!( elem.type || elem.href || ~elem.tabIndex );
+ },
+
+ // Boolean properties
+ "enabled": createDisabledPseudo( false ),
+ "disabled": createDisabledPseudo( true ),
+
+ "checked": function( elem ) {
+
+ // In CSS3, :checked should return both checked and selected elements
+ // http://www.w3.org/TR/2011/REC-css3-selectors-20110929/#checked
+ var nodeName = elem.nodeName.toLowerCase();
+ return ( nodeName === "input" && !!elem.checked ) ||
+ ( nodeName === "option" && !!elem.selected );
+ },
+
+ "selected": function( elem ) {
+
+ // Accessing this property makes selected-by-default
+ // options in Safari work properly
+ if ( elem.parentNode ) {
+ // eslint-disable-next-line no-unused-expressions
+ elem.parentNode.selectedIndex;
+ }
+
+ return elem.selected === true;
+ },
+
+ // Contents
+ "empty": function( elem ) {
+
+ // http://www.w3.org/TR/selectors/#empty-pseudo
+ // :empty is negated by element (1) or content nodes (text: 3; cdata: 4; entity ref: 5),
+ // but not by others (comment: 8; processing instruction: 7; etc.)
+ // nodeType < 6 works because attributes (2) do not appear as children
+ for ( elem = elem.firstChild; elem; elem = elem.nextSibling ) {
+ if ( elem.nodeType < 6 ) {
+ return false;
+ }
+ }
+ return true;
+ },
+
+ "parent": function( elem ) {
+ return !Expr.pseudos[ "empty" ]( elem );
+ },
+
+ // Element/input types
+ "header": function( elem ) {
+ return rheader.test( elem.nodeName );
+ },
+
+ "input": function( elem ) {
+ return rinputs.test( elem.nodeName );
+ },
+
+ "button": function( elem ) {
+ var name = elem.nodeName.toLowerCase();
+ return name === "input" && elem.type === "button" || name === "button";
+ },
+
+ "text": function( elem ) {
+ var attr;
+ return elem.nodeName.toLowerCase() === "input" &&
+ elem.type === "text" &&
+
+ // Support: IE <10 only
+ // New HTML5 attribute values (e.g., "search") appear with elem.type === "text"
+ ( ( attr = elem.getAttribute( "type" ) ) == null ||
+ attr.toLowerCase() === "text" );
+ },
+
+ // Position-in-collection
+ "first": createPositionalPseudo( function() {
+ return [ 0 ];
+ } ),
+
+ "last": createPositionalPseudo( function( _matchIndexes, length ) {
+ return [ length - 1 ];
+ } ),
+
+ "eq": createPositionalPseudo( function( _matchIndexes, length, argument ) {
+ return [ argument < 0 ? argument + length : argument ];
+ } ),
+
+ "even": createPositionalPseudo( function( matchIndexes, length ) {
+ var i = 0;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "odd": createPositionalPseudo( function( matchIndexes, length ) {
+ var i = 1;
+ for ( ; i < length; i += 2 ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "lt": createPositionalPseudo( function( matchIndexes, length, argument ) {
+ var i = argument < 0 ?
+ argument + length :
+ argument > length ?
+ length :
+ argument;
+ for ( ; --i >= 0; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } ),
+
+ "gt": createPositionalPseudo( function( matchIndexes, length, argument ) {
+ var i = argument < 0 ? argument + length : argument;
+ for ( ; ++i < length; ) {
+ matchIndexes.push( i );
+ }
+ return matchIndexes;
+ } )
+ }
+};
+
+Expr.pseudos[ "nth" ] = Expr.pseudos[ "eq" ];
+
+// Add button/input type pseudos
+for ( i in { radio: true, checkbox: true, file: true, password: true, image: true } ) {
+ Expr.pseudos[ i ] = createInputPseudo( i );
+}
+for ( i in { submit: true, reset: true } ) {
+ Expr.pseudos[ i ] = createButtonPseudo( i );
+}
+
+// Easy API for creating new setFilters
+function setFilters() {}
+setFilters.prototype = Expr.filters = Expr.pseudos;
+Expr.setFilters = new setFilters();
+
+tokenize = Sizzle.tokenize = function( selector, parseOnly ) {
+ var matched, match, tokens, type,
+ soFar, groups, preFilters,
+ cached = tokenCache[ selector + " " ];
+
+ if ( cached ) {
+ return parseOnly ? 0 : cached.slice( 0 );
+ }
+
+ soFar = selector;
+ groups = [];
+ preFilters = Expr.preFilter;
+
+ while ( soFar ) {
+
+ // Comma and first run
+ if ( !matched || ( match = rcomma.exec( soFar ) ) ) {
+ if ( match ) {
+
+ // Don't consume trailing commas as valid
+ soFar = soFar.slice( match[ 0 ].length ) || soFar;
+ }
+ groups.push( ( tokens = [] ) );
+ }
+
+ matched = false;
+
+ // Combinators
+ if ( ( match = rleadingCombinator.exec( soFar ) ) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+
+ // Cast descendant combinators to space
+ type: match[ 0 ].replace( rtrim, " " )
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+
+ // Filters
+ for ( type in Expr.filter ) {
+ if ( ( match = matchExpr[ type ].exec( soFar ) ) && ( !preFilters[ type ] ||
+ ( match = preFilters[ type ]( match ) ) ) ) {
+ matched = match.shift();
+ tokens.push( {
+ value: matched,
+ type: type,
+ matches: match
+ } );
+ soFar = soFar.slice( matched.length );
+ }
+ }
+
+ if ( !matched ) {
+ break;
+ }
+ }
+
+ // Return the length of the invalid excess
+ // if we're just parsing
+ // Otherwise, throw an error or return tokens
+ return parseOnly ?
+ soFar.length :
+ soFar ?
+ Sizzle.error( selector ) :
+
+ // Cache the tokens
+ tokenCache( selector, groups ).slice( 0 );
+};
+
+function toSelector( tokens ) {
+ var i = 0,
+ len = tokens.length,
+ selector = "";
+ for ( ; i < len; i++ ) {
+ selector += tokens[ i ].value;
+ }
+ return selector;
+}
+
+function addCombinator( matcher, combinator, base ) {
+ var dir = combinator.dir,
+ skip = combinator.next,
+ key = skip || dir,
+ checkNonElements = base && key === "parentNode",
+ doneName = done++;
+
+ return combinator.first ?
+
+ // Check against closest ancestor/preceding element
+ function( elem, context, xml ) {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ return matcher( elem, context, xml );
+ }
+ }
+ return false;
+ } :
+
+ // Check against all ancestor/preceding elements
+ function( elem, context, xml ) {
+ var oldCache, uniqueCache, outerCache,
+ newCache = [ dirruns, doneName ];
+
+ // We can't set arbitrary data on XML nodes, so they don't benefit from combinator caching
+ if ( xml ) {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ if ( matcher( elem, context, xml ) ) {
+ return true;
+ }
+ }
+ }
+ } else {
+ while ( ( elem = elem[ dir ] ) ) {
+ if ( elem.nodeType === 1 || checkNonElements ) {
+ outerCache = elem[ expando ] || ( elem[ expando ] = {} );
+
+ // Support: IE <9 only
+ // Defend against cloned attroperties (jQuery gh-1709)
+ uniqueCache = outerCache[ elem.uniqueID ] ||
+ ( outerCache[ elem.uniqueID ] = {} );
+
+ if ( skip && skip === elem.nodeName.toLowerCase() ) {
+ elem = elem[ dir ] || elem;
+ } else if ( ( oldCache = uniqueCache[ key ] ) &&
+ oldCache[ 0 ] === dirruns && oldCache[ 1 ] === doneName ) {
+
+ // Assign to newCache so results back-propagate to previous elements
+ return ( newCache[ 2 ] = oldCache[ 2 ] );
+ } else {
+
+ // Reuse newcache so results back-propagate to previous elements
+ uniqueCache[ key ] = newCache;
+
+ // A match means we're done; a fail means we have to keep checking
+ if ( ( newCache[ 2 ] = matcher( elem, context, xml ) ) ) {
+ return true;
+ }
+ }
+ }
+ }
+ }
+ return false;
+ };
+}
+
+function elementMatcher( matchers ) {
+ return matchers.length > 1 ?
+ function( elem, context, xml ) {
+ var i = matchers.length;
+ while ( i-- ) {
+ if ( !matchers[ i ]( elem, context, xml ) ) {
+ return false;
+ }
+ }
+ return true;
+ } :
+ matchers[ 0 ];
+}
+
+function multipleContexts( selector, contexts, results ) {
+ var i = 0,
+ len = contexts.length;
+ for ( ; i < len; i++ ) {
+ Sizzle( selector, contexts[ i ], results );
+ }
+ return results;
+}
+
+function condense( unmatched, map, filter, context, xml ) {
+ var elem,
+ newUnmatched = [],
+ i = 0,
+ len = unmatched.length,
+ mapped = map != null;
+
+ for ( ; i < len; i++ ) {
+ if ( ( elem = unmatched[ i ] ) ) {
+ if ( !filter || filter( elem, context, xml ) ) {
+ newUnmatched.push( elem );
+ if ( mapped ) {
+ map.push( i );
+ }
+ }
+ }
+ }
+
+ return newUnmatched;
+}
+
+function setMatcher( preFilter, selector, matcher, postFilter, postFinder, postSelector ) {
+ if ( postFilter && !postFilter[ expando ] ) {
+ postFilter = setMatcher( postFilter );
+ }
+ if ( postFinder && !postFinder[ expando ] ) {
+ postFinder = setMatcher( postFinder, postSelector );
+ }
+ return markFunction( function( seed, results, context, xml ) {
+ var temp, i, elem,
+ preMap = [],
+ postMap = [],
+ preexisting = results.length,
+
+ // Get initial elements from seed or context
+ elems = seed || multipleContexts(
+ selector || "*",
+ context.nodeType ? [ context ] : context,
+ []
+ ),
+
+ // Prefilter to get matcher input, preserving a map for seed-results synchronization
+ matcherIn = preFilter && ( seed || !selector ) ?
+ condense( elems, preMap, preFilter, context, xml ) :
+ elems,
+
+ matcherOut = matcher ?
+
+ // If we have a postFinder, or filtered seed, or non-seed postFilter or preexisting results,
+ postFinder || ( seed ? preFilter : preexisting || postFilter ) ?
+
+ // ...intermediate processing is necessary
+ [] :
+
+ // ...otherwise use results directly
+ results :
+ matcherIn;
+
+ // Find primary matches
+ if ( matcher ) {
+ matcher( matcherIn, matcherOut, context, xml );
+ }
+
+ // Apply postFilter
+ if ( postFilter ) {
+ temp = condense( matcherOut, postMap );
+ postFilter( temp, [], context, xml );
+
+ // Un-match failing elements by moving them back to matcherIn
+ i = temp.length;
+ while ( i-- ) {
+ if ( ( elem = temp[ i ] ) ) {
+ matcherOut[ postMap[ i ] ] = !( matcherIn[ postMap[ i ] ] = elem );
+ }
+ }
+ }
+
+ if ( seed ) {
+ if ( postFinder || preFilter ) {
+ if ( postFinder ) {
+
+ // Get the final matcherOut by condensing this intermediate into postFinder contexts
+ temp = [];
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( ( elem = matcherOut[ i ] ) ) {
+
+ // Restore matcherIn since elem is not yet a final match
+ temp.push( ( matcherIn[ i ] = elem ) );
+ }
+ }
+ postFinder( null, ( matcherOut = [] ), temp, xml );
+ }
+
+ // Move matched elements from seed to results to keep them synchronized
+ i = matcherOut.length;
+ while ( i-- ) {
+ if ( ( elem = matcherOut[ i ] ) &&
+ ( temp = postFinder ? indexOf( seed, elem ) : preMap[ i ] ) > -1 ) {
+
+ seed[ temp ] = !( results[ temp ] = elem );
+ }
+ }
+ }
+
+ // Add elements to results, through postFinder if defined
+ } else {
+ matcherOut = condense(
+ matcherOut === results ?
+ matcherOut.splice( preexisting, matcherOut.length ) :
+ matcherOut
+ );
+ if ( postFinder ) {
+ postFinder( null, results, matcherOut, xml );
+ } else {
+ push.apply( results, matcherOut );
+ }
+ }
+ } );
+}
+
+function matcherFromTokens( tokens ) {
+ var checkContext, matcher, j,
+ len = tokens.length,
+ leadingRelative = Expr.relative[ tokens[ 0 ].type ],
+ implicitRelative = leadingRelative || Expr.relative[ " " ],
+ i = leadingRelative ? 1 : 0,
+
+ // The foundational matcher ensures that elements are reachable from top-level context(s)
+ matchContext = addCombinator( function( elem ) {
+ return elem === checkContext;
+ }, implicitRelative, true ),
+ matchAnyContext = addCombinator( function( elem ) {
+ return indexOf( checkContext, elem ) > -1;
+ }, implicitRelative, true ),
+ matchers = [ function( elem, context, xml ) {
+ var ret = ( !leadingRelative && ( xml || context !== outermostContext ) ) || (
+ ( checkContext = context ).nodeType ?
+ matchContext( elem, context, xml ) :
+ matchAnyContext( elem, context, xml ) );
+
+ // Avoid hanging onto element (issue #299)
+ checkContext = null;
+ return ret;
+ } ];
+
+ for ( ; i < len; i++ ) {
+ if ( ( matcher = Expr.relative[ tokens[ i ].type ] ) ) {
+ matchers = [ addCombinator( elementMatcher( matchers ), matcher ) ];
+ } else {
+ matcher = Expr.filter[ tokens[ i ].type ].apply( null, tokens[ i ].matches );
+
+ // Return special upon seeing a positional matcher
+ if ( matcher[ expando ] ) {
+
+ // Find the next relative operator (if any) for proper handling
+ j = ++i;
+ for ( ; j < len; j++ ) {
+ if ( Expr.relative[ tokens[ j ].type ] ) {
+ break;
+ }
+ }
+ return setMatcher(
+ i > 1 && elementMatcher( matchers ),
+ i > 1 && toSelector(
+
+ // If the preceding token was a descendant combinator, insert an implicit any-element `*`
+ tokens
+ .slice( 0, i - 1 )
+ .concat( { value: tokens[ i - 2 ].type === " " ? "*" : "" } )
+ ).replace( rtrim, "$1" ),
+ matcher,
+ i < j && matcherFromTokens( tokens.slice( i, j ) ),
+ j < len && matcherFromTokens( ( tokens = tokens.slice( j ) ) ),
+ j < len && toSelector( tokens )
+ );
+ }
+ matchers.push( matcher );
+ }
+ }
+
+ return elementMatcher( matchers );
+}
+
+function matcherFromGroupMatchers( elementMatchers, setMatchers ) {
+ var bySet = setMatchers.length > 0,
+ byElement = elementMatchers.length > 0,
+ superMatcher = function( seed, context, xml, results, outermost ) {
+ var elem, j, matcher,
+ matchedCount = 0,
+ i = "0",
+ unmatched = seed && [],
+ setMatched = [],
+ contextBackup = outermostContext,
+
+ // We must always have either seed elements or outermost context
+ elems = seed || byElement && Expr.find[ "TAG" ]( "*", outermost ),
+
+ // Use integer dirruns iff this is the outermost matcher
+ dirrunsUnique = ( dirruns += contextBackup == null ? 1 : Math.random() || 0.1 ),
+ len = elems.length;
+
+ if ( outermost ) {
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ outermostContext = context == document || context || outermost;
+ }
+
+ // Add elements passing elementMatchers directly to results
+ // Support: IE<9, Safari
+ // Tolerate NodeList properties (IE: "length"; Safari: <number>) matching elements by id
+ for ( ; i !== len && ( elem = elems[ i ] ) != null; i++ ) {
+ if ( byElement && elem ) {
+ j = 0;
+
+ // Support: IE 11+, Edge 17 - 18+
+ // IE/Edge sometimes throw a "Permission denied" error when strict-comparing
+ // two documents; shallow comparisons work.
+ // eslint-disable-next-line eqeqeq
+ if ( !context && elem.ownerDocument != document ) {
+ setDocument( elem );
+ xml = !documentIsHTML;
+ }
+ while ( ( matcher = elementMatchers[ j++ ] ) ) {
+ if ( matcher( elem, context || document, xml ) ) {
+ results.push( elem );
+ break;
+ }
+ }
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ }
+ }
+
+ // Track unmatched elements for set filters
+ if ( bySet ) {
+
+ // They will have gone through all possible matchers
+ if ( ( elem = !matcher && elem ) ) {
+ matchedCount--;
+ }
+
+ // Lengthen the array for every element, matched or not
+ if ( seed ) {
+ unmatched.push( elem );
+ }
+ }
+ }
+
+ // `i` is now the count of elements visited above, and adding it to `matchedCount`
+ // makes the latter nonnegative.
+ matchedCount += i;
+
+ // Apply set filters to unmatched elements
+ // NOTE: This can be skipped if there are no unmatched elements (i.e., `matchedCount`
+ // equals `i`), unless we didn't visit _any_ elements in the above loop because we have
+ // no element matchers and no seed.
+ // Incrementing an initially-string "0" `i` allows `i` to remain a string only in that
+ // case, which will result in a "00" `matchedCount` that differs from `i` but is also
+ // numerically zero.
+ if ( bySet && i !== matchedCount ) {
+ j = 0;
+ while ( ( matcher = setMatchers[ j++ ] ) ) {
+ matcher( unmatched, setMatched, context, xml );
+ }
+
+ if ( seed ) {
+
+ // Reintegrate element matches to eliminate the need for sorting
+ if ( matchedCount > 0 ) {
+ while ( i-- ) {
+ if ( !( unmatched[ i ] || setMatched[ i ] ) ) {
+ setMatched[ i ] = pop.call( results );
+ }
+ }
+ }
+
+ // Discard index placeholder values to get only actual matches
+ setMatched = condense( setMatched );
+ }
+
+ // Add matches to results
+ push.apply( results, setMatched );
+
+ // Seedless set matches succeeding multiple successful matchers stipulate sorting
+ if ( outermost && !seed && setMatched.length > 0 &&
+ ( matchedCount + setMatchers.length ) > 1 ) {
+
+ Sizzle.uniqueSort( results );
+ }
+ }
+
+ // Override manipulation of globals by nested matchers
+ if ( outermost ) {
+ dirruns = dirrunsUnique;
+ outermostContext = contextBackup;
+ }
+
+ return unmatched;
+ };
+
+ return bySet ?
+ markFunction( superMatcher ) :
+ superMatcher;
+}
+
+compile = Sizzle.compile = function( selector, match /* Internal Use Only */ ) {
+ var i,
+ setMatchers = [],
+ elementMatchers = [],
+ cached = compilerCache[ selector + " " ];
+
+ if ( !cached ) {
+
+ // Generate a function of recursive functions that can be used to check each element
+ if ( !match ) {
+ match = tokenize( selector );
+ }
+ i = match.length;
+ while ( i-- ) {
+ cached = matcherFromTokens( match[ i ] );
+ if ( cached[ expando ] ) {
+ setMatchers.push( cached );
+ } else {
+ elementMatchers.push( cached );
+ }
+ }
+
+ // Cache the compiled function
+ cached = compilerCache(
+ selector,
+ matcherFromGroupMatchers( elementMatchers, setMatchers )
+ );
+
+ // Save selector and tokenization
+ cached.selector = selector;
+ }
+ return cached;
+};
+
+/**
+ * A low-level selection function that works with Sizzle's compiled
+ * selector functions
+ * @param {String|Function} selector A selector or a pre-compiled
+ * selector function built with Sizzle.compile
+ * @param {Element} context
+ * @param {Array} [results]
+ * @param {Array} [seed] A set of elements to match against
+ */
+select = Sizzle.select = function( selector, context, results, seed ) {
+ var i, tokens, token, type, find,
+ compiled = typeof selector === "function" && selector,
+ match = !seed && tokenize( ( selector = compiled.selector || selector ) );
+
+ results = results || [];
+
+ // Try to minimize operations if there is only one selector in the list and no seed
+ // (the latter of which guarantees us context)
+ if ( match.length === 1 ) {
+
+ // Reduce context if the leading compound selector is an ID
+ tokens = match[ 0 ] = match[ 0 ].slice( 0 );
+ if ( tokens.length > 2 && ( token = tokens[ 0 ] ).type === "ID" &&
+ context.nodeType === 9 && documentIsHTML && Expr.relative[ tokens[ 1 ].type ] ) {
+
+ context = ( Expr.find[ "ID" ]( token.matches[ 0 ]
+ .replace( runescape, funescape ), context ) || [] )[ 0 ];
+ if ( !context ) {
+ return results;
+
+ // Precompiled matchers will still verify ancestry, so step up a level
+ } else if ( compiled ) {
+ context = context.parentNode;
+ }
+
+ selector = selector.slice( tokens.shift().value.length );
+ }
+
+ // Fetch a seed set for right-to-left matching
+ i = matchExpr[ "needsContext" ].test( selector ) ? 0 : tokens.length;
+ while ( i-- ) {
+ token = tokens[ i ];
+
+ // Abort if we hit a combinator
+ if ( Expr.relative[ ( type = token.type ) ] ) {
+ break;
+ }
+ if ( ( find = Expr.find[ type ] ) ) {
+
+ // Search, expanding context for leading sibling combinators
+ if ( ( seed = find(
+ token.matches[ 0 ].replace( runescape, funescape ),
+ rsibling.test( tokens[ 0 ].type ) && testContext( context.parentNode ) ||
+ context
+ ) ) ) {
+
+ // If seed is empty or no tokens remain, we can return early
+ tokens.splice( i, 1 );
+ selector = seed.length && toSelector( tokens );
+ if ( !selector ) {
+ push.apply( results, seed );
+ return results;
+ }
+
+ break;
+ }
+ }
+ }
+ }
+
+ // Compile and execute a filtering function if one is not provided
+ // Provide `match` to avoid retokenization if we modified the selector above
+ ( compiled || compile( selector, match ) )(
+ seed,
+ context,
+ !documentIsHTML,
+ results,
+ !context || rsibling.test( selector ) && testContext( context.parentNode ) || context
+ );
+ return results;
+};
+
+// One-time assignments
+
+// Sort stability
+support.sortStable = expando.split( "" ).sort( sortOrder ).join( "" ) === expando;
+
+// Support: Chrome 14-35+
+// Always assume duplicates if they aren't passed to the comparison function
+support.detectDuplicates = !!hasDuplicate;
+
+// Initialize against the default document
+setDocument();
+
+// Support: Webkit<537.32 - Safari 6.0.3/Chrome 25 (fixed in Chrome 27)
+// Detached nodes confoundingly follow *each other*
+support.sortDetached = assert( function( el ) {
+
+ // Should return 1, but returns 4 (following)
+ return el.compareDocumentPosition( document.createElement( "fieldset" ) ) & 1;
+} );
+
+// Support: IE<8
+// Prevent attribute/property "interpolation"
+// https://msdn.microsoft.com/en-us/library/ms536429%28VS.85%29.aspx
+if ( !assert( function( el ) {
+ el.innerHTML = "<a href='#'></a>";
+ return el.firstChild.getAttribute( "href" ) === "#";
+} ) ) {
+ addHandle( "type|href|height|width", function( elem, name, isXML ) {
+ if ( !isXML ) {
+ return elem.getAttribute( name, name.toLowerCase() === "type" ? 1 : 2 );
+ }
+ } );
+}
+
+// Support: IE<9
+// Use defaultValue in place of getAttribute("value")
+if ( !support.attributes || !assert( function( el ) {
+ el.innerHTML = "<input/>";
+ el.firstChild.setAttribute( "value", "" );
+ return el.firstChild.getAttribute( "value" ) === "";
+} ) ) {
+ addHandle( "value", function( elem, _name, isXML ) {
+ if ( !isXML && elem.nodeName.toLowerCase() === "input" ) {
+ return elem.defaultValue;
+ }
+ } );
+}
+
+// Support: IE<9
+// Use getAttributeNode to fetch booleans when getAttribute lies
+if ( !assert( function( el ) {
+ return el.getAttribute( "disabled" ) == null;
+} ) ) {
+ addHandle( booleans, function( elem, name, isXML ) {
+ var val;
+ if ( !isXML ) {
+ return elem[ name ] === true ? name.toLowerCase() :
+ ( val = elem.getAttributeNode( name ) ) && val.specified ?
+ val.value :
+ null;
+ }
+ } );
+}
+
+// EXPOSE
+var _sizzle = window.Sizzle;
+
+Sizzle.noConflict = function() {
+ if ( window.Sizzle === Sizzle ) {
+ window.Sizzle = _sizzle;
+ }
+
+ return Sizzle;
+};
+
+if ( typeof define === "function" && define.amd ) {
+ define( function() {
+ return Sizzle;
+ } );
+
+// Sizzle requires that there be a global window in Common-JS like environments
+} else if ( typeof module !== "undefined" && module.exports ) {
+ module.exports = Sizzle;
+} else {
+ window.Sizzle = Sizzle;
+}
+
+// EXPOSE
+
+} )( window );
diff --git a/node_modules/sizzle/dist/sizzle.min.js b/node_modules/sizzle/dist/sizzle.min.js
new file mode 100644
index 0000000..8c686df
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.min.js
@@ -0,0 +1,3 @@
+/*! Sizzle v2.3.10 | (c) JS Foundation and other contributors | js.foundation */
+!function(e){var t,n,r,i,o,u,l,a,c,s,f,d,p,h,g,m,y,v,w,b="sizzle"+1*new Date,N=e.document,C=0,x=0,E=ae(),A=ae(),S=ae(),D=ae(),T=function(e,t){return e===t&&(f=!0),0},L={}.hasOwnProperty,q=[],I=q.pop,B=q.push,R=q.push,$=q.slice,k=function(e,t){for(var n=0,r=e.length;n<r;n++)if(e[n]===t)return n;return-1},H="checked|selected|async|autofocus|autoplay|controls|defer|disabled|hidden|ismap|loop|multiple|open|readonly|required|scoped",M="[\\x20\\t\\r\\n\\f]",P="(?:\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\[^\\r\\n\\f]|[\\w-]|[^\0-\\x7f])+",z="\\["+M+"*("+P+")(?:"+M+"*([*^$|!~]?=)"+M+"*(?:'((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\"|("+P+"))|)"+M+"*\\]",F=":("+P+")(?:\\((('((?:\\\\.|[^\\\\'])*)'|\"((?:\\\\.|[^\\\\\"])*)\")|((?:\\\\.|[^\\\\()[\\]]|"+z+")*)|.*)\\)|)",O=new RegExp(M+"+","g"),j=new RegExp("^"+M+"+|((?:^|[^\\\\])(?:\\\\.)*)"+M+"+$","g"),G=new RegExp("^"+M+"*,"+M+"*"),U=new RegExp("^"+M+"*([>+~]|"+M+")"+M+"*"),V=new RegExp(M+"|>"),X=new RegExp(F),J=new RegExp("^"+P+"$"),K={ID:new RegExp("^#("+P+")"),CLASS:new RegExp("^\\.("+P+")"),TAG:new RegExp("^("+P+"|[*])"),ATTR:new RegExp("^"+z),PSEUDO:new RegExp("^"+F),CHILD:new RegExp("^:(only|first|last|nth|nth-last)-(child|of-type)(?:\\("+M+"*(even|odd|(([+-]|)(\\d*)n|)"+M+"*(?:([+-]|)"+M+"*(\\d+)|))"+M+"*\\)|)","i"),bool:new RegExp("^(?:"+H+")$","i"),needsContext:new RegExp("^"+M+"*[>+~]|:(even|odd|eq|gt|lt|nth|first|last)(?:\\("+M+"*((?:-\\d)?\\d*)"+M+"*\\)|)(?=[^-]|$)","i")},Q=/HTML$/i,W=/^(?:input|select|textarea|button)$/i,Y=/^h\d$/i,Z=/^[^{]+\{\s*\[native \w/,_=/^(?:#([\w-]+)|(\w+)|\.([\w-]+))$/,ee=/[+~]/,te=new RegExp("\\\\[\\da-fA-F]{1,6}"+M+"?|\\\\([^\\r\\n\\f])","g"),ne=function(e,t){var n="0x"+e.slice(1)-65536;return t||(n<0?String.fromCharCode(n+65536):String.fromCharCode(n>>10|55296,1023&n|56320))},re=/([\0-\x1f\x7f]|^-?\d)|^-$|[^\0-\x1f\x7f-\uFFFF\w-]/g,ie=function(e,t){return t?"\0"===e?"\ufffd":e.slice(0,-1)+"\\"+e.charCodeAt(e.length-1).toString(16)+" ":"\\"+e},oe=function(){d()},ue=ve(function(e){return!0===e.disabled&&"fieldset"===e.nodeName.toLowerCase()},{dir:"parentNode",next:"legend"});try{R.apply(q=$.call(N.childNodes),N.childNodes),q[N.childNodes.length].nodeType}catch(e){R={apply:q.length?function(e,t){B.apply(e,$.call(t))}:function(e,t){var n=e.length,r=0;while(e[n++]=t[r++]);e.length=n-1}}}function le(e,t,r,i){var o,l,c,s,f,h,y,v=t&&t.ownerDocument,N=t?t.nodeType:9;if(r=r||[],"string"!=typeof e||!e||1!==N&&9!==N&&11!==N)return r;if(!i&&(d(t),t=t||p,g)){if(11!==N&&(f=_.exec(e)))if(o=f[1]){if(9===N){if(!(c=t.getElementById(o)))return r;if(c.id===o)return r.push(c),r}else if(v&&(c=v.getElementById(o))&&w(t,c)&&c.id===o)return r.push(c),r}else{if(f[2])return R.apply(r,t.getElementsByTagName(e)),r;if((o=f[3])&&n.getElementsByClassName&&t.getElementsByClassName)return R.apply(r,t.getElementsByClassName(o)),r}if(n.qsa&&!D[e+" "]&&(!m||!m.test(e))&&(1!==N||"object"!==t.nodeName.toLowerCase())){if(y=e,v=t,1===N&&(V.test(e)||U.test(e))){(v=ee.test(e)&&ge(t.parentNode)||t)===t&&n.scope||((s=t.getAttribute("id"))?s=s.replace(re,ie):t.setAttribute("id",s=b)),l=(h=u(e)).length;while(l--)h[l]=(s?"#"+s:":scope")+" "+ye(h[l]);y=h.join(",")}try{return R.apply(r,v.querySelectorAll(y)),r}catch(t){D(e,!0)}finally{s===b&&t.removeAttribute("id")}}}return a(e.replace(j,"$1"),t,r,i)}function ae(){var e=[];function t(n,i){return e.push(n+" ")>r.cacheLength&&delete t[e.shift()],t[n+" "]=i}return t}function ce(e){return e[b]=!0,e}function se(e){var t=p.createElement("fieldset");try{return!!e(t)}catch(e){return!1}finally{t.parentNode&&t.parentNode.removeChild(t),t=null}}function fe(e,t){var n=e.split("|"),i=n.length;while(i--)r.attrHandle[n[i]]=t}function de(e,t){var n=t&&e,r=n&&1===e.nodeType&&1===t.nodeType&&e.sourceIndex-t.sourceIndex;if(r)return r;if(n)while(n=n.nextSibling)if(n===t)return-1;return e?1:-1}function pe(e){return function(t){return"form"in t?t.parentNode&&!1===t.disabled?"label"in t?"label"in t.parentNode?t.parentNode.disabled===e:t.disabled===e:t.isDisabled===e||t.isDisabled!==!e&&ue(t)===e:t.disabled===e:"label"in t&&t.disabled===e}}function he(e){return ce(function(t){return t=+t,ce(function(n,r){var i,o=e([],n.length,t),u=o.length;while(u--)n[i=o[u]]&&(n[i]=!(r[i]=n[i]))})})}function ge(e){return e&&void 0!==e.getElementsByTagName&&e}n=le.support={},o=le.isXML=function(e){var t=e&&e.namespaceURI,n=e&&(e.ownerDocument||e).documentElement;return!Q.test(t||n&&n.nodeName||"HTML")},d=le.setDocument=function(e){var t,i,u=e?e.ownerDocument||e:N;return u!=p&&9===u.nodeType&&u.documentElement?(p=u,h=p.documentElement,g=!o(p),N!=p&&(i=p.defaultView)&&i.top!==i&&(i.addEventListener?i.addEventListener("unload",oe,!1):i.attachEvent&&i.attachEvent("onunload",oe)),n.scope=se(function(e){return h.appendChild(e).appendChild(p.createElement("div")),void 0!==e.querySelectorAll&&!e.querySelectorAll(":scope fieldset div").length}),n.cssHas=se(function(){try{return p.querySelector(":has(*,:jqfake)"),!1}catch(e){return!0}}),n.attributes=se(function(e){return e.className="i",!e.getAttribute("className")}),n.getElementsByTagName=se(function(e){return e.appendChild(p.createComment("")),!e.getElementsByTagName("*").length}),n.getElementsByClassName=Z.test(p.getElementsByClassName),n.getById=se(function(e){return h.appendChild(e).id=b,!p.getElementsByName||!p.getElementsByName(b).length}),n.getById?(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){return e.getAttribute("id")===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n=t.getElementById(e);return n?[n]:[]}}):(r.filter.ID=function(e){var t=e.replace(te,ne);return function(e){var n=void 0!==e.getAttributeNode&&e.getAttributeNode("id");return n&&n.value===t}},r.find.ID=function(e,t){if(void 0!==t.getElementById&&g){var n,r,i,o=t.getElementById(e);if(o){if((n=o.getAttributeNode("id"))&&n.value===e)return[o];i=t.getElementsByName(e),r=0;while(o=i[r++])if((n=o.getAttributeNode("id"))&&n.value===e)return[o]}return[]}}),r.find.TAG=n.getElementsByTagName?function(e,t){return void 0!==t.getElementsByTagName?t.getElementsByTagName(e):n.qsa?t.querySelectorAll(e):void 0}:function(e,t){var n,r=[],i=0,o=t.getElementsByTagName(e);if("*"===e){while(n=o[i++])1===n.nodeType&&r.push(n);return r}return o},r.find.CLASS=n.getElementsByClassName&&function(e,t){if(void 0!==t.getElementsByClassName&&g)return t.getElementsByClassName(e)},y=[],m=[],(n.qsa=Z.test(p.querySelectorAll))&&(se(function(e){var t;h.appendChild(e).innerHTML="<a id='"+b+"'></a><select id='"+b+"-\r\\' msallowcapture=''><option selected=''></option></select>",e.querySelectorAll("[msallowcapture^='']").length&&m.push("[*^$]="+M+"*(?:''|\"\")"),e.querySelectorAll("[selected]").length||m.push("\\["+M+"*(?:value|"+H+")"),e.querySelectorAll("[id~="+b+"-]").length||m.push("~="),(t=p.createElement("input")).setAttribute("name",""),e.appendChild(t),e.querySelectorAll("[name='']").length||m.push("\\["+M+"*name"+M+"*="+M+"*(?:''|\"\")"),e.querySelectorAll(":checked").length||m.push(":checked"),e.querySelectorAll("a#"+b+"+*").length||m.push(".#.+[+~]"),e.querySelectorAll("\\\f"),m.push("[\\r\\n\\f]")}),se(function(e){e.innerHTML="<a href='' disabled='disabled'></a><select disabled='disabled'><option/></select>";var t=p.createElement("input");t.setAttribute("type","hidden"),e.appendChild(t).setAttribute("name","D"),e.querySelectorAll("[name=d]").length&&m.push("name"+M+"*[*^$|!~]?="),2!==e.querySelectorAll(":enabled").length&&m.push(":enabled",":disabled"),h.appendChild(e).disabled=!0,2!==e.querySelectorAll(":disabled").length&&m.push(":enabled",":disabled"),e.querySelectorAll("*,:x"),m.push(",.*:")})),(n.matchesSelector=Z.test(v=h.matches||h.webkitMatchesSelector||h.mozMatchesSelector||h.oMatchesSelector||h.msMatchesSelector))&&se(function(e){n.disconnectedMatch=v.call(e,"*"),v.call(e,"[s!='']:x"),y.push("!=",F)}),n.cssHas||m.push(":has"),m=m.length&&new RegExp(m.join("|")),y=y.length&&new RegExp(y.join("|")),t=Z.test(h.compareDocumentPosition),w=t||Z.test(h.contains)?function(e,t){var n=9===e.nodeType&&e.documentElement||e,r=t&&t.parentNode;return e===r||!(!r||1!==r.nodeType||!(n.contains?n.contains(r):e.compareDocumentPosition&&16&e.compareDocumentPosition(r)))}:function(e,t){if(t)while(t=t.parentNode)if(t===e)return!0;return!1},T=t?function(e,t){if(e===t)return f=!0,0;var r=!e.compareDocumentPosition-!t.compareDocumentPosition;return r||(1&(r=(e.ownerDocument||e)==(t.ownerDocument||t)?e.compareDocumentPosition(t):1)||!n.sortDetached&&t.compareDocumentPosition(e)===r?e==p||e.ownerDocument==N&&w(N,e)?-1:t==p||t.ownerDocument==N&&w(N,t)?1:s?k(s,e)-k(s,t):0:4&r?-1:1)}:function(e,t){if(e===t)return f=!0,0;var n,r=0,i=e.parentNode,o=t.parentNode,u=[e],l=[t];if(!i||!o)return e==p?-1:t==p?1:i?-1:o?1:s?k(s,e)-k(s,t):0;if(i===o)return de(e,t);n=e;while(n=n.parentNode)u.unshift(n);n=t;while(n=n.parentNode)l.unshift(n);while(u[r]===l[r])r++;return r?de(u[r],l[r]):u[r]==N?-1:l[r]==N?1:0},p):p},le.matches=function(e,t){return le(e,null,null,t)},le.matchesSelector=function(e,t){if(d(e),n.matchesSelector&&g&&!D[t+" "]&&(!y||!y.test(t))&&(!m||!m.test(t)))try{var r=v.call(e,t);if(r||n.disconnectedMatch||e.document&&11!==e.document.nodeType)return r}catch(e){D(t,!0)}return le(t,p,null,[e]).length>0},le.contains=function(e,t){return(e.ownerDocument||e)!=p&&d(e),w(e,t)},le.attr=function(e,t){(e.ownerDocument||e)!=p&&d(e);var i=r.attrHandle[t.toLowerCase()],o=i&&L.call(r.attrHandle,t.toLowerCase())?i(e,t,!g):void 0;return void 0!==o?o:n.attributes||!g?e.getAttribute(t):(o=e.getAttributeNode(t))&&o.specified?o.value:null},le.escape=function(e){return(e+"").replace(re,ie)},le.error=function(e){throw new Error("Syntax error, unrecognized expression: "+e)},le.uniqueSort=function(e){var t,r=[],i=0,o=0;if(f=!n.detectDuplicates,s=!n.sortStable&&e.slice(0),e.sort(T),f){while(t=e[o++])t===e[o]&&(i=r.push(o));while(i--)e.splice(r[i],1)}return s=null,e},i=le.getText=function(e){var t,n="",r=0,o=e.nodeType;if(o){if(1===o||9===o||11===o){if("string"==typeof e.textContent)return e.textContent;for(e=e.firstChild;e;e=e.nextSibling)n+=i(e)}else if(3===o||4===o)return e.nodeValue}else while(t=e[r++])n+=i(t);return n},(r=le.selectors={cacheLength:50,createPseudo:ce,match:K,attrHandle:{},find:{},relative:{">":{dir:"parentNode",first:!0}," ":{dir:"parentNode"},"+":{dir:"previousSibling",first:!0},"~":{dir:"previousSibling"}},preFilter:{ATTR:function(e){return e[1]=e[1].replace(te,ne),e[3]=(e[3]||e[4]||e[5]||"").replace(te,ne),"~="===e[2]&&(e[3]=" "+e[3]+" "),e.slice(0,4)},CHILD:function(e){return e[1]=e[1].toLowerCase(),"nth"===e[1].slice(0,3)?(e[3]||le.error(e[0]),e[4]=+(e[4]?e[5]+(e[6]||1):2*("even"===e[3]||"odd"===e[3])),e[5]=+(e[7]+e[8]||"odd"===e[3])):e[3]&&le.error(e[0]),e},PSEUDO:function(e){var t,n=!e[6]&&e[2];return K.CHILD.test(e[0])?null:(e[3]?e[2]=e[4]||e[5]||"":n&&X.test(n)&&(t=u(n,!0))&&(t=n.indexOf(")",n.length-t)-n.length)&&(e[0]=e[0].slice(0,t),e[2]=n.slice(0,t)),e.slice(0,3))}},filter:{TAG:function(e){var t=e.replace(te,ne).toLowerCase();return"*"===e?function(){return!0}:function(e){return e.nodeName&&e.nodeName.toLowerCase()===t}},CLASS:function(e){var t=E[e+" "];return t||(t=new RegExp("(^|"+M+")"+e+"("+M+"|$)"))&&E(e,function(e){return t.test("string"==typeof e.className&&e.className||void 0!==e.getAttribute&&e.getAttribute("class")||"")})},ATTR:function(e,t,n){return function(r){var i=le.attr(r,e);return null==i?"!="===t:!t||(i+="","="===t?i===n:"!="===t?i!==n:"^="===t?n&&0===i.indexOf(n):"*="===t?n&&i.indexOf(n)>-1:"$="===t?n&&i.slice(-n.length)===n:"~="===t?(" "+i.replace(O," ")+" ").indexOf(n)>-1:"|="===t&&(i===n||i.slice(0,n.length+1)===n+"-"))}},CHILD:function(e,t,n,r,i){var o="nth"!==e.slice(0,3),u="last"!==e.slice(-4),l="of-type"===t;return 1===r&&0===i?function(e){return!!e.parentNode}:function(t,n,a){var c,s,f,d,p,h,g=o!==u?"nextSibling":"previousSibling",m=t.parentNode,y=l&&t.nodeName.toLowerCase(),v=!a&&!l,w=!1;if(m){if(o){while(g){d=t;while(d=d[g])if(l?d.nodeName.toLowerCase()===y:1===d.nodeType)return!1;h=g="only"===e&&!h&&"nextSibling"}return!0}if(h=[u?m.firstChild:m.lastChild],u&&v){w=(p=(c=(s=(f=(d=m)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&c[1])&&c[2],d=p&&m.childNodes[p];while(d=++p&&d&&d[g]||(w=p=0)||h.pop())if(1===d.nodeType&&++w&&d===t){s[e]=[C,p,w];break}}else if(v&&(w=p=(c=(s=(f=(d=t)[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]||[])[0]===C&&c[1]),!1===w)while(d=++p&&d&&d[g]||(w=p=0)||h.pop())if((l?d.nodeName.toLowerCase()===y:1===d.nodeType)&&++w&&(v&&((s=(f=d[b]||(d[b]={}))[d.uniqueID]||(f[d.uniqueID]={}))[e]=[C,w]),d===t))break;return(w-=i)===r||w%r==0&&w/r>=0}}},PSEUDO:function(e,t){var n,i=r.pseudos[e]||r.setFilters[e.toLowerCase()]||le.error("unsupported pseudo: "+e);return i[b]?i(t):i.length>1?(n=[e,e,"",t],r.setFilters.hasOwnProperty(e.toLowerCase())?ce(function(e,n){var r,o=i(e,t),u=o.length;while(u--)e[r=k(e,o[u])]=!(n[r]=o[u])}):function(e){return i(e,0,n)}):i}},pseudos:{not:ce(function(e){var t=[],n=[],r=l(e.replace(j,"$1"));return r[b]?ce(function(e,t,n,i){var o,u=r(e,null,i,[]),l=e.length;while(l--)(o=u[l])&&(e[l]=!(t[l]=o))}):function(e,i,o){return t[0]=e,r(t,null,o,n),t[0]=null,!n.pop()}}),has:ce(function(e){return function(t){return le(e,t).length>0}}),contains:ce(function(e){return e=e.replace(te,ne),function(t){return(t.textContent||i(t)).indexOf(e)>-1}}),lang:ce(function(e){return J.test(e||"")||le.error("unsupported lang: "+e),e=e.replace(te,ne).toLowerCase(),function(t){var n;do{if(n=g?t.lang:t.getAttribute("xml:lang")||t.getAttribute("lang"))return(n=n.toLowerCase())===e||0===n.indexOf(e+"-")}while((t=t.parentNode)&&1===t.nodeType);return!1}}),target:function(t){var n=e.location&&e.location.hash;return n&&n.slice(1)===t.id},root:function(e){return e===h},focus:function(e){return e===p.activeElement&&(!p.hasFocus||p.hasFocus())&&!!(e.type||e.href||~e.tabIndex)},enabled:pe(!1),disabled:pe(!0),checked:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&!!e.checked||"option"===t&&!!e.selected},selected:function(e){return e.parentNode&&e.parentNode.selectedIndex,!0===e.selected},empty:function(e){for(e=e.firstChild;e;e=e.nextSibling)if(e.nodeType<6)return!1;return!0},parent:function(e){return!r.pseudos.empty(e)},header:function(e){return Y.test(e.nodeName)},input:function(e){return W.test(e.nodeName)},button:function(e){var t=e.nodeName.toLowerCase();return"input"===t&&"button"===e.type||"button"===t},text:function(e){var t;return"input"===e.nodeName.toLowerCase()&&"text"===e.type&&(null==(t=e.getAttribute("type"))||"text"===t.toLowerCase())},first:he(function(){return[0]}),last:he(function(e,t){return[t-1]}),eq:he(function(e,t,n){return[n<0?n+t:n]}),even:he(function(e,t){for(var n=0;n<t;n+=2)e.push(n);return e}),odd:he(function(e,t){for(var n=1;n<t;n+=2)e.push(n);return e}),lt:he(function(e,t,n){for(var r=n<0?n+t:n>t?t:n;--r>=0;)e.push(r);return e}),gt:he(function(e,t,n){for(var r=n<0?n+t:n;++r<t;)e.push(r);return e})}}).pseudos.nth=r.pseudos.eq;for(t in{radio:!0,checkbox:!0,file:!0,password:!0,image:!0})r.pseudos[t]=function(e){return function(t){return"input"===t.nodeName.toLowerCase()&&t.type===e}}(t);for(t in{submit:!0,reset:!0})r.pseudos[t]=function(e){return function(t){var n=t.nodeName.toLowerCase();return("input"===n||"button"===n)&&t.type===e}}(t);function me(){}me.prototype=r.filters=r.pseudos,r.setFilters=new me,u=le.tokenize=function(e,t){var n,i,o,u,l,a,c,s=A[e+" "];if(s)return t?0:s.slice(0);l=e,a=[],c=r.preFilter;while(l){n&&!(i=G.exec(l))||(i&&(l=l.slice(i[0].length)||l),a.push(o=[])),n=!1,(i=U.exec(l))&&(n=i.shift(),o.push({value:n,type:i[0].replace(j," ")}),l=l.slice(n.length));for(u in r.filter)!(i=K[u].exec(l))||c[u]&&!(i=c[u](i))||(n=i.shift(),o.push({value:n,type:u,matches:i}),l=l.slice(n.length));if(!n)break}return t?l.length:l?le.error(e):A(e,a).slice(0)};function ye(e){for(var t=0,n=e.length,r="";t<n;t++)r+=e[t].value;return r}function ve(e,t,n){var r=t.dir,i=t.next,o=i||r,u=n&&"parentNode"===o,l=x++;return t.first?function(t,n,i){while(t=t[r])if(1===t.nodeType||u)return e(t,n,i);return!1}:function(t,n,a){var c,s,f,d=[C,l];if(a){while(t=t[r])if((1===t.nodeType||u)&&e(t,n,a))return!0}else while(t=t[r])if(1===t.nodeType||u)if(f=t[b]||(t[b]={}),s=f[t.uniqueID]||(f[t.uniqueID]={}),i&&i===t.nodeName.toLowerCase())t=t[r]||t;else{if((c=s[o])&&c[0]===C&&c[1]===l)return d[2]=c[2];if(s[o]=d,d[2]=e(t,n,a))return!0}return!1}}function we(e){return e.length>1?function(t,n,r){var i=e.length;while(i--)if(!e[i](t,n,r))return!1;return!0}:e[0]}function be(e,t,n){for(var r=0,i=t.length;r<i;r++)le(e,t[r],n);return n}function Ne(e,t,n,r,i){for(var o,u=[],l=0,a=e.length,c=null!=t;l<a;l++)(o=e[l])&&(n&&!n(o,r,i)||(u.push(o),c&&t.push(l)));return u}function Ce(e,t,n,r,i,o){return r&&!r[b]&&(r=Ce(r)),i&&!i[b]&&(i=Ce(i,o)),ce(function(o,u,l,a){var c,s,f,d=[],p=[],h=u.length,g=o||be(t||"*",l.nodeType?[l]:l,[]),m=!e||!o&&t?g:Ne(g,d,e,l,a),y=n?i||(o?e:h||r)?[]:u:m;if(n&&n(m,y,l,a),r){c=Ne(y,p),r(c,[],l,a),s=c.length;while(s--)(f=c[s])&&(y[p[s]]=!(m[p[s]]=f))}if(o){if(i||e){if(i){c=[],s=y.length;while(s--)(f=y[s])&&c.push(m[s]=f);i(null,y=[],c,a)}s=y.length;while(s--)(f=y[s])&&(c=i?k(o,f):d[s])>-1&&(o[c]=!(u[c]=f))}}else y=Ne(y===u?y.splice(h,y.length):y),i?i(null,u,y,a):R.apply(u,y)})}function xe(e){for(var t,n,i,o=e.length,u=r.relative[e[0].type],l=u||r.relative[" "],a=u?1:0,s=ve(function(e){return e===t},l,!0),f=ve(function(e){return k(t,e)>-1},l,!0),d=[function(e,n,r){var i=!u&&(r||n!==c)||((t=n).nodeType?s(e,n,r):f(e,n,r));return t=null,i}];a<o;a++)if(n=r.relative[e[a].type])d=[ve(we(d),n)];else{if((n=r.filter[e[a].type].apply(null,e[a].matches))[b]){for(i=++a;i<o;i++)if(r.relative[e[i].type])break;return Ce(a>1&&we(d),a>1&&ye(e.slice(0,a-1).concat({value:" "===e[a-2].type?"*":""})).replace(j,"$1"),n,a<i&&xe(e.slice(a,i)),i<o&&xe(e=e.slice(i)),i<o&&ye(e))}d.push(n)}return we(d)}function Ee(e,t){var n=t.length>0,i=e.length>0,o=function(o,u,l,a,s){var f,h,m,y=0,v="0",w=o&&[],b=[],N=c,x=o||i&&r.find.TAG("*",s),E=C+=null==N?1:Math.random()||.1,A=x.length;for(s&&(c=u==p||u||s);v!==A&&null!=(f=x[v]);v++){if(i&&f){h=0,u||f.ownerDocument==p||(d(f),l=!g);while(m=e[h++])if(m(f,u||p,l)){a.push(f);break}s&&(C=E)}n&&((f=!m&&f)&&y--,o&&w.push(f))}if(y+=v,n&&v!==y){h=0;while(m=t[h++])m(w,b,u,l);if(o){if(y>0)while(v--)w[v]||b[v]||(b[v]=I.call(a));b=Ne(b)}R.apply(a,b),s&&!o&&b.length>0&&y+t.length>1&&le.uniqueSort(a)}return s&&(C=E,c=N),w};return n?ce(o):o}l=le.compile=function(e,t){var n,r=[],i=[],o=S[e+" "];if(!o){t||(t=u(e)),n=t.length;while(n--)(o=xe(t[n]))[b]?r.push(o):i.push(o);(o=S(e,Ee(i,r))).selector=e}return o},a=le.select=function(e,t,n,i){var o,a,c,s,f,d="function"==typeof e&&e,p=!i&&u(e=d.selector||e);if(n=n||[],1===p.length){if((a=p[0]=p[0].slice(0)).length>2&&"ID"===(c=a[0]).type&&9===t.nodeType&&g&&r.relative[a[1].type]){if(!(t=(r.find.ID(c.matches[0].replace(te,ne),t)||[])[0]))return n;d&&(t=t.parentNode),e=e.slice(a.shift().value.length)}o=K.needsContext.test(e)?0:a.length;while(o--){if(c=a[o],r.relative[s=c.type])break;if((f=r.find[s])&&(i=f(c.matches[0].replace(te,ne),ee.test(a[0].type)&&ge(t.parentNode)||t))){if(a.splice(o,1),!(e=i.length&&ye(a)))return R.apply(n,i),n;break}}}return(d||l(e,p))(i,t,!g,n,!t||ee.test(e)&&ge(t.parentNode)||t),n},n.sortStable=b.split("").sort(T).join("")===b,n.detectDuplicates=!!f,d(),n.sortDetached=se(function(e){return 1&e.compareDocumentPosition(p.createElement("fieldset"))}),se(function(e){return e.innerHTML="<a href='#'></a>","#"===e.firstChild.getAttribute("href")})||fe("type|href|height|width",function(e,t,n){if(!n)return e.getAttribute(t,"type"===t.toLowerCase()?1:2)}),n.attributes&&se(function(e){return e.innerHTML="<input/>",e.firstChild.setAttribute("value",""),""===e.firstChild.getAttribute("value")})||fe("value",function(e,t,n){if(!n&&"input"===e.nodeName.toLowerCase())return e.defaultValue}),se(function(e){return null==e.getAttribute("disabled")})||fe(H,function(e,t,n){var r;if(!n)return!0===e[t]?t.toLowerCase():(r=e.getAttributeNode(t))&&r.specified?r.value:null});var Ae=e.Sizzle;le.noConflict=function(){return e.Sizzle===le&&(e.Sizzle=Ae),le},"function"==typeof define&&define.amd?define(function(){return le}):"undefined"!=typeof module&&module.exports?module.exports=le:e.Sizzle=le}(window);
+//# sourceMappingURL=sizzle.min.map \ No newline at end of file
diff --git a/node_modules/sizzle/dist/sizzle.min.map b/node_modules/sizzle/dist/sizzle.min.map
new file mode 100644
index 0000000..43f672d
--- /dev/null
+++ b/node_modules/sizzle/dist/sizzle.min.map
@@ -0,0 +1 @@
+{"version":3,"sources":["sizzle.js"],"names":["window","i","support","Expr","getText","isXML","tokenize","compile","select","outermostContext","sortInput","hasDuplicate","setDocument","document","docElem","documentIsHTML","rbuggyQSA","rbuggyMatches","matches","contains","expando","Date","preferredDoc","dirruns","done","classCache","createCache","tokenCache","compilerCache","nonnativeSelectorCache","sortOrder","a","b","hasOwn","hasOwnProperty","arr","pop","pushNative","push","slice","indexOf","list","elem","len","length","booleans","whitespace","identifier","attributes","pseudos","rwhitespace","RegExp","rtrim","rcomma","rleadingCombinator","rdescend","rpseudo","ridentifier","matchExpr","ID","CLASS","TAG","ATTR","PSEUDO","CHILD","bool","needsContext","rhtml","rinputs","rheader","rnative","rquickExpr","rsibling","runescape","funescape","escape","nonHex","high","String","fromCharCode","rcssescape","fcssescape","ch","asCodePoint","charCodeAt","toString","unloadHandler","inDisabledFieldset","addCombinator","disabled","nodeName","toLowerCase","dir","next","apply","call","childNodes","nodeType","e","target","els","j","Sizzle","selector","context","results","seed","m","nid","match","groups","newSelector","newContext","ownerDocument","exec","getElementById","id","getElementsByTagName","getElementsByClassName","qsa","test","testContext","parentNode","scope","getAttribute","replace","setAttribute","toSelector","join","querySelectorAll","qsaError","removeAttribute","keys","cache","key","value","cacheLength","shift","markFunction","fn","assert","el","createElement","removeChild","addHandle","attrs","handler","split","attrHandle","siblingCheck","cur","diff","sourceIndex","nextSibling","createDisabledPseudo","isDisabled","createPositionalPseudo","argument","matchIndexes","namespace","namespaceURI","documentElement","node","hasCompare","subWindow","doc","defaultView","top","addEventListener","attachEvent","appendChild","cssHas","querySelector","className","createComment","getById","getElementsByName","filter","attrId","find","getAttributeNode","elems","tag","tmp","input","innerHTML","matchesSelector","webkitMatchesSelector","mozMatchesSelector","oMatchesSelector","msMatchesSelector","disconnectedMatch","compareDocumentPosition","adown","bup","compare","sortDetached","aup","ap","bp","unshift","expr","elements","ret","attr","name","val","undefined","specified","sel","error","msg","Error","uniqueSort","duplicates","detectDuplicates","sortStable","sort","splice","textContent","firstChild","nodeValue","selectors","createPseudo","relative",">","first"," ","+","~","preFilter","excess","unquoted","nodeNameSelector","pattern","operator","check","result","type","what","_argument","last","simple","forward","ofType","_context","xml","uniqueCache","outerCache","nodeIndex","start","parent","useCache","lastChild","uniqueID","pseudo","args","setFilters","idx","matched","not","matcher","unmatched","has","text","lang","elemLang","hash","location","root","focus","activeElement","hasFocus","href","tabIndex","enabled","checked","selected","selectedIndex","empty","header","button","_matchIndexes","eq","even","odd","lt","gt","radio","checkbox","file","password","image","createInputPseudo","submit","reset","createButtonPseudo","prototype","filters","parseOnly","tokens","soFar","preFilters","cached","combinator","base","skip","checkNonElements","doneName","oldCache","newCache","elementMatcher","matchers","multipleContexts","contexts","condense","map","newUnmatched","mapped","setMatcher","postFilter","postFinder","postSelector","temp","preMap","postMap","preexisting","matcherIn","matcherOut","matcherFromTokens","checkContext","leadingRelative","implicitRelative","matchContext","matchAnyContext","concat","matcherFromGroupMatchers","elementMatchers","setMatchers","bySet","byElement","superMatcher","outermost","matchedCount","setMatched","contextBackup","dirrunsUnique","Math","random","token","compiled","_name","defaultValue","_sizzle","noConflict","define","amd","module","exports"],"mappings":";CAUA,SAAYA,GACZ,IAAIC,EACHC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAGAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EACAC,EAGAC,EAAU,SAAW,EAAI,IAAIC,KAC7BC,EAAetB,EAAOa,SACtBU,EAAU,EACVC,EAAO,EACPC,EAAaC,KACbC,EAAaD,KACbE,EAAgBF,KAChBG,EAAyBH,KACzBI,EAAY,SAAUC,EAAGC,GAIxB,OAHKD,IAAMC,IACVrB,GAAe,GAET,GAIRsB,KAAgBC,eAChBC,KACAC,EAAMD,EAAIC,IACVC,EAAaF,EAAIG,KACjBA,EAAOH,EAAIG,KACXC,EAAQJ,EAAII,MAIZC,EAAU,SAAUC,EAAMC,GAGzB,IAFA,IAAIzC,EAAI,EACP0C,EAAMF,EAAKG,OACJ3C,EAAI0C,EAAK1C,IAChB,GAAKwC,EAAMxC,KAAQyC,EAClB,OAAOzC,EAGT,OAAQ,GAGT4C,EAAW,6HAMXC,EAAa,sBAGbC,EAAa,0BAA4BD,EACxC,0CAGDE,EAAa,MAAQF,EAAa,KAAOC,EAAa,OAASD,EAG9D,gBAAkBA,EAIlB,2DAA6DC,EAAa,OAC1ED,EAAa,OAEdG,EAAU,KAAOF,EAAa,wFAOAC,EAAa,eAO3CE,EAAc,IAAIC,OAAQL,EAAa,IAAK,KAC5CM,EAAQ,IAAID,OAAQ,IAAML,EAAa,8BACtCA,EAAa,KAAM,KAEpBO,EAAS,IAAIF,OAAQ,IAAML,EAAa,KAAOA,EAAa,KAC5DQ,EAAqB,IAAIH,OAAQ,IAAML,EAAa,WAAaA,EAAa,IAAMA,EACnF,KACDS,EAAW,IAAIJ,OAAQL,EAAa,MAEpCU,EAAU,IAAIL,OAAQF,GACtBQ,EAAc,IAAIN,OAAQ,IAAMJ,EAAa,KAE7CW,GACCC,GAAM,IAAIR,OAAQ,MAAQJ,EAAa,KACvCa,MAAS,IAAIT,OAAQ,QAAUJ,EAAa,KAC5Cc,IAAO,IAAIV,OAAQ,KAAOJ,EAAa,SACvCe,KAAQ,IAAIX,OAAQ,IAAMH,GAC1Be,OAAU,IAAIZ,OAAQ,IAAMF,GAC5Be,MAAS,IAAIb,OAAQ,yDACpBL,EAAa,+BAAiCA,EAAa,cAC3DA,EAAa,aAAeA,EAAa,SAAU,KACpDmB,KAAQ,IAAId,OAAQ,OAASN,EAAW,KAAM,KAI9CqB,aAAgB,IAAIf,OAAQ,IAAML,EACjC,mDAAqDA,EACrD,mBAAqBA,EAAa,mBAAoB,MAGxDqB,EAAQ,SACRC,EAAU,sCACVC,EAAU,SAEVC,EAAU,yBAGVC,EAAa,mCAEbC,GAAW,OAIXC,GAAY,IAAItB,OAAQ,uBAAyBL,EAAa,uBAAwB,KACtF4B,GAAY,SAAUC,EAAQC,GAC7B,IAAIC,EAAO,KAAOF,EAAOpC,MAAO,GAAM,MAEtC,OAAOqC,IASNC,EAAO,EACNC,OAAOC,aAAcF,EAAO,OAC5BC,OAAOC,aAAcF,GAAQ,GAAK,MAAe,KAAPA,EAAe,SAK5DG,GAAa,sDACbC,GAAa,SAAUC,EAAIC,GAC1B,OAAKA,EAGQ,OAAPD,EACG,SAIDA,EAAG3C,MAAO,GAAI,GAAM,KAC1B2C,EAAGE,WAAYF,EAAGtC,OAAS,GAAIyC,SAAU,IAAO,IAI3C,KAAOH,GAOfI,GAAgB,WACf1E,KAGD2E,GAAqBC,GACpB,SAAU9C,GACT,OAAyB,IAAlBA,EAAK+C,UAAqD,aAAhC/C,EAAKgD,SAASC,gBAE9CC,IAAK,aAAcC,KAAM,WAI7B,IACCvD,EAAKwD,MACF3D,EAAMI,EAAMwD,KAAMzE,EAAa0E,YACjC1E,EAAa0E,YAMd7D,EAAKb,EAAa0E,WAAWpD,QAASqD,SACrC,MAAQC,GACT5D,GAASwD,MAAO3D,EAAIS,OAGnB,SAAUuD,EAAQC,GACjB/D,EAAWyD,MAAOK,EAAQ5D,EAAMwD,KAAMK,KAKvC,SAAUD,EAAQC,GACjB,IAAIC,EAAIF,EAAOvD,OACd3C,EAAI,EAGL,MAAUkG,EAAQE,KAAQD,EAAKnG,MAC/BkG,EAAOvD,OAASyD,EAAI,IAKvB,SAASC,GAAQC,EAAUC,EAASC,EAASC,GAC5C,IAAIC,EAAG1G,EAAGyC,EAAMkE,EAAKC,EAAOC,EAAQC,EACnCC,EAAaR,GAAWA,EAAQS,cAGhChB,EAAWO,EAAUA,EAAQP,SAAW,EAKzC,GAHAQ,EAAUA,MAGe,iBAAbF,IAA0BA,GACxB,IAAbN,GAA+B,IAAbA,GAA+B,KAAbA,EAEpC,OAAOQ,EAIR,IAAMC,IACL9F,EAAa4F,GACbA,EAAUA,GAAW3F,EAEhBE,GAAiB,CAIrB,GAAkB,KAAbkF,IAAqBY,EAAQtC,EAAW2C,KAAMX,IAGlD,GAAOI,EAAIE,EAAO,IAGjB,GAAkB,IAAbZ,EAAiB,CACrB,KAAOvD,EAAO8D,EAAQW,eAAgBR,IAUrC,OAAOF,EALP,GAAK/D,EAAK0E,KAAOT,EAEhB,OADAF,EAAQnE,KAAMI,GACP+D,OAYT,GAAKO,IAAgBtE,EAAOsE,EAAWG,eAAgBR,KACtDxF,EAAUqF,EAAS9D,IACnBA,EAAK0E,KAAOT,EAGZ,OADAF,EAAQnE,KAAMI,GACP+D,MAKH,CAAA,GAAKI,EAAO,GAElB,OADAvE,EAAKwD,MAAOW,EAASD,EAAQa,qBAAsBd,IAC5CE,EAGD,IAAOE,EAAIE,EAAO,KAAS3G,EAAQoH,wBACzCd,EAAQc,uBAGR,OADAhF,EAAKwD,MAAOW,EAASD,EAAQc,uBAAwBX,IAC9CF,EAKT,GAAKvG,EAAQqH,MACX1F,EAAwB0E,EAAW,QACjCvF,IAAcA,EAAUwG,KAAMjB,MAIlB,IAAbN,GAAqD,WAAnCO,EAAQd,SAASC,eAA+B,CAYpE,GAVAoB,EAAcR,EACdS,EAAaR,EASK,IAAbP,IACF1C,EAASiE,KAAMjB,IAAcjD,EAAmBkE,KAAMjB,IAAe,EAGvES,EAAaxC,GAASgD,KAAMjB,IAAckB,GAAajB,EAAQkB,aAC9DlB,KAImBA,GAAYtG,EAAQyH,SAGhCf,EAAMJ,EAAQoB,aAAc,OAClChB,EAAMA,EAAIiB,QAAS7C,GAAYC,IAE/BuB,EAAQsB,aAAc,KAAQlB,EAAMxF,IAMtCnB,GADA6G,EAASxG,EAAUiG,IACR3D,OACX,MAAQ3C,IACP6G,EAAQ7G,IAAQ2G,EAAM,IAAMA,EAAM,UAAa,IAC9CmB,GAAYjB,EAAQ7G,IAEtB8G,EAAcD,EAAOkB,KAAM,KAG5B,IAIC,OAHA1F,EAAKwD,MAAOW,EACXO,EAAWiB,iBAAkBlB,IAEvBN,EACN,MAAQyB,GACTrG,EAAwB0E,GAAU,GACjC,QACIK,IAAQxF,GACZoF,EAAQ2B,gBAAiB,QAQ9B,OAAO3H,EAAQ+F,EAASsB,QAASzE,EAAO,MAAQoD,EAASC,EAASC,GASnE,SAAShF,KACR,IAAI0G,KAEJ,SAASC,EAAOC,EAAKC,GAQpB,OALKH,EAAK9F,KAAMgG,EAAM,KAAQnI,EAAKqI,oBAG3BH,EAAOD,EAAKK,SAEXJ,EAAOC,EAAM,KAAQC,EAE/B,OAAOF,EAOR,SAASK,GAAcC,GAEtB,OADAA,EAAIvH,IAAY,EACTuH,EAOR,SAASC,GAAQD,GAChB,IAAIE,EAAKhI,EAASiI,cAAe,YAEjC,IACC,QAASH,EAAIE,GACZ,MAAQ3C,GACT,OAAO,EACN,QAGI2C,EAAGnB,YACPmB,EAAGnB,WAAWqB,YAAaF,GAI5BA,EAAK,MASP,SAASG,GAAWC,EAAOC,GAC1B,IAAI/G,EAAM8G,EAAME,MAAO,KACtBlJ,EAAIkC,EAAIS,OAET,MAAQ3C,IACPE,EAAKiJ,WAAYjH,EAAKlC,IAAQiJ,EAUhC,SAASG,GAActH,EAAGC,GACzB,IAAIsH,EAAMtH,GAAKD,EACdwH,EAAOD,GAAsB,IAAfvH,EAAEkE,UAAiC,IAAfjE,EAAEiE,UACnClE,EAAEyH,YAAcxH,EAAEwH,YAGpB,GAAKD,EACJ,OAAOA,EAIR,GAAKD,EACJ,MAAUA,EAAMA,EAAIG,YACnB,GAAKH,IAAQtH,EACZ,OAAQ,EAKX,OAAOD,EAAI,GAAK,EA6BjB,SAAS2H,GAAsBjE,GAG9B,OAAO,SAAU/C,GAKhB,MAAK,SAAUA,EASTA,EAAKgF,aAAgC,IAAlBhF,EAAK+C,SAGvB,UAAW/C,EACV,UAAWA,EAAKgF,WACbhF,EAAKgF,WAAWjC,WAAaA,EAE7B/C,EAAK+C,WAAaA,EAMpB/C,EAAKiH,aAAelE,GAI1B/C,EAAKiH,cAAgBlE,GACrBF,GAAoB7C,KAAW+C,EAG1B/C,EAAK+C,WAAaA,EAKd,UAAW/C,GACfA,EAAK+C,WAAaA,GAY5B,SAASmE,GAAwBjB,GAChC,OAAOD,GAAc,SAAUmB,GAE9B,OADAA,GAAYA,EACLnB,GAAc,SAAUhC,EAAMxF,GACpC,IAAImF,EACHyD,EAAenB,KAAQjC,EAAK9D,OAAQiH,GACpC5J,EAAI6J,EAAalH,OAGlB,MAAQ3C,IACFyG,EAAQL,EAAIyD,EAAc7J,MAC9ByG,EAAML,KAASnF,EAASmF,GAAMK,EAAML,SAYzC,SAASoB,GAAajB,GACrB,OAAOA,QAAmD,IAAjCA,EAAQa,sBAAwCb,EAI1EtG,EAAUoG,GAAOpG,WAOjBG,EAAQiG,GAAOjG,MAAQ,SAAUqC,GAChC,IAAIqH,EAAYrH,GAAQA,EAAKsH,aAC5BlJ,EAAU4B,IAAUA,EAAKuE,eAAiBvE,GAAOuH,gBAKlD,OAAQ9F,EAAMqD,KAAMuC,GAAajJ,GAAWA,EAAQ4E,UAAY,SAQjE9E,EAAc0F,GAAO1F,YAAc,SAAUsJ,GAC5C,IAAIC,EAAYC,EACfC,EAAMH,EAAOA,EAAKjD,eAAiBiD,EAAO5I,EAO3C,OAAK+I,GAAOxJ,GAA6B,IAAjBwJ,EAAIpE,UAAmBoE,EAAIJ,iBAKnDpJ,EAAWwJ,EACXvJ,EAAUD,EAASoJ,gBACnBlJ,GAAkBV,EAAOQ,GAQpBS,GAAgBT,IAClBuJ,EAAYvJ,EAASyJ,cAAiBF,EAAUG,MAAQH,IAGrDA,EAAUI,iBACdJ,EAAUI,iBAAkB,SAAUlF,IAAe,GAG1C8E,EAAUK,aACrBL,EAAUK,YAAa,WAAYnF,KASrCpF,EAAQyH,MAAQiB,GAAQ,SAAUC,GAEjC,OADA/H,EAAQ4J,YAAa7B,GAAK6B,YAAa7J,EAASiI,cAAe,aACzB,IAAxBD,EAAGZ,mBACfY,EAAGZ,iBAAkB,uBAAwBrF,SAYhD1C,EAAQyK,OAAS/B,GAAQ,WACxB,IAEC,OADA/H,EAAS+J,cAAe,oBACjB,EACN,MAAQ1E,GACT,OAAO,KAUThG,EAAQ8C,WAAa4F,GAAQ,SAAUC,GAEtC,OADAA,EAAGgC,UAAY,KACPhC,EAAGjB,aAAc,eAO1B1H,EAAQmH,qBAAuBuB,GAAQ,SAAUC,GAEhD,OADAA,EAAG6B,YAAa7J,EAASiK,cAAe,MAChCjC,EAAGxB,qBAAsB,KAAMzE,SAIxC1C,EAAQoH,uBAAyBhD,EAAQkD,KAAM3G,EAASyG,wBAMxDpH,EAAQ6K,QAAUnC,GAAQ,SAAUC,GAEnC,OADA/H,EAAQ4J,YAAa7B,GAAKzB,GAAKhG,GACvBP,EAASmK,oBAAsBnK,EAASmK,kBAAmB5J,GAAUwB,SAIzE1C,EAAQ6K,SACZ5K,EAAK8K,OAAa,GAAI,SAAU7D,GAC/B,IAAI8D,EAAS9D,EAAGS,QAASpD,GAAWC,IACpC,OAAO,SAAUhC,GAChB,OAAOA,EAAKkF,aAAc,QAAWsD,IAGvC/K,EAAKgL,KAAW,GAAI,SAAU/D,EAAIZ,GACjC,QAAuC,IAA3BA,EAAQW,gBAAkCpG,EAAiB,CACtE,IAAI2B,EAAO8D,EAAQW,eAAgBC,GACnC,OAAO1E,GAASA,UAIlBvC,EAAK8K,OAAa,GAAK,SAAU7D,GAChC,IAAI8D,EAAS9D,EAAGS,QAASpD,GAAWC,IACpC,OAAO,SAAUhC,GAChB,IAAIwH,OAAwC,IAA1BxH,EAAK0I,kBACtB1I,EAAK0I,iBAAkB,MACxB,OAAOlB,GAAQA,EAAK3B,QAAU2C,IAMhC/K,EAAKgL,KAAW,GAAI,SAAU/D,EAAIZ,GACjC,QAAuC,IAA3BA,EAAQW,gBAAkCpG,EAAiB,CACtE,IAAImJ,EAAMjK,EAAGoL,EACZ3I,EAAO8D,EAAQW,eAAgBC,GAEhC,GAAK1E,EAAO,CAIX,IADAwH,EAAOxH,EAAK0I,iBAAkB,QACjBlB,EAAK3B,QAAUnB,EAC3B,OAAS1E,GAIV2I,EAAQ7E,EAAQwE,kBAAmB5D,GACnCnH,EAAI,EACJ,MAAUyC,EAAO2I,EAAOpL,KAEvB,IADAiK,EAAOxH,EAAK0I,iBAAkB,QACjBlB,EAAK3B,QAAUnB,EAC3B,OAAS1E,GAKZ,YAMHvC,EAAKgL,KAAY,IAAIjL,EAAQmH,qBAC5B,SAAUiE,EAAK9E,GACd,YAA6C,IAAjCA,EAAQa,qBACZb,EAAQa,qBAAsBiE,GAG1BpL,EAAQqH,IACZf,EAAQyB,iBAAkBqD,QAD3B,GAKR,SAAUA,EAAK9E,GACd,IAAI9D,EACH6I,KACAtL,EAAI,EAGJwG,EAAUD,EAAQa,qBAAsBiE,GAGzC,GAAa,MAARA,EAAc,CAClB,MAAU5I,EAAO+D,EAASxG,KACF,IAAlByC,EAAKuD,UACTsF,EAAIjJ,KAAMI,GAIZ,OAAO6I,EAER,OAAO9E,GAITtG,EAAKgL,KAAc,MAAIjL,EAAQoH,wBAA0B,SAAUuD,EAAWrE,GAC7E,QAA+C,IAAnCA,EAAQc,wBAA0CvG,EAC7D,OAAOyF,EAAQc,uBAAwBuD,IAUzC5J,KAOAD,MAEOd,EAAQqH,IAAMjD,EAAQkD,KAAM3G,EAASoH,qBAI3CW,GAAQ,SAAUC,GAEjB,IAAI2C,EAOJ1K,EAAQ4J,YAAa7B,GAAK4C,UAAY,UAAYrK,EAAU,qBAC1CA,EAAU,kEAOvByH,EAAGZ,iBAAkB,wBAAyBrF,QAClD5B,EAAUsB,KAAM,SAAWQ,EAAa,gBAKnC+F,EAAGZ,iBAAkB,cAAerF,QACzC5B,EAAUsB,KAAM,MAAQQ,EAAa,aAAeD,EAAW,KAI1DgG,EAAGZ,iBAAkB,QAAU7G,EAAU,MAAOwB,QACrD5B,EAAUsB,KAAM,OAQjBkJ,EAAQ3K,EAASiI,cAAe,UAC1BhB,aAAc,OAAQ,IAC5Be,EAAG6B,YAAac,GACV3C,EAAGZ,iBAAkB,aAAcrF,QACxC5B,EAAUsB,KAAM,MAAQQ,EAAa,QAAUA,EAAa,KAC3DA,EAAa,gBAMT+F,EAAGZ,iBAAkB,YAAarF,QACvC5B,EAAUsB,KAAM,YAMXuG,EAAGZ,iBAAkB,KAAO7G,EAAU,MAAOwB,QAClD5B,EAAUsB,KAAM,YAKjBuG,EAAGZ,iBAAkB,QACrBjH,EAAUsB,KAAM,iBAGjBsG,GAAQ,SAAUC,GACjBA,EAAG4C,UAAY,oFAKf,IAAID,EAAQ3K,EAASiI,cAAe,SACpC0C,EAAM1D,aAAc,OAAQ,UAC5Be,EAAG6B,YAAac,GAAQ1D,aAAc,OAAQ,KAIzCe,EAAGZ,iBAAkB,YAAarF,QACtC5B,EAAUsB,KAAM,OAASQ,EAAa,eAKW,IAA7C+F,EAAGZ,iBAAkB,YAAarF,QACtC5B,EAAUsB,KAAM,WAAY,aAK7BxB,EAAQ4J,YAAa7B,GAAKpD,UAAW,EACc,IAA9CoD,EAAGZ,iBAAkB,aAAcrF,QACvC5B,EAAUsB,KAAM,WAAY,aAK7BuG,EAAGZ,iBAAkB,QACrBjH,EAAUsB,KAAM,YAIXpC,EAAQwL,gBAAkBpH,EAAQkD,KAAQtG,EAAUJ,EAAQI,SAClEJ,EAAQ6K,uBACR7K,EAAQ8K,oBACR9K,EAAQ+K,kBACR/K,EAAQgL,qBAERlD,GAAQ,SAAUC,GAIjB3I,EAAQ6L,kBAAoB7K,EAAQ6E,KAAM8C,EAAI,KAI9C3H,EAAQ6E,KAAM8C,EAAI,aAClB5H,EAAcqB,KAAM,KAAMW,KAItB/C,EAAQyK,QAQb3J,EAAUsB,KAAM,QAGjBtB,EAAYA,EAAU4B,QAAU,IAAIO,OAAQnC,EAAUgH,KAAM,MAC5D/G,EAAgBA,EAAc2B,QAAU,IAAIO,OAAQlC,EAAc+G,KAAM,MAIxEmC,EAAa7F,EAAQkD,KAAM1G,EAAQkL,yBAKnC7K,EAAWgJ,GAAc7F,EAAQkD,KAAM1G,EAAQK,UAC9C,SAAUY,EAAGC,GAQZ,IAAIiK,EAAuB,IAAflK,EAAEkE,UAAkBlE,EAAEkI,iBAAmBlI,EACpDmK,EAAMlK,GAAKA,EAAE0F,WACd,OAAO3F,IAAMmK,MAAWA,GAAwB,IAAjBA,EAAIjG,YAClCgG,EAAM9K,SACL8K,EAAM9K,SAAU+K,GAChBnK,EAAEiK,yBAA8D,GAAnCjK,EAAEiK,wBAAyBE,MAG3D,SAAUnK,EAAGC,GACZ,GAAKA,EACJ,MAAUA,EAAIA,EAAE0F,WACf,GAAK1F,IAAMD,EACV,OAAO,EAIV,OAAO,GAOTD,EAAYqI,EACZ,SAAUpI,EAAGC,GAGZ,GAAKD,IAAMC,EAEV,OADArB,GAAe,EACR,EAIR,IAAIwL,GAAWpK,EAAEiK,yBAA2BhK,EAAEgK,wBAC9C,OAAKG,IAgBU,GAPfA,GAAYpK,EAAEkF,eAAiBlF,KAASC,EAAEiF,eAAiBjF,GAC1DD,EAAEiK,wBAAyBhK,GAG3B,KAIG9B,EAAQkM,cAAgBpK,EAAEgK,wBAAyBjK,KAAQoK,EAOzDpK,GAAKlB,GAAYkB,EAAEkF,eAAiB3F,GACxCH,EAAUG,EAAcS,IAChB,EAOJC,GAAKnB,GAAYmB,EAAEiF,eAAiB3F,GACxCH,EAAUG,EAAcU,GACjB,EAIDtB,EACJ8B,EAAS9B,EAAWqB,GAAMS,EAAS9B,EAAWsB,GAChD,EAGe,EAAVmK,GAAe,EAAI,IAE3B,SAAUpK,EAAGC,GAGZ,GAAKD,IAAMC,EAEV,OADArB,GAAe,EACR,EAGR,IAAI2I,EACHrJ,EAAI,EACJoM,EAAMtK,EAAE2F,WACRwE,EAAMlK,EAAE0F,WACR4E,GAAOvK,GACPwK,GAAOvK,GAGR,IAAMqK,IAAQH,EAMb,OAAOnK,GAAKlB,GAAY,EACvBmB,GAAKnB,EAAW,EAEhBwL,GAAO,EACPH,EAAM,EACNxL,EACE8B,EAAS9B,EAAWqB,GAAMS,EAAS9B,EAAWsB,GAChD,EAGK,GAAKqK,IAAQH,EACnB,OAAO7C,GAActH,EAAGC,GAIzBsH,EAAMvH,EACN,MAAUuH,EAAMA,EAAI5B,WACnB4E,EAAGE,QAASlD,GAEbA,EAAMtH,EACN,MAAUsH,EAAMA,EAAI5B,WACnB6E,EAAGC,QAASlD,GAIb,MAAQgD,EAAIrM,KAAQsM,EAAItM,GACvBA,IAGD,OAAOA,EAGNoJ,GAAciD,EAAIrM,GAAKsM,EAAItM,IAO3BqM,EAAIrM,IAAOqB,GAAgB,EAC3BiL,EAAItM,IAAOqB,EAAe,EAE1B,GAGKT,GAnfCA,GAsfTyF,GAAOpF,QAAU,SAAUuL,EAAMC,GAChC,OAAOpG,GAAQmG,EAAM,KAAM,KAAMC,IAGlCpG,GAAOoF,gBAAkB,SAAUhJ,EAAM+J,GAGxC,GAFA7L,EAAa8B,GAERxC,EAAQwL,iBAAmB3K,IAC9Bc,EAAwB4K,EAAO,QAC7BxL,IAAkBA,EAAcuG,KAAMiF,OACtCzL,IAAkBA,EAAUwG,KAAMiF,IAErC,IACC,IAAIE,EAAMzL,EAAQ6E,KAAMrD,EAAM+J,GAG9B,GAAKE,GAAOzM,EAAQ6L,mBAInBrJ,EAAK7B,UAAuC,KAA3B6B,EAAK7B,SAASoF,SAC/B,OAAO0G,EAEP,MAAQzG,GACTrE,EAAwB4K,GAAM,GAIhC,OAAOnG,GAAQmG,EAAM5L,EAAU,MAAQ6B,IAASE,OAAS,GAG1D0D,GAAOnF,SAAW,SAAUqF,EAAS9D,GAUpC,OAHO8D,EAAQS,eAAiBT,IAAa3F,GAC5CD,EAAa4F,GAEPrF,EAAUqF,EAAS9D,IAG3B4D,GAAOsG,KAAO,SAAUlK,EAAMmK,IAOtBnK,EAAKuE,eAAiBvE,IAAU7B,GACtCD,EAAa8B,GAGd,IAAIiG,EAAKxI,EAAKiJ,WAAYyD,EAAKlH,eAG9BmH,EAAMnE,GAAM1G,EAAO8D,KAAM5F,EAAKiJ,WAAYyD,EAAKlH,eAC9CgD,EAAIjG,EAAMmK,GAAO9L,QACjBgM,EAEF,YAAeA,IAARD,EACNA,EACA5M,EAAQ8C,aAAejC,EACtB2B,EAAKkF,aAAciF,IACjBC,EAAMpK,EAAK0I,iBAAkByB,KAAYC,EAAIE,UAC9CF,EAAIvE,MACJ,MAGJjC,GAAO3B,OAAS,SAAUsI,GACzB,OAASA,EAAM,IAAKpF,QAAS7C,GAAYC,KAG1CqB,GAAO4G,MAAQ,SAAUC,GACxB,MAAM,IAAIC,MAAO,0CAA4CD,IAO9D7G,GAAO+G,WAAa,SAAU5G,GAC7B,IAAI/D,EACH4K,KACAjH,EAAI,EACJpG,EAAI,EAOL,GAJAU,GAAgBT,EAAQqN,iBACxB7M,GAAaR,EAAQsN,YAAc/G,EAAQlE,MAAO,GAClDkE,EAAQgH,KAAM3L,GAETnB,EAAe,CACnB,MAAU+B,EAAO+D,EAASxG,KACpByC,IAAS+D,EAASxG,KACtBoG,EAAIiH,EAAWhL,KAAMrC,IAGvB,MAAQoG,IACPI,EAAQiH,OAAQJ,EAAYjH,GAAK,GAQnC,OAFA3F,EAAY,KAEL+F,GAORrG,EAAUkG,GAAOlG,QAAU,SAAUsC,GACpC,IAAIwH,EACHyC,EAAM,GACN1M,EAAI,EACJgG,EAAWvD,EAAKuD,SAEjB,GAAMA,GAQC,GAAkB,IAAbA,GAA+B,IAAbA,GAA+B,KAAbA,EAAkB,CAIjE,GAAiC,iBAArBvD,EAAKiL,YAChB,OAAOjL,EAAKiL,YAIZ,IAAMjL,EAAOA,EAAKkL,WAAYlL,EAAMA,EAAOA,EAAK+G,YAC/CkD,GAAOvM,EAASsC,QAGZ,GAAkB,IAAbuD,GAA+B,IAAbA,EAC7B,OAAOvD,EAAKmL,eAnBZ,MAAU3D,EAAOxH,EAAMzC,KAGtB0M,GAAOvM,EAAS8J,GAqBlB,OAAOyC,IAGRxM,EAAOmG,GAAOwH,WAGbtF,YAAa,GAEbuF,aAAcrF,GAEd7B,MAAOnD,EAEP0F,cAEA+B,QAEA6C,UACCC,KAAOrI,IAAK,aAAcsI,OAAO,GACjCC,KAAOvI,IAAK,cACZwI,KAAOxI,IAAK,kBAAmBsI,OAAO,GACtCG,KAAOzI,IAAK,oBAGb0I,WACCxK,KAAQ,SAAU+C,GAWjB,OAVAA,EAAO,GAAMA,EAAO,GAAIgB,QAASpD,GAAWC,IAG5CmC,EAAO,IAAQA,EAAO,IAAOA,EAAO,IACnCA,EAAO,IAAO,IAAKgB,QAASpD,GAAWC,IAEpB,OAAfmC,EAAO,KACXA,EAAO,GAAM,IAAMA,EAAO,GAAM,KAG1BA,EAAMtE,MAAO,EAAG,IAGxByB,MAAS,SAAU6C,GAiClB,OArBAA,EAAO,GAAMA,EAAO,GAAIlB,cAEU,QAA7BkB,EAAO,GAAItE,MAAO,EAAG,IAGnBsE,EAAO,IACZP,GAAO4G,MAAOrG,EAAO,IAKtBA,EAAO,KAASA,EAAO,GACtBA,EAAO,IAAQA,EAAO,IAAO,GAC7B,GAAqB,SAAfA,EAAO,IAAiC,QAAfA,EAAO,KACvCA,EAAO,KAAWA,EAAO,GAAMA,EAAO,IAAwB,QAAfA,EAAO,KAG3CA,EAAO,IAClBP,GAAO4G,MAAOrG,EAAO,IAGfA,GAGR9C,OAAU,SAAU8C,GACnB,IAAI0H,EACHC,GAAY3H,EAAO,IAAOA,EAAO,GAElC,OAAKnD,EAAmB,MAAE8D,KAAMX,EAAO,IAC/B,MAIHA,EAAO,GACXA,EAAO,GAAMA,EAAO,IAAOA,EAAO,IAAO,GAG9B2H,GAAYhL,EAAQgE,KAAMgH,KAGnCD,EAASjO,EAAUkO,GAAU,MAG7BD,EAASC,EAAShM,QAAS,IAAKgM,EAAS5L,OAAS2L,GAAWC,EAAS5L,UAGxEiE,EAAO,GAAMA,EAAO,GAAItE,MAAO,EAAGgM,GAClC1H,EAAO,GAAM2H,EAASjM,MAAO,EAAGgM,IAI1B1H,EAAMtE,MAAO,EAAG,MAIzB0I,QAECpH,IAAO,SAAU4K,GAChB,IAAI/I,EAAW+I,EAAiB5G,QAASpD,GAAWC,IAAYiB,cAChE,MAA4B,MAArB8I,EACN,WACC,OAAO,GAER,SAAU/L,GACT,OAAOA,EAAKgD,UAAYhD,EAAKgD,SAASC,gBAAkBD,IAI3D9B,MAAS,SAAUiH,GAClB,IAAI6D,EAAUjN,EAAYoJ,EAAY,KAEtC,OAAO6D,IACJA,EAAU,IAAIvL,OAAQ,MAAQL,EAC/B,IAAM+H,EAAY,IAAM/H,EAAa,SAAarB,EACjDoJ,EAAW,SAAUnI,GACpB,OAAOgM,EAAQlH,KACY,iBAAnB9E,EAAKmI,WAA0BnI,EAAKmI,gBACd,IAAtBnI,EAAKkF,cACXlF,EAAKkF,aAAc,UACpB,OAKN9D,KAAQ,SAAU+I,EAAM8B,EAAUC,GACjC,OAAO,SAAUlM,GAChB,IAAImM,EAASvI,GAAOsG,KAAMlK,EAAMmK,GAEhC,OAAe,MAAVgC,EACgB,OAAbF,GAEFA,IAINE,GAAU,GAIU,MAAbF,EAAmBE,IAAWD,EACvB,OAAbD,EAAoBE,IAAWD,EAClB,OAAbD,EAAoBC,GAAqC,IAA5BC,EAAOrM,QAASoM,GAChC,OAAbD,EAAoBC,GAASC,EAAOrM,QAASoM,IAAW,EAC3C,OAAbD,EAAoBC,GAASC,EAAOtM,OAAQqM,EAAMhM,UAAagM,EAClD,OAAbD,GAAsB,IAAME,EAAOhH,QAAS3E,EAAa,KAAQ,KAAMV,QAASoM,IAAW,EAC9E,OAAbD,IAAoBE,IAAWD,GAASC,EAAOtM,MAAO,EAAGqM,EAAMhM,OAAS,KAAQgM,EAAQ,QAO3F5K,MAAS,SAAU8K,EAAMC,EAAMC,EAAWd,EAAOe,GAChD,IAAIC,EAAgC,QAAvBJ,EAAKvM,MAAO,EAAG,GAC3B4M,EAA+B,SAArBL,EAAKvM,OAAQ,GACvB6M,EAAkB,YAATL,EAEV,OAAiB,IAAVb,GAAwB,IAATe,EAGrB,SAAUvM,GACT,QAASA,EAAKgF,YAGf,SAAUhF,EAAM2M,EAAUC,GACzB,IAAIjH,EAAOkH,EAAaC,EAAYtF,EAAMuF,EAAWC,EACpD9J,EAAMsJ,IAAWC,EAAU,cAAgB,kBAC3CQ,EAASjN,EAAKgF,WACdmF,EAAOuC,GAAU1M,EAAKgD,SAASC,cAC/BiK,GAAYN,IAAQF,EACpB7F,GAAO,EAER,GAAKoG,EAAS,CAGb,GAAKT,EAAS,CACb,MAAQtJ,EAAM,CACbsE,EAAOxH,EACP,MAAUwH,EAAOA,EAAMtE,GACtB,GAAKwJ,EACJlF,EAAKxE,SAASC,gBAAkBkH,EACd,IAAlB3C,EAAKjE,SAEL,OAAO,EAKTyJ,EAAQ9J,EAAe,SAATkJ,IAAoBY,GAAS,cAE5C,OAAO,EAMR,GAHAA,GAAUP,EAAUQ,EAAO/B,WAAa+B,EAAOE,WAG1CV,GAAWS,EAAW,CAe1BrG,GADAkG,GADApH,GAHAkH,GAJAC,GADAtF,EAAOyF,GACYvO,KAAe8I,EAAM9I,QAId8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEChB,QACF,KAAQvN,GAAW8G,EAAO,KACzBA,EAAO,GAC3B6B,EAAOuF,GAAaE,EAAO3J,WAAYyJ,GAEvC,MAAUvF,IAASuF,GAAavF,GAAQA,EAAMtE,KAG3C2D,EAAOkG,EAAY,IAAOC,EAAMtN,MAGlC,GAAuB,IAAlB8H,EAAKjE,YAAoBsD,GAAQW,IAASxH,EAAO,CACrD6M,EAAaT,IAAWvN,EAASkO,EAAWlG,GAC5C,YAyBF,GAlBKqG,IAaJrG,EADAkG,GADApH,GAHAkH,GAJAC,GADAtF,EAAOxH,GACYtB,KAAe8I,EAAM9I,QAId8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEChB,QACF,KAAQvN,GAAW8G,EAAO,KAMhC,IAATkB,EAGJ,MAAUW,IAASuF,GAAavF,GAAQA,EAAMtE,KAC3C2D,EAAOkG,EAAY,IAAOC,EAAMtN,MAElC,IAAOgN,EACNlF,EAAKxE,SAASC,gBAAkBkH,EACd,IAAlB3C,EAAKjE,aACHsD,IAGGqG,KAMJL,GALAC,EAAatF,EAAM9I,KAChB8I,EAAM9I,QAIiB8I,EAAK4F,YAC5BN,EAAYtF,EAAK4F,eAEPhB,IAAWvN,EAASgI,IAG7BW,IAASxH,GACb,MASL,OADA6G,GAAQ0F,KACQf,GAAW3E,EAAO2E,GAAU,GAAK3E,EAAO2E,GAAS,KAKrEnK,OAAU,SAAUgM,EAAQlG,GAM3B,IAAImG,EACHrH,EAAKxI,EAAK8C,QAAS8M,IAAY5P,EAAK8P,WAAYF,EAAOpK,gBACtDW,GAAO4G,MAAO,uBAAyB6C,GAKzC,OAAKpH,EAAIvH,GACDuH,EAAIkB,GAIPlB,EAAG/F,OAAS,GAChBoN,GAASD,EAAQA,EAAQ,GAAIlG,GACtB1J,EAAK8P,WAAW/N,eAAgB6N,EAAOpK,eAC7C+C,GAAc,SAAUhC,EAAMxF,GAC7B,IAAIgP,EACHC,EAAUxH,EAAIjC,EAAMmD,GACpB5J,EAAIkQ,EAAQvN,OACb,MAAQ3C,IAEPyG,EADAwJ,EAAM1N,EAASkE,EAAMyJ,EAASlQ,OACbiB,EAASgP,GAAQC,EAASlQ,MAG7C,SAAUyC,GACT,OAAOiG,EAAIjG,EAAM,EAAGsN,KAIhBrH,IAIT1F,SAGCmN,IAAO1H,GAAc,SAAUnC,GAK9B,IAAIiF,KACH/E,KACA4J,EAAU9P,EAASgG,EAASsB,QAASzE,EAAO,OAE7C,OAAOiN,EAASjP,GACfsH,GAAc,SAAUhC,EAAMxF,EAASmO,EAAUC,GAChD,IAAI5M,EACH4N,EAAYD,EAAS3J,EAAM,KAAM4I,MACjCrP,EAAIyG,EAAK9D,OAGV,MAAQ3C,KACAyC,EAAO4N,EAAWrQ,MACxByG,EAAMzG,KAASiB,EAASjB,GAAMyC,MAIjC,SAAUA,EAAM2M,EAAUC,GAMzB,OALA9D,EAAO,GAAM9I,EACb2N,EAAS7E,EAAO,KAAM8D,EAAK7I,GAG3B+E,EAAO,GAAM,MACL/E,EAAQrE,SAInBmO,IAAO7H,GAAc,SAAUnC,GAC9B,OAAO,SAAU7D,GAChB,OAAO4D,GAAQC,EAAU7D,GAAOE,OAAS,KAI3CzB,SAAYuH,GAAc,SAAU8H,GAEnC,OADAA,EAAOA,EAAK3I,QAASpD,GAAWC,IACzB,SAAUhC,GAChB,OAASA,EAAKiL,aAAevN,EAASsC,IAASF,QAASgO,IAAU,KAWpEC,KAAQ/H,GAAc,SAAU+H,GAO/B,OAJMhN,EAAY+D,KAAMiJ,GAAQ,KAC/BnK,GAAO4G,MAAO,qBAAuBuD,GAEtCA,EAAOA,EAAK5I,QAASpD,GAAWC,IAAYiB,cACrC,SAAUjD,GAChB,IAAIgO,EACJ,GACC,GAAOA,EAAW3P,EACjB2B,EAAK+N,KACL/N,EAAKkF,aAAc,aAAgBlF,EAAKkF,aAAc,QAGtD,OADA8I,EAAWA,EAAS/K,iBACA8K,GAA2C,IAAnCC,EAASlO,QAASiO,EAAO,YAE3C/N,EAAOA,EAAKgF,aAAkC,IAAlBhF,EAAKuD,UAC7C,OAAO,KAKTE,OAAU,SAAUzD,GACnB,IAAIiO,EAAO3Q,EAAO4Q,UAAY5Q,EAAO4Q,SAASD,KAC9C,OAAOA,GAAQA,EAAKpO,MAAO,KAAQG,EAAK0E,IAGzCyJ,KAAQ,SAAUnO,GACjB,OAAOA,IAAS5B,GAGjBgQ,MAAS,SAAUpO,GAClB,OAAOA,IAAS7B,EAASkQ,iBACrBlQ,EAASmQ,UAAYnQ,EAASmQ,gBAC7BtO,EAAKoM,MAAQpM,EAAKuO,OAASvO,EAAKwO,WAItCC,QAAWzH,IAAsB,GACjCjE,SAAYiE,IAAsB,GAElC0H,QAAW,SAAU1O,GAIpB,IAAIgD,EAAWhD,EAAKgD,SAASC,cAC7B,MAAsB,UAAbD,KAA0BhD,EAAK0O,SACxB,WAAb1L,KAA2BhD,EAAK2O,UAGpCA,SAAY,SAAU3O,GASrB,OALKA,EAAKgF,YAEThF,EAAKgF,WAAW4J,eAGQ,IAAlB5O,EAAK2O,UAIbE,MAAS,SAAU7O,GAMlB,IAAMA,EAAOA,EAAKkL,WAAYlL,EAAMA,EAAOA,EAAK+G,YAC/C,GAAK/G,EAAKuD,SAAW,EACpB,OAAO,EAGT,OAAO,GAGR0J,OAAU,SAAUjN,GACnB,OAAQvC,EAAK8C,QAAiB,MAAGP,IAIlC8O,OAAU,SAAU9O,GACnB,OAAO2B,EAAQmD,KAAM9E,EAAKgD,WAG3B8F,MAAS,SAAU9I,GAClB,OAAO0B,EAAQoD,KAAM9E,EAAKgD,WAG3B+L,OAAU,SAAU/O,GACnB,IAAImK,EAAOnK,EAAKgD,SAASC,cACzB,MAAgB,UAATkH,GAAkC,WAAdnK,EAAKoM,MAA8B,WAATjC,GAGtD2D,KAAQ,SAAU9N,GACjB,IAAIkK,EACJ,MAAuC,UAAhClK,EAAKgD,SAASC,eACN,SAAdjD,EAAKoM,OAIuC,OAAxClC,EAAOlK,EAAKkF,aAAc,UACN,SAAvBgF,EAAKjH,gBAIRuI,MAAStE,GAAwB,WAChC,OAAS,KAGVqF,KAAQrF,GAAwB,SAAU8H,EAAe9O,GACxD,OAASA,EAAS,KAGnB+O,GAAM/H,GAAwB,SAAU8H,EAAe9O,EAAQiH,GAC9D,OAASA,EAAW,EAAIA,EAAWjH,EAASiH,KAG7C+H,KAAQhI,GAAwB,SAAUE,EAAclH,GAEvD,IADA,IAAI3C,EAAI,EACAA,EAAI2C,EAAQ3C,GAAK,EACxB6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGR+H,IAAOjI,GAAwB,SAAUE,EAAclH,GAEtD,IADA,IAAI3C,EAAI,EACAA,EAAI2C,EAAQ3C,GAAK,EACxB6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGRgI,GAAMlI,GAAwB,SAAUE,EAAclH,EAAQiH,GAM7D,IALA,IAAI5J,EAAI4J,EAAW,EAClBA,EAAWjH,EACXiH,EAAWjH,EACVA,EACAiH,IACQ5J,GAAK,GACd6J,EAAaxH,KAAMrC,GAEpB,OAAO6J,IAGRiI,GAAMnI,GAAwB,SAAUE,EAAclH,EAAQiH,GAE7D,IADA,IAAI5J,EAAI4J,EAAW,EAAIA,EAAWjH,EAASiH,IACjC5J,EAAI2C,GACbkH,EAAaxH,KAAMrC,GAEpB,OAAO6J,OAKL7G,QAAe,IAAI9C,EAAK8C,QAAc,GAG3C,IAAMhD,KAAO+R,OAAO,EAAMC,UAAU,EAAMC,MAAM,EAAMC,UAAU,EAAMC,OAAO,GAC5EjS,EAAK8C,QAAShD,GA7zCf,SAA4B6O,GAC3B,OAAO,SAAUpM,GAEhB,MAAgB,UADLA,EAAKgD,SAASC,eACEjD,EAAKoM,OAASA,GA0zCtBuD,CAAmBpS,GAExC,IAAMA,KAAOqS,QAAQ,EAAMC,OAAO,GACjCpS,EAAK8C,QAAShD,GArzCf,SAA6B6O,GAC5B,OAAO,SAAUpM,GAChB,IAAImK,EAAOnK,EAAKgD,SAASC,cACzB,OAAkB,UAATkH,GAA6B,WAATA,IAAuBnK,EAAKoM,OAASA,GAkzC/C0D,CAAoBvS,GAIzC,SAASgQ,MACTA,GAAWwC,UAAYtS,EAAKuS,QAAUvS,EAAK8C,QAC3C9C,EAAK8P,WAAa,IAAIA,GAEtB3P,EAAWgG,GAAOhG,SAAW,SAAUiG,EAAUoM,GAChD,IAAIxC,EAAStJ,EAAO+L,EAAQ9D,EAC3B+D,EAAO/L,EAAQgM,EACfC,EAASpR,EAAY4E,EAAW,KAEjC,GAAKwM,EACJ,OAAOJ,EAAY,EAAII,EAAOxQ,MAAO,GAGtCsQ,EAAQtM,EACRO,KACAgM,EAAa3S,EAAKmO,UAElB,MAAQuE,EAAQ,CAGT1C,KAAatJ,EAAQxD,EAAO6D,KAAM2L,MAClChM,IAGJgM,EAAQA,EAAMtQ,MAAOsE,EAAO,GAAIjE,SAAYiQ,GAE7C/L,EAAOxE,KAAQsQ,OAGhBzC,GAAU,GAGHtJ,EAAQvD,EAAmB4D,KAAM2L,MACvC1C,EAAUtJ,EAAM4B,QAChBmK,EAAOtQ,MACNiG,MAAO4H,EAGPrB,KAAMjI,EAAO,GAAIgB,QAASzE,EAAO,OAElCyP,EAAQA,EAAMtQ,MAAO4N,EAAQvN,SAI9B,IAAMkM,KAAQ3O,EAAK8K,SACXpE,EAAQnD,EAAWoL,GAAO5H,KAAM2L,KAAgBC,EAAYhE,MAChEjI,EAAQiM,EAAYhE,GAAQjI,MAC9BsJ,EAAUtJ,EAAM4B,QAChBmK,EAAOtQ,MACNiG,MAAO4H,EACPrB,KAAMA,EACN5N,QAAS2F,IAEVgM,EAAQA,EAAMtQ,MAAO4N,EAAQvN,SAI/B,IAAMuN,EACL,MAOF,OAAOwC,EACNE,EAAMjQ,OACNiQ,EACCvM,GAAO4G,MAAO3G,GAGd5E,EAAY4E,EAAUO,GAASvE,MAAO,IAGzC,SAASwF,GAAY6K,GAIpB,IAHA,IAAI3S,EAAI,EACP0C,EAAMiQ,EAAOhQ,OACb2D,EAAW,GACJtG,EAAI0C,EAAK1C,IAChBsG,GAAYqM,EAAQ3S,GAAIsI,MAEzB,OAAOhC,EAGR,SAASf,GAAe6K,EAAS2C,EAAYC,GAC5C,IAAIrN,EAAMoN,EAAWpN,IACpBsN,EAAOF,EAAWnN,KAClByC,EAAM4K,GAAQtN,EACduN,EAAmBF,GAAgB,eAAR3K,EAC3B8K,EAAW5R,IAEZ,OAAOwR,EAAW9E,MAGjB,SAAUxL,EAAM8D,EAAS8I,GACxB,MAAU5M,EAAOA,EAAMkD,GACtB,GAAuB,IAAlBlD,EAAKuD,UAAkBkN,EAC3B,OAAO9C,EAAS3N,EAAM8D,EAAS8I,GAGjC,OAAO,GAIR,SAAU5M,EAAM8D,EAAS8I,GACxB,IAAI+D,EAAU9D,EAAaC,EAC1B8D,GAAa/R,EAAS6R,GAGvB,GAAK9D,GACJ,MAAU5M,EAAOA,EAAMkD,GACtB,IAAuB,IAAlBlD,EAAKuD,UAAkBkN,IACtB9C,EAAS3N,EAAM8D,EAAS8I,GAC5B,OAAO,OAKV,MAAU5M,EAAOA,EAAMkD,GACtB,GAAuB,IAAlBlD,EAAKuD,UAAkBkN,EAQ3B,GAPA3D,EAAa9M,EAAMtB,KAAesB,EAAMtB,OAIxCmO,EAAcC,EAAY9M,EAAKoN,YAC5BN,EAAY9M,EAAKoN,cAEfoD,GAAQA,IAASxQ,EAAKgD,SAASC,cACnCjD,EAAOA,EAAMkD,IAASlD,MAChB,CAAA,IAAO2Q,EAAW9D,EAAajH,KACrC+K,EAAU,KAAQ9R,GAAW8R,EAAU,KAAQD,EAG/C,OAASE,EAAU,GAAMD,EAAU,GAOnC,GAHA9D,EAAajH,GAAQgL,EAGdA,EAAU,GAAMjD,EAAS3N,EAAM8D,EAAS8I,GAC9C,OAAO,EAMZ,OAAO,GAIV,SAASiE,GAAgBC,GACxB,OAAOA,EAAS5Q,OAAS,EACxB,SAAUF,EAAM8D,EAAS8I,GACxB,IAAIrP,EAAIuT,EAAS5Q,OACjB,MAAQ3C,IACP,IAAMuT,EAAUvT,GAAKyC,EAAM8D,EAAS8I,GACnC,OAAO,EAGT,OAAO,GAERkE,EAAU,GAGZ,SAASC,GAAkBlN,EAAUmN,EAAUjN,GAG9C,IAFA,IAAIxG,EAAI,EACP0C,EAAM+Q,EAAS9Q,OACR3C,EAAI0C,EAAK1C,IAChBqG,GAAQC,EAAUmN,EAAUzT,GAAKwG,GAElC,OAAOA,EAGR,SAASkN,GAAUrD,EAAWsD,EAAK3I,EAAQzE,EAAS8I,GAOnD,IANA,IAAI5M,EACHmR,KACA5T,EAAI,EACJ0C,EAAM2N,EAAU1N,OAChBkR,EAAgB,MAAPF,EAEF3T,EAAI0C,EAAK1C,KACTyC,EAAO4N,EAAWrQ,MAClBgL,IAAUA,EAAQvI,EAAM8D,EAAS8I,KACtCuE,EAAavR,KAAMI,GACdoR,GACJF,EAAItR,KAAMrC,KAMd,OAAO4T,EAGR,SAASE,GAAYzF,EAAW/H,EAAU8J,EAAS2D,EAAYC,EAAYC,GAO1E,OANKF,IAAeA,EAAY5S,KAC/B4S,EAAaD,GAAYC,IAErBC,IAAeA,EAAY7S,KAC/B6S,EAAaF,GAAYE,EAAYC,IAE/BxL,GAAc,SAAUhC,EAAMD,EAASD,EAAS8I,GACtD,IAAI6E,EAAMlU,EAAGyC,EACZ0R,KACAC,KACAC,EAAc7N,EAAQ7D,OAGtByI,EAAQ3E,GAAQ+M,GACflN,GAAY,IACZC,EAAQP,UAAaO,GAAYA,MAKlC+N,GAAYjG,IAAe5H,GAASH,EAEnC8E,EADAsI,GAAUtI,EAAO+I,EAAQ9F,EAAW9H,EAAS8I,GAG9CkF,EAAanE,EAGZ4D,IAAgBvN,EAAO4H,EAAYgG,GAAeN,MAMjDvN,EACD8N,EAQF,GALKlE,GACJA,EAASkE,EAAWC,EAAYhO,EAAS8I,GAIrC0E,EAAa,CACjBG,EAAOR,GAAUa,EAAYH,GAC7BL,EAAYG,KAAU3N,EAAS8I,GAG/BrP,EAAIkU,EAAKvR,OACT,MAAQ3C,KACAyC,EAAOyR,EAAMlU,MACnBuU,EAAYH,EAASpU,MAAWsU,EAAWF,EAASpU,IAAQyC,IAK/D,GAAKgE,GACJ,GAAKuN,GAAc3F,EAAY,CAC9B,GAAK2F,EAAa,CAGjBE,KACAlU,EAAIuU,EAAW5R,OACf,MAAQ3C,KACAyC,EAAO8R,EAAYvU,KAGzBkU,EAAK7R,KAAQiS,EAAWtU,GAAMyC,GAGhCuR,EAAY,KAAQO,KAAmBL,EAAM7E,GAI9CrP,EAAIuU,EAAW5R,OACf,MAAQ3C,KACAyC,EAAO8R,EAAYvU,MACvBkU,EAAOF,EAAazR,EAASkE,EAAMhE,GAAS0R,EAAQnU,KAAS,IAE/DyG,EAAMyN,KAAY1N,EAAS0N,GAASzR,UAOvC8R,EAAab,GACZa,IAAe/N,EACd+N,EAAW9G,OAAQ4G,EAAaE,EAAW5R,QAC3C4R,GAEGP,EACJA,EAAY,KAAMxN,EAAS+N,EAAYlF,GAEvChN,EAAKwD,MAAOW,EAAS+N,KAMzB,SAASC,GAAmB7B,GAyB3B,IAxBA,IAAI8B,EAAcrE,EAAShK,EAC1B1D,EAAMiQ,EAAOhQ,OACb+R,EAAkBxU,EAAK6N,SAAU4E,EAAQ,GAAI9D,MAC7C8F,EAAmBD,GAAmBxU,EAAK6N,SAAU,KACrD/N,EAAI0U,EAAkB,EAAI,EAG1BE,EAAerP,GAAe,SAAU9C,GACvC,OAAOA,IAASgS,GACdE,GAAkB,GACrBE,EAAkBtP,GAAe,SAAU9C,GAC1C,OAAOF,EAASkS,EAAchS,IAAU,GACtCkS,GAAkB,GACrBpB,GAAa,SAAU9Q,EAAM8D,EAAS8I,GACrC,IAAI3C,GAASgI,IAAqBrF,GAAO9I,IAAY/F,MAClDiU,EAAelO,GAAUP,SAC1B4O,EAAcnS,EAAM8D,EAAS8I,GAC7BwF,EAAiBpS,EAAM8D,EAAS8I,IAIlC,OADAoF,EAAe,KACR/H,IAGD1M,EAAI0C,EAAK1C,IAChB,GAAOoQ,EAAUlQ,EAAK6N,SAAU4E,EAAQ3S,GAAI6O,MAC3C0E,GAAahO,GAAe+N,GAAgBC,GAAYnD,QAClD,CAIN,IAHAA,EAAUlQ,EAAK8K,OAAQ2H,EAAQ3S,GAAI6O,MAAOhJ,MAAO,KAAM8M,EAAQ3S,GAAIiB,UAGrDE,GAAY,CAIzB,IADAiF,IAAMpG,EACEoG,EAAI1D,EAAK0D,IAChB,GAAKlG,EAAK6N,SAAU4E,EAAQvM,GAAIyI,MAC/B,MAGF,OAAOiF,GACN9T,EAAI,GAAKsT,GAAgBC,GACzBvT,EAAI,GAAK8H,GAGT6K,EACErQ,MAAO,EAAGtC,EAAI,GACd8U,QAAUxM,MAAgC,MAAzBqK,EAAQ3S,EAAI,GAAI6O,KAAe,IAAM,MACtDjH,QAASzE,EAAO,MAClBiN,EACApQ,EAAIoG,GAAKoO,GAAmB7B,EAAOrQ,MAAOtC,EAAGoG,IAC7CA,EAAI1D,GAAO8R,GAAqB7B,EAASA,EAAOrQ,MAAO8D,IACvDA,EAAI1D,GAAOoF,GAAY6K,IAGzBY,EAASlR,KAAM+N,GAIjB,OAAOkD,GAAgBC,GAGxB,SAASwB,GAA0BC,EAAiBC,GACnD,IAAIC,EAAQD,EAAYtS,OAAS,EAChCwS,EAAYH,EAAgBrS,OAAS,EACrCyS,EAAe,SAAU3O,EAAMF,EAAS8I,EAAK7I,EAAS6O,GACrD,IAAI5S,EAAM2D,EAAGgK,EACZkF,EAAe,EACftV,EAAI,IACJqQ,EAAY5J,MACZ8O,KACAC,EAAgBhV,EAGhB4K,EAAQ3E,GAAQ0O,GAAajV,EAAKgL,KAAY,IAAG,IAAKmK,GAGtDI,EAAkBnU,GAA4B,MAAjBkU,EAAwB,EAAIE,KAAKC,UAAY,GAC1EjT,EAAM0I,EAAMzI,OAcb,IAZK0S,IAMJ7U,EAAmB+F,GAAW3F,GAAY2F,GAAW8O,GAM9CrV,IAAM0C,GAAgC,OAAvBD,EAAO2I,EAAOpL,IAAeA,IAAM,CACzD,GAAKmV,GAAa1S,EAAO,CACxB2D,EAAI,EAMEG,GAAW9D,EAAKuE,eAAiBpG,IACtCD,EAAa8B,GACb4M,GAAOvO,GAER,MAAUsP,EAAU4E,EAAiB5O,KACpC,GAAKgK,EAAS3N,EAAM8D,GAAW3F,EAAUyO,GAAQ,CAChD7I,EAAQnE,KAAMI,GACd,MAGG4S,IACJ/T,EAAUmU,GAKPP,KAGGzS,GAAQ2N,GAAW3N,IACzB6S,IAII7O,GACJ4J,EAAUhO,KAAMI,IAgBnB,GATA6S,GAAgBtV,EASXkV,GAASlV,IAAMsV,EAAe,CAClClP,EAAI,EACJ,MAAUgK,EAAU6E,EAAa7O,KAChCgK,EAASC,EAAWkF,EAAYhP,EAAS8I,GAG1C,GAAK5I,EAAO,CAGX,GAAK6O,EAAe,EACnB,MAAQtV,IACCqQ,EAAWrQ,IAAOuV,EAAYvV,KACrCuV,EAAYvV,GAAMmC,EAAI2D,KAAMU,IAM/B+O,EAAa7B,GAAU6B,GAIxBlT,EAAKwD,MAAOW,EAAS+O,GAGhBF,IAAc5O,GAAQ8O,EAAW5S,OAAS,GAC5C2S,EAAeL,EAAYtS,OAAW,GAExC0D,GAAO+G,WAAY5G,GAUrB,OALK6O,IACJ/T,EAAUmU,EACVjV,EAAmBgV,GAGbnF,GAGT,OAAO6E,EACNzM,GAAc2M,GACdA,EAGF9U,EAAU+F,GAAO/F,QAAU,SAAUgG,EAAUM,GAC9C,IAAI5G,EACHiV,KACAD,KACAlC,EAASnR,EAAe2E,EAAW,KAEpC,IAAMwM,EAAS,CAGRlM,IACLA,EAAQvG,EAAUiG,IAEnBtG,EAAI4G,EAAMjE,OACV,MAAQ3C,KACP8S,EAAS0B,GAAmB5N,EAAO5G,KACtBmB,GACZ8T,EAAY5S,KAAMyQ,GAElBkC,EAAgB3S,KAAMyQ,IAKxBA,EAASnR,EACR2E,EACAyO,GAA0BC,EAAiBC,KAIrC3O,SAAWA,EAEnB,OAAOwM,GAYRvS,EAAS8F,GAAO9F,OAAS,SAAU+F,EAAUC,EAASC,EAASC,GAC9D,IAAIzG,EAAG2S,EAAQiD,EAAO/G,EAAM3D,EAC3B2K,EAA+B,mBAAbvP,GAA2BA,EAC7CM,GAASH,GAAQpG,EAAYiG,EAAWuP,EAASvP,UAAYA,GAM9D,GAJAE,EAAUA,MAIY,IAAjBI,EAAMjE,OAAe,CAIzB,IADAgQ,EAAS/L,EAAO,GAAMA,EAAO,GAAItE,MAAO,IAC5BK,OAAS,GAAsC,QAA/BiT,EAAQjD,EAAQ,IAAM9D,MAC5B,IAArBtI,EAAQP,UAAkBlF,GAAkBZ,EAAK6N,SAAU4E,EAAQ,GAAI9D,MAAS,CAIhF,KAFAtI,GAAYrG,EAAKgL,KAAW,GAAG0K,EAAM3U,QAAS,GAC5C2G,QAASpD,GAAWC,IAAa8B,QAAmB,IAErD,OAAOC,EAGIqP,IACXtP,EAAUA,EAAQkB,YAGnBnB,EAAWA,EAAShE,MAAOqQ,EAAOnK,QAAQF,MAAM3F,QAIjD3C,EAAIyD,EAA0B,aAAE8D,KAAMjB,GAAa,EAAIqM,EAAOhQ,OAC9D,MAAQ3C,IAAM,CAIb,GAHA4V,EAAQjD,EAAQ3S,GAGXE,EAAK6N,SAAYc,EAAO+G,EAAM/G,MAClC,MAED,IAAO3D,EAAOhL,EAAKgL,KAAM2D,MAGjBpI,EAAOyE,EACb0K,EAAM3U,QAAS,GAAI2G,QAASpD,GAAWC,IACvCF,GAASgD,KAAMoL,EAAQ,GAAI9D,OAAUrH,GAAajB,EAAQkB,aACzDlB,IACI,CAKL,GAFAoM,EAAOlF,OAAQzN,EAAG,KAClBsG,EAAWG,EAAK9D,QAAUmF,GAAY6K,IAGrC,OADAtQ,EAAKwD,MAAOW,EAASC,GACdD,EAGR,QAeJ,OAPEqP,GAAYvV,EAASgG,EAAUM,IAChCH,EACAF,GACCzF,EACD0F,GACCD,GAAWhC,GAASgD,KAAMjB,IAAckB,GAAajB,EAAQkB,aAAgBlB,GAExEC,GAMRvG,EAAQsN,WAAapM,EAAQ+H,MAAO,IAAKsE,KAAM3L,GAAYkG,KAAM,MAAS5G,EAI1ElB,EAAQqN,mBAAqB5M,EAG7BC,IAIAV,EAAQkM,aAAexD,GAAQ,SAAUC,GAGxC,OAA4E,EAArEA,EAAGmD,wBAAyBnL,EAASiI,cAAe,eAMtDF,GAAQ,SAAUC,GAEvB,OADAA,EAAG4C,UAAY,mBACiC,MAAzC5C,EAAG+E,WAAWhG,aAAc,WAEnCoB,GAAW,yBAA0B,SAAUtG,EAAMmK,EAAMxM,GAC1D,IAAMA,EACL,OAAOqC,EAAKkF,aAAciF,EAA6B,SAAvBA,EAAKlH,cAA2B,EAAI,KAOjEzF,EAAQ8C,YAAe4F,GAAQ,SAAUC,GAG9C,OAFAA,EAAG4C,UAAY,WACf5C,EAAG+E,WAAW9F,aAAc,QAAS,IACY,KAA1Ce,EAAG+E,WAAWhG,aAAc,YAEnCoB,GAAW,QAAS,SAAUtG,EAAMqT,EAAO1V,GAC1C,IAAMA,GAAyC,UAAhCqC,EAAKgD,SAASC,cAC5B,OAAOjD,EAAKsT,eAOTpN,GAAQ,SAAUC,GACvB,OAAwC,MAAjCA,EAAGjB,aAAc,eAExBoB,GAAWnG,EAAU,SAAUH,EAAMmK,EAAMxM,GAC1C,IAAIyM,EACJ,IAAMzM,EACL,OAAwB,IAAjBqC,EAAMmK,GAAkBA,EAAKlH,eACjCmH,EAAMpK,EAAK0I,iBAAkByB,KAAYC,EAAIE,UAC9CF,EAAIvE,MACJ,OAML,IAAI0N,GAAUjW,EAAOsG,OAErBA,GAAO4P,WAAa,WAKnB,OAJKlW,EAAOsG,SAAWA,KACtBtG,EAAOsG,OAAS2P,IAGV3P,IAGe,mBAAX6P,QAAyBA,OAAOC,IAC3CD,OAAQ,WACP,OAAO7P,KAIqB,oBAAX+P,QAA0BA,OAAOC,QACnDD,OAAOC,QAAUhQ,GAEjBtG,EAAOsG,OAASA,GAl8EjB,CAu8EKtG","file":"sizzle.min.js"} \ No newline at end of file
diff --git a/node_modules/sizzle/package.json b/node_modules/sizzle/package.json
new file mode 100644
index 0000000..6e252ec
--- /dev/null
+++ b/node_modules/sizzle/package.json
@@ -0,0 +1,85 @@
+{
+ "title": "Sizzle",
+ "name": "sizzle",
+ "version": "2.3.10",
+ "description": "A pure-JavaScript, bottom-up CSS selector engine designed to be easily dropped in to a host library.",
+ "keywords": [
+ "sizzle",
+ "javascript",
+ "CSS",
+ "selector",
+ "jquery"
+ ],
+ "homepage": "https://sizzlejs.com",
+ "author": {
+ "name": "JS Foundation and other contributors",
+ "url": "https://github.com/jquery/sizzle/blob/2.3.10/AUTHORS.txt"
+ },
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/jquery/sizzle.git"
+ },
+ "bugs": {
+ "url": "https://github.com/jquery/sizzle/issues"
+ },
+ "license": "MIT",
+ "files": [
+ "AUTHORS.txt",
+ "LICENSE.txt",
+ "dist/sizzle.js",
+ "dist/sizzle.min.js",
+ "dist/sizzle.min.map"
+ ],
+ "main": "dist/sizzle.js",
+ "dependencies": {},
+ "devDependencies": {
+ "benchmark": "2.1.4",
+ "commitplease": "2.7.10",
+ "eslint-config-jquery": "2.0.0",
+ "grunt": "1.5.3",
+ "grunt-cli": "1.4.3",
+ "grunt-compare-size": "0.4.2",
+ "grunt-contrib-qunit": "2.0.0",
+ "grunt-contrib-uglify": "3.0.1",
+ "grunt-contrib-watch": "1.0.0",
+ "grunt-eslint": "21.0.0",
+ "grunt-git-authors": "3.2.0",
+ "grunt-jsonlint": "1.1.0",
+ "grunt-karma": "2.0.0",
+ "grunt-npmcopy": "0.1.0",
+ "gzip-js": "0.3.2",
+ "jquery": "1.9.1",
+ "karma": "1.3.0",
+ "karma-browserstack-launcher": "1.3.0",
+ "karma-chrome-launcher": "2.2.0",
+ "karma-firefox-launcher": "1.2.0",
+ "karma-html2js-preprocessor": "1.1.0",
+ "karma-phantomjs-launcher": "1.0.4",
+ "karma-qunit": "1.2.1",
+ "load-grunt-tasks": "3.5.2",
+ "phantomjs-prebuilt": "2.1.15",
+ "qunitjs": "1.23.1",
+ "requirejs": "2.3.5",
+ "requirejs-domready": "2.0.3",
+ "requirejs-text": "2.0.15"
+ },
+ "scripts": {
+ "build": "npm install && grunt",
+ "start": "grunt start",
+ "test": "grunt && grunt test"
+ },
+ "commitplease": {
+ "components": [
+ "Misc",
+ "Docs",
+ "Tests",
+ "Build",
+ "Release",
+ "Core",
+ "Tokenize",
+ "Compile",
+ "Selector",
+ "SetDocument"
+ ]
+ }
+}
diff --git a/node_modules/ws/LICENSE b/node_modules/ws/LICENSE
new file mode 100644
index 0000000..a145cd1
--- /dev/null
+++ b/node_modules/ws/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2011 Einar Otto Stangvik <einaros@gmail.com>
+
+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.
diff --git a/node_modules/ws/README.md b/node_modules/ws/README.md
new file mode 100644
index 0000000..20a6114
--- /dev/null
+++ b/node_modules/ws/README.md
@@ -0,0 +1,495 @@
+# ws: a Node.js WebSocket library
+
+[![Version npm](https://img.shields.io/npm/v/ws.svg?logo=npm)](https://www.npmjs.com/package/ws)
+[![CI](https://img.shields.io/github/workflow/status/websockets/ws/CI/master?label=CI&logo=github)](https://github.com/websockets/ws/actions?query=workflow%3ACI+branch%3Amaster)
+[![Coverage Status](https://img.shields.io/coveralls/websockets/ws/master.svg?logo=coveralls)](https://coveralls.io/github/websockets/ws)
+
+ws is a simple to use, blazing fast, and thoroughly tested WebSocket client and
+server implementation.
+
+Passes the quite extensive Autobahn test suite: [server][server-report],
+[client][client-report].
+
+**Note**: This module does not work in the browser. The client in the docs is a
+reference to a back end with the role of a client in the WebSocket
+communication. Browser clients must use the native
+[`WebSocket`](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket)
+object. To make the same code work seamlessly on Node.js and the browser, you
+can use one of the many wrappers available on npm, like
+[isomorphic-ws](https://github.com/heineiuo/isomorphic-ws).
+
+## Table of Contents
+
+- [Protocol support](#protocol-support)
+- [Installing](#installing)
+ - [Opt-in for performance](#opt-in-for-performance)
+- [API docs](#api-docs)
+- [WebSocket compression](#websocket-compression)
+- [Usage examples](#usage-examples)
+ - [Sending and receiving text data](#sending-and-receiving-text-data)
+ - [Sending binary data](#sending-binary-data)
+ - [Simple server](#simple-server)
+ - [External HTTP/S server](#external-https-server)
+ - [Multiple servers sharing a single HTTP/S server](#multiple-servers-sharing-a-single-https-server)
+ - [Client authentication](#client-authentication)
+ - [Server broadcast](#server-broadcast)
+ - [echo.websocket.org demo](#echowebsocketorg-demo)
+ - [Use the Node.js streams API](#use-the-nodejs-streams-api)
+ - [Other examples](#other-examples)
+- [FAQ](#faq)
+ - [How to get the IP address of the client?](#how-to-get-the-ip-address-of-the-client)
+ - [How to detect and close broken connections?](#how-to-detect-and-close-broken-connections)
+ - [How to connect via a proxy?](#how-to-connect-via-a-proxy)
+- [Changelog](#changelog)
+- [License](#license)
+
+## Protocol support
+
+- **HyBi drafts 07-12** (Use the option `protocolVersion: 8`)
+- **HyBi drafts 13-17** (Current default, alternatively option
+ `protocolVersion: 13`)
+
+## Installing
+
+```
+npm install ws
+```
+
+### Opt-in for performance
+
+There are 2 optional modules that can be installed along side with the ws
+module. These modules are binary addons which improve certain operations.
+Prebuilt binaries are available for the most popular platforms so you don't
+necessarily need to have a C++ compiler installed on your machine.
+
+- `npm install --save-optional bufferutil`: Allows to efficiently perform
+ operations such as masking and unmasking the data payload of the WebSocket
+ frames.
+- `npm install --save-optional utf-8-validate`: Allows to efficiently check if a
+ message contains valid UTF-8.
+
+## API docs
+
+See [`/doc/ws.md`](./doc/ws.md) for Node.js-like documentation of ws classes and
+utility functions.
+
+## WebSocket compression
+
+ws supports the [permessage-deflate extension][permessage-deflate] which enables
+the client and server to negotiate a compression algorithm and its parameters,
+and then selectively apply it to the data payloads of each WebSocket message.
+
+The extension is disabled by default on the server and enabled by default on the
+client. It adds a significant overhead in terms of performance and memory
+consumption so we suggest to enable it only if it is really needed.
+
+Note that Node.js has a variety of issues with high-performance compression,
+where increased concurrency, especially on Linux, can lead to [catastrophic
+memory fragmentation][node-zlib-bug] and slow performance. If you intend to use
+permessage-deflate in production, it is worthwhile to set up a test
+representative of your workload and ensure Node.js/zlib will handle it with
+acceptable performance and memory usage.
+
+Tuning of permessage-deflate can be done via the options defined below. You can
+also use `zlibDeflateOptions` and `zlibInflateOptions`, which is passed directly
+into the creation of [raw deflate/inflate streams][node-zlib-deflaterawdocs].
+
+See [the docs][ws-server-options] for more options.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({
+ port: 8080,
+ perMessageDeflate: {
+ zlibDeflateOptions: {
+ // See zlib defaults.
+ chunkSize: 1024,
+ memLevel: 7,
+ level: 3
+ },
+ zlibInflateOptions: {
+ chunkSize: 10 * 1024
+ },
+ // Other options settable:
+ clientNoContextTakeover: true, // Defaults to negotiated value.
+ serverNoContextTakeover: true, // Defaults to negotiated value.
+ serverMaxWindowBits: 10, // Defaults to negotiated value.
+ // Below options specified as default values.
+ concurrencyLimit: 10, // Limits zlib concurrency for perf.
+ threshold: 1024 // Size (in bytes) below which messages
+ // should not be compressed.
+ }
+});
+```
+
+The client will only use the extension if it is supported and enabled on the
+server. To always disable the extension on the client set the
+`perMessageDeflate` option to `false`.
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path', {
+ perMessageDeflate: false
+});
+```
+
+## Usage examples
+
+### Sending and receiving text data
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path');
+
+ws.on('open', function open() {
+ ws.send('something');
+});
+
+ws.on('message', function incoming(data) {
+ console.log(data);
+});
+```
+
+### Sending binary data
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('ws://www.host.com/path');
+
+ws.on('open', function open() {
+ const array = new Float32Array(5);
+
+ for (var i = 0; i < array.length; ++i) {
+ array[i] = i / 2;
+ }
+
+ ws.send(array);
+});
+```
+
+### Simple server
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(message) {
+ console.log('received: %s', message);
+ });
+
+ ws.send('something');
+});
+```
+
+### External HTTP/S server
+
+```js
+const fs = require('fs');
+const https = require('https');
+const WebSocket = require('ws');
+
+const server = https.createServer({
+ cert: fs.readFileSync('/path/to/cert.pem'),
+ key: fs.readFileSync('/path/to/key.pem')
+});
+const wss = new WebSocket.Server({ server });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(message) {
+ console.log('received: %s', message);
+ });
+
+ ws.send('something');
+});
+
+server.listen(8080);
+```
+
+### Multiple servers sharing a single HTTP/S server
+
+```js
+const http = require('http');
+const WebSocket = require('ws');
+const url = require('url');
+
+const server = http.createServer();
+const wss1 = new WebSocket.Server({ noServer: true });
+const wss2 = new WebSocket.Server({ noServer: true });
+
+wss1.on('connection', function connection(ws) {
+ // ...
+});
+
+wss2.on('connection', function connection(ws) {
+ // ...
+});
+
+server.on('upgrade', function upgrade(request, socket, head) {
+ const pathname = url.parse(request.url).pathname;
+
+ if (pathname === '/foo') {
+ wss1.handleUpgrade(request, socket, head, function done(ws) {
+ wss1.emit('connection', ws, request);
+ });
+ } else if (pathname === '/bar') {
+ wss2.handleUpgrade(request, socket, head, function done(ws) {
+ wss2.emit('connection', ws, request);
+ });
+ } else {
+ socket.destroy();
+ }
+});
+
+server.listen(8080);
+```
+
+### Client authentication
+
+```js
+const http = require('http');
+const WebSocket = require('ws');
+
+const server = http.createServer();
+const wss = new WebSocket.Server({ noServer: true });
+
+wss.on('connection', function connection(ws, request, client) {
+ ws.on('message', function message(msg) {
+ console.log(`Received message ${msg} from user ${client}`);
+ });
+});
+
+server.on('upgrade', function upgrade(request, socket, head) {
+ // This function is not defined on purpose. Implement it with your own logic.
+ authenticate(request, (err, client) => {
+ if (err || !client) {
+ socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
+ socket.destroy();
+ return;
+ }
+
+ wss.handleUpgrade(request, socket, head, function done(ws) {
+ wss.emit('connection', ws, request, client);
+ });
+ });
+});
+
+server.listen(8080);
+```
+
+Also see the provided [example][session-parse-example] using `express-session`.
+
+### Server broadcast
+
+A client WebSocket broadcasting to all connected WebSocket clients, including
+itself.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(data) {
+ wss.clients.forEach(function each(client) {
+ if (client.readyState === WebSocket.OPEN) {
+ client.send(data);
+ }
+ });
+ });
+});
+```
+
+A client WebSocket broadcasting to every other connected WebSocket clients,
+excluding itself.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.on('message', function incoming(data) {
+ wss.clients.forEach(function each(client) {
+ if (client !== ws && client.readyState === WebSocket.OPEN) {
+ client.send(data);
+ }
+ });
+ });
+});
+```
+
+### echo.websocket.org demo
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('wss://echo.websocket.org/', {
+ origin: 'https://websocket.org'
+});
+
+ws.on('open', function open() {
+ console.log('connected');
+ ws.send(Date.now());
+});
+
+ws.on('close', function close() {
+ console.log('disconnected');
+});
+
+ws.on('message', function incoming(data) {
+ console.log(`Roundtrip time: ${Date.now() - data} ms`);
+
+ setTimeout(function timeout() {
+ ws.send(Date.now());
+ }, 500);
+});
+```
+
+### Use the Node.js streams API
+
+```js
+const WebSocket = require('ws');
+
+const ws = new WebSocket('wss://echo.websocket.org/', {
+ origin: 'https://websocket.org'
+});
+
+const duplex = WebSocket.createWebSocketStream(ws, { encoding: 'utf8' });
+
+duplex.pipe(process.stdout);
+process.stdin.pipe(duplex);
+```
+
+### Other examples
+
+For a full example with a browser client communicating with a ws server, see the
+examples folder.
+
+Otherwise, see the test cases.
+
+## FAQ
+
+### How to get the IP address of the client?
+
+The remote IP address can be obtained from the raw socket.
+
+```js
+const WebSocket = require('ws');
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws, req) {
+ const ip = req.socket.remoteAddress;
+});
+```
+
+When the server runs behind a proxy like NGINX, the de-facto standard is to use
+the `X-Forwarded-For` header.
+
+```js
+wss.on('connection', function connection(ws, req) {
+ const ip = req.headers['x-forwarded-for'].split(',')[0].trim();
+});
+```
+
+### How to detect and close broken connections?
+
+Sometimes the link between the server and the client can be interrupted in a way
+that keeps both the server and the client unaware of the broken state of the
+connection (e.g. when pulling the cord).
+
+In these cases ping messages can be used as a means to verify that the remote
+endpoint is still responsive.
+
+```js
+const WebSocket = require('ws');
+
+function noop() {}
+
+function heartbeat() {
+ this.isAlive = true;
+}
+
+const wss = new WebSocket.Server({ port: 8080 });
+
+wss.on('connection', function connection(ws) {
+ ws.isAlive = true;
+ ws.on('pong', heartbeat);
+});
+
+const interval = setInterval(function ping() {
+ wss.clients.forEach(function each(ws) {
+ if (ws.isAlive === false) return ws.terminate();
+
+ ws.isAlive = false;
+ ws.ping(noop);
+ });
+}, 30000);
+
+wss.on('close', function close() {
+ clearInterval(interval);
+});
+```
+
+Pong messages are automatically sent in response to ping messages as required by
+the spec.
+
+Just like the server example above your clients might as well lose connection
+without knowing it. You might want to add a ping listener on your clients to
+prevent that. A simple implementation would be:
+
+```js
+const WebSocket = require('ws');
+
+function heartbeat() {
+ clearTimeout(this.pingTimeout);
+
+ // Use `WebSocket#terminate()`, which immediately destroys the connection,
+ // instead of `WebSocket#close()`, which waits for the close timer.
+ // Delay should be equal to the interval at which your server
+ // sends out pings plus a conservative assumption of the latency.
+ this.pingTimeout = setTimeout(() => {
+ this.terminate();
+ }, 30000 + 1000);
+}
+
+const client = new WebSocket('wss://echo.websocket.org/');
+
+client.on('open', heartbeat);
+client.on('ping', heartbeat);
+client.on('close', function clear() {
+ clearTimeout(this.pingTimeout);
+});
+```
+
+### How to connect via a proxy?
+
+Use a custom `http.Agent` implementation like [https-proxy-agent][] or
+[socks-proxy-agent][].
+
+## Changelog
+
+We're using the GitHub [releases][changelog] for changelog entries.
+
+## License
+
+[MIT](LICENSE)
+
+[changelog]: https://github.com/websockets/ws/releases
+[client-report]: http://websockets.github.io/ws/autobahn/clients/
+[https-proxy-agent]: https://github.com/TooTallNate/node-https-proxy-agent
+[node-zlib-bug]: https://github.com/nodejs/node/issues/8871
+[node-zlib-deflaterawdocs]:
+ https://nodejs.org/api/zlib.html#zlib_zlib_createdeflateraw_options
+[permessage-deflate]: https://tools.ietf.org/html/rfc7692
+[server-report]: http://websockets.github.io/ws/autobahn/servers/
+[session-parse-example]: ./examples/express-session-parse
+[socks-proxy-agent]: https://github.com/TooTallNate/node-socks-proxy-agent
+[ws-server-options]:
+ https://github.com/websockets/ws/blob/master/doc/ws.md#new-websocketserveroptions-callback
diff --git a/node_modules/ws/browser.js b/node_modules/ws/browser.js
new file mode 100644
index 0000000..ca4f628
--- /dev/null
+++ b/node_modules/ws/browser.js
@@ -0,0 +1,8 @@
+'use strict';
+
+module.exports = function () {
+ throw new Error(
+ 'ws does not work in the browser. Browser clients must use the native ' +
+ 'WebSocket object'
+ );
+};
diff --git a/node_modules/ws/index.js b/node_modules/ws/index.js
new file mode 100644
index 0000000..722c786
--- /dev/null
+++ b/node_modules/ws/index.js
@@ -0,0 +1,10 @@
+'use strict';
+
+const WebSocket = require('./lib/websocket');
+
+WebSocket.createWebSocketStream = require('./lib/stream');
+WebSocket.Server = require('./lib/websocket-server');
+WebSocket.Receiver = require('./lib/receiver');
+WebSocket.Sender = require('./lib/sender');
+
+module.exports = WebSocket;
diff --git a/node_modules/ws/lib/buffer-util.js b/node_modules/ws/lib/buffer-util.js
new file mode 100644
index 0000000..6fd84c3
--- /dev/null
+++ b/node_modules/ws/lib/buffer-util.js
@@ -0,0 +1,129 @@
+'use strict';
+
+const { EMPTY_BUFFER } = require('./constants');
+
+/**
+ * Merges an array of buffers into a new buffer.
+ *
+ * @param {Buffer[]} list The array of buffers to concat
+ * @param {Number} totalLength The total length of buffers in the list
+ * @return {Buffer} The resulting buffer
+ * @public
+ */
+function concat(list, totalLength) {
+ if (list.length === 0) return EMPTY_BUFFER;
+ if (list.length === 1) return list[0];
+
+ const target = Buffer.allocUnsafe(totalLength);
+ let offset = 0;
+
+ for (let i = 0; i < list.length; i++) {
+ const buf = list[i];
+ target.set(buf, offset);
+ offset += buf.length;
+ }
+
+ if (offset < totalLength) return target.slice(0, offset);
+
+ return target;
+}
+
+/**
+ * Masks a buffer using the given mask.
+ *
+ * @param {Buffer} source The buffer to mask
+ * @param {Buffer} mask The mask to use
+ * @param {Buffer} output The buffer where to store the result
+ * @param {Number} offset The offset at which to start writing
+ * @param {Number} length The number of bytes to mask.
+ * @public
+ */
+function _mask(source, mask, output, offset, length) {
+ for (let i = 0; i < length; i++) {
+ output[offset + i] = source[i] ^ mask[i & 3];
+ }
+}
+
+/**
+ * Unmasks a buffer using the given mask.
+ *
+ * @param {Buffer} buffer The buffer to unmask
+ * @param {Buffer} mask The mask to use
+ * @public
+ */
+function _unmask(buffer, mask) {
+ // Required until https://github.com/nodejs/node/issues/9006 is resolved.
+ const length = buffer.length;
+ for (let i = 0; i < length; i++) {
+ buffer[i] ^= mask[i & 3];
+ }
+}
+
+/**
+ * Converts a buffer to an `ArrayBuffer`.
+ *
+ * @param {Buffer} buf The buffer to convert
+ * @return {ArrayBuffer} Converted buffer
+ * @public
+ */
+function toArrayBuffer(buf) {
+ if (buf.byteLength === buf.buffer.byteLength) {
+ return buf.buffer;
+ }
+
+ return buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
+}
+
+/**
+ * Converts `data` to a `Buffer`.
+ *
+ * @param {*} data The data to convert
+ * @return {Buffer} The buffer
+ * @throws {TypeError}
+ * @public
+ */
+function toBuffer(data) {
+ toBuffer.readOnly = true;
+
+ if (Buffer.isBuffer(data)) return data;
+
+ let buf;
+
+ if (data instanceof ArrayBuffer) {
+ buf = Buffer.from(data);
+ } else if (ArrayBuffer.isView(data)) {
+ buf = Buffer.from(data.buffer, data.byteOffset, data.byteLength);
+ } else {
+ buf = Buffer.from(data);
+ toBuffer.readOnly = false;
+ }
+
+ return buf;
+}
+
+try {
+ const bufferUtil = require('bufferutil');
+ const bu = bufferUtil.BufferUtil || bufferUtil;
+
+ module.exports = {
+ concat,
+ mask(source, mask, output, offset, length) {
+ if (length < 48) _mask(source, mask, output, offset, length);
+ else bu.mask(source, mask, output, offset, length);
+ },
+ toArrayBuffer,
+ toBuffer,
+ unmask(buffer, mask) {
+ if (buffer.length < 32) _unmask(buffer, mask);
+ else bu.unmask(buffer, mask);
+ }
+ };
+} catch (e) /* istanbul ignore next */ {
+ module.exports = {
+ concat,
+ mask: _mask,
+ toArrayBuffer,
+ toBuffer,
+ unmask: _unmask
+ };
+}
diff --git a/node_modules/ws/lib/constants.js b/node_modules/ws/lib/constants.js
new file mode 100644
index 0000000..4082981
--- /dev/null
+++ b/node_modules/ws/lib/constants.js
@@ -0,0 +1,10 @@
+'use strict';
+
+module.exports = {
+ BINARY_TYPES: ['nodebuffer', 'arraybuffer', 'fragments'],
+ GUID: '258EAFA5-E914-47DA-95CA-C5AB0DC85B11',
+ kStatusCode: Symbol('status-code'),
+ kWebSocket: Symbol('websocket'),
+ EMPTY_BUFFER: Buffer.alloc(0),
+ NOOP: () => {}
+};
diff --git a/node_modules/ws/lib/event-target.js b/node_modules/ws/lib/event-target.js
new file mode 100644
index 0000000..a6fbe72
--- /dev/null
+++ b/node_modules/ws/lib/event-target.js
@@ -0,0 +1,184 @@
+'use strict';
+
+/**
+ * Class representing an event.
+ *
+ * @private
+ */
+class Event {
+ /**
+ * Create a new `Event`.
+ *
+ * @param {String} type The name of the event
+ * @param {Object} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(type, target) {
+ this.target = target;
+ this.type = type;
+ }
+}
+
+/**
+ * Class representing a message event.
+ *
+ * @extends Event
+ * @private
+ */
+class MessageEvent extends Event {
+ /**
+ * Create a new `MessageEvent`.
+ *
+ * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The received data
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(data, target) {
+ super('message', target);
+
+ this.data = data;
+ }
+}
+
+/**
+ * Class representing a close event.
+ *
+ * @extends Event
+ * @private
+ */
+class CloseEvent extends Event {
+ /**
+ * Create a new `CloseEvent`.
+ *
+ * @param {Number} code The status code explaining why the connection is being
+ * closed
+ * @param {String} reason A human-readable string explaining why the
+ * connection is closing
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(code, reason, target) {
+ super('close', target);
+
+ this.wasClean = target._closeFrameReceived && target._closeFrameSent;
+ this.reason = reason;
+ this.code = code;
+ }
+}
+
+/**
+ * Class representing an open event.
+ *
+ * @extends Event
+ * @private
+ */
+class OpenEvent extends Event {
+ /**
+ * Create a new `OpenEvent`.
+ *
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(target) {
+ super('open', target);
+ }
+}
+
+/**
+ * Class representing an error event.
+ *
+ * @extends Event
+ * @private
+ */
+class ErrorEvent extends Event {
+ /**
+ * Create a new `ErrorEvent`.
+ *
+ * @param {Object} error The error that generated this event
+ * @param {WebSocket} target A reference to the target to which the event was
+ * dispatched
+ */
+ constructor(error, target) {
+ super('error', target);
+
+ this.message = error.message;
+ this.error = error;
+ }
+}
+
+/**
+ * This provides methods for emulating the `EventTarget` interface. It's not
+ * meant to be used directly.
+ *
+ * @mixin
+ */
+const EventTarget = {
+ /**
+ * Register an event listener.
+ *
+ * @param {String} type A string representing the event type to listen for
+ * @param {Function} listener The listener to add
+ * @param {Object} [options] An options object specifies characteristics about
+ * the event listener
+ * @param {Boolean} [options.once=false] A `Boolean`` indicating that the
+ * listener should be invoked at most once after being added. If `true`,
+ * the listener would be automatically removed when invoked.
+ * @public
+ */
+ addEventListener(type, listener, options) {
+ if (typeof listener !== 'function') return;
+
+ function onMessage(data) {
+ listener.call(this, new MessageEvent(data, this));
+ }
+
+ function onClose(code, message) {
+ listener.call(this, new CloseEvent(code, message, this));
+ }
+
+ function onError(error) {
+ listener.call(this, new ErrorEvent(error, this));
+ }
+
+ function onOpen() {
+ listener.call(this, new OpenEvent(this));
+ }
+
+ const method = options && options.once ? 'once' : 'on';
+
+ if (type === 'message') {
+ onMessage._listener = listener;
+ this[method](type, onMessage);
+ } else if (type === 'close') {
+ onClose._listener = listener;
+ this[method](type, onClose);
+ } else if (type === 'error') {
+ onError._listener = listener;
+ this[method](type, onError);
+ } else if (type === 'open') {
+ onOpen._listener = listener;
+ this[method](type, onOpen);
+ } else {
+ this[method](type, listener);
+ }
+ },
+
+ /**
+ * Remove an event listener.
+ *
+ * @param {String} type A string representing the event type to remove
+ * @param {Function} listener The listener to remove
+ * @public
+ */
+ removeEventListener(type, listener) {
+ const listeners = this.listeners(type);
+
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i] === listener || listeners[i]._listener === listener) {
+ this.removeListener(type, listeners[i]);
+ }
+ }
+ }
+};
+
+module.exports = EventTarget;
diff --git a/node_modules/ws/lib/extension.js b/node_modules/ws/lib/extension.js
new file mode 100644
index 0000000..87a4213
--- /dev/null
+++ b/node_modules/ws/lib/extension.js
@@ -0,0 +1,223 @@
+'use strict';
+
+//
+// Allowed token characters:
+//
+// '!', '#', '$', '%', '&', ''', '*', '+', '-',
+// '.', 0-9, A-Z, '^', '_', '`', a-z, '|', '~'
+//
+// tokenChars[32] === 0 // ' '
+// tokenChars[33] === 1 // '!'
+// tokenChars[34] === 0 // '"'
+// ...
+//
+// prettier-ignore
+const tokenChars = [
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 0 - 15
+ 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // 16 - 31
+ 0, 1, 0, 1, 1, 1, 1, 1, 0, 0, 1, 1, 0, 1, 1, 0, // 32 - 47
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, // 48 - 63
+ 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 64 - 79
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 1, // 80 - 95
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, // 96 - 111
+ 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 0, 1, 0, 1, 0 // 112 - 127
+];
+
+/**
+ * Adds an offer to the map of extension offers or a parameter to the map of
+ * parameters.
+ *
+ * @param {Object} dest The map of extension offers or parameters
+ * @param {String} name The extension or parameter name
+ * @param {(Object|Boolean|String)} elem The extension parameters or the
+ * parameter value
+ * @private
+ */
+function push(dest, name, elem) {
+ if (dest[name] === undefined) dest[name] = [elem];
+ else dest[name].push(elem);
+}
+
+/**
+ * Parses the `Sec-WebSocket-Extensions` header into an object.
+ *
+ * @param {String} header The field value of the header
+ * @return {Object} The parsed object
+ * @public
+ */
+function parse(header) {
+ const offers = Object.create(null);
+
+ if (header === undefined || header === '') return offers;
+
+ let params = Object.create(null);
+ let mustUnescape = false;
+ let isEscaping = false;
+ let inQuotes = false;
+ let extensionName;
+ let paramName;
+ let start = -1;
+ let end = -1;
+ let i = 0;
+
+ for (; i < header.length; i++) {
+ const code = header.charCodeAt(i);
+
+ if (extensionName === undefined) {
+ if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x20 /* ' ' */ || code === 0x09 /* '\t' */) {
+ if (end === -1 && start !== -1) end = i;
+ } else if (code === 0x3b /* ';' */ || code === 0x2c /* ',' */) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ const name = header.slice(start, end);
+ if (code === 0x2c) {
+ push(offers, name, params);
+ params = Object.create(null);
+ } else {
+ extensionName = name;
+ }
+
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else if (paramName === undefined) {
+ if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x20 || code === 0x09) {
+ if (end === -1 && start !== -1) end = i;
+ } else if (code === 0x3b || code === 0x2c) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ push(params, header.slice(start, end), true);
+ if (code === 0x2c) {
+ push(offers, extensionName, params);
+ params = Object.create(null);
+ extensionName = undefined;
+ }
+
+ start = end = -1;
+ } else if (code === 0x3d /* '=' */ && start !== -1 && end === -1) {
+ paramName = header.slice(start, i);
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else {
+ //
+ // The value of a quoted-string after unescaping must conform to the
+ // token ABNF, so only token characters are valid.
+ // Ref: https://tools.ietf.org/html/rfc6455#section-9.1
+ //
+ if (isEscaping) {
+ if (tokenChars[code] !== 1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ if (start === -1) start = i;
+ else if (!mustUnescape) mustUnescape = true;
+ isEscaping = false;
+ } else if (inQuotes) {
+ if (tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (code === 0x22 /* '"' */ && start !== -1) {
+ inQuotes = false;
+ end = i;
+ } else if (code === 0x5c /* '\' */) {
+ isEscaping = true;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ } else if (code === 0x22 && header.charCodeAt(i - 1) === 0x3d) {
+ inQuotes = true;
+ } else if (end === -1 && tokenChars[code] === 1) {
+ if (start === -1) start = i;
+ } else if (start !== -1 && (code === 0x20 || code === 0x09)) {
+ if (end === -1) end = i;
+ } else if (code === 0x3b || code === 0x2c) {
+ if (start === -1) {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+
+ if (end === -1) end = i;
+ let value = header.slice(start, end);
+ if (mustUnescape) {
+ value = value.replace(/\\/g, '');
+ mustUnescape = false;
+ }
+ push(params, paramName, value);
+ if (code === 0x2c) {
+ push(offers, extensionName, params);
+ params = Object.create(null);
+ extensionName = undefined;
+ }
+
+ paramName = undefined;
+ start = end = -1;
+ } else {
+ throw new SyntaxError(`Unexpected character at index ${i}`);
+ }
+ }
+ }
+
+ if (start === -1 || inQuotes) {
+ throw new SyntaxError('Unexpected end of input');
+ }
+
+ if (end === -1) end = i;
+ const token = header.slice(start, end);
+ if (extensionName === undefined) {
+ push(offers, token, params);
+ } else {
+ if (paramName === undefined) {
+ push(params, token, true);
+ } else if (mustUnescape) {
+ push(params, paramName, token.replace(/\\/g, ''));
+ } else {
+ push(params, paramName, token);
+ }
+ push(offers, extensionName, params);
+ }
+
+ return offers;
+}
+
+/**
+ * Builds the `Sec-WebSocket-Extensions` header field value.
+ *
+ * @param {Object} extensions The map of extensions and parameters to format
+ * @return {String} A string representing the given object
+ * @public
+ */
+function format(extensions) {
+ return Object.keys(extensions)
+ .map((extension) => {
+ let configurations = extensions[extension];
+ if (!Array.isArray(configurations)) configurations = [configurations];
+ return configurations
+ .map((params) => {
+ return [extension]
+ .concat(
+ Object.keys(params).map((k) => {
+ let values = params[k];
+ if (!Array.isArray(values)) values = [values];
+ return values
+ .map((v) => (v === true ? k : `${k}=${v}`))
+ .join('; ');
+ })
+ )
+ .join('; ');
+ })
+ .join(', ');
+ })
+ .join(', ');
+}
+
+module.exports = { format, parse };
diff --git a/node_modules/ws/lib/limiter.js b/node_modules/ws/lib/limiter.js
new file mode 100644
index 0000000..3fd3578
--- /dev/null
+++ b/node_modules/ws/lib/limiter.js
@@ -0,0 +1,55 @@
+'use strict';
+
+const kDone = Symbol('kDone');
+const kRun = Symbol('kRun');
+
+/**
+ * A very simple job queue with adjustable concurrency. Adapted from
+ * https://github.com/STRML/async-limiter
+ */
+class Limiter {
+ /**
+ * Creates a new `Limiter`.
+ *
+ * @param {Number} [concurrency=Infinity] The maximum number of jobs allowed
+ * to run concurrently
+ */
+ constructor(concurrency) {
+ this[kDone] = () => {
+ this.pending--;
+ this[kRun]();
+ };
+ this.concurrency = concurrency || Infinity;
+ this.jobs = [];
+ this.pending = 0;
+ }
+
+ /**
+ * Adds a job to the queue.
+ *
+ * @param {Function} job The job to run
+ * @public
+ */
+ add(job) {
+ this.jobs.push(job);
+ this[kRun]();
+ }
+
+ /**
+ * Removes a job from the queue and runs it if possible.
+ *
+ * @private
+ */
+ [kRun]() {
+ if (this.pending === this.concurrency) return;
+
+ if (this.jobs.length) {
+ const job = this.jobs.shift();
+
+ this.pending++;
+ job(this[kDone]);
+ }
+ }
+}
+
+module.exports = Limiter;
diff --git a/node_modules/ws/lib/permessage-deflate.js b/node_modules/ws/lib/permessage-deflate.js
new file mode 100644
index 0000000..ce91784
--- /dev/null
+++ b/node_modules/ws/lib/permessage-deflate.js
@@ -0,0 +1,518 @@
+'use strict';
+
+const zlib = require('zlib');
+
+const bufferUtil = require('./buffer-util');
+const Limiter = require('./limiter');
+const { kStatusCode, NOOP } = require('./constants');
+
+const TRAILER = Buffer.from([0x00, 0x00, 0xff, 0xff]);
+const kPerMessageDeflate = Symbol('permessage-deflate');
+const kTotalLength = Symbol('total-length');
+const kCallback = Symbol('callback');
+const kBuffers = Symbol('buffers');
+const kError = Symbol('error');
+
+//
+// We limit zlib concurrency, which prevents severe memory fragmentation
+// as documented in https://github.com/nodejs/node/issues/8871#issuecomment-250915913
+// and https://github.com/websockets/ws/issues/1202
+//
+// Intentionally global; it's the global thread pool that's an issue.
+//
+let zlibLimiter;
+
+/**
+ * permessage-deflate implementation.
+ */
+class PerMessageDeflate {
+ /**
+ * Creates a PerMessageDeflate instance.
+ *
+ * @param {Object} [options] Configuration options
+ * @param {Boolean} [options.serverNoContextTakeover=false] Request/accept
+ * disabling of server context takeover
+ * @param {Boolean} [options.clientNoContextTakeover=false] Advertise/
+ * acknowledge disabling of client context takeover
+ * @param {(Boolean|Number)} [options.serverMaxWindowBits] Request/confirm the
+ * use of a custom server window size
+ * @param {(Boolean|Number)} [options.clientMaxWindowBits] Advertise support
+ * for, or request, a custom client window size
+ * @param {Object} [options.zlibDeflateOptions] Options to pass to zlib on
+ * deflate
+ * @param {Object} [options.zlibInflateOptions] Options to pass to zlib on
+ * inflate
+ * @param {Number} [options.threshold=1024] Size (in bytes) below which
+ * messages should not be compressed
+ * @param {Number} [options.concurrencyLimit=10] The number of concurrent
+ * calls to zlib
+ * @param {Boolean} [isServer=false] Create the instance in either server or
+ * client mode
+ * @param {Number} [maxPayload=0] The maximum allowed message length
+ */
+ constructor(options, isServer, maxPayload) {
+ this._maxPayload = maxPayload | 0;
+ this._options = options || {};
+ this._threshold =
+ this._options.threshold !== undefined ? this._options.threshold : 1024;
+ this._isServer = !!isServer;
+ this._deflate = null;
+ this._inflate = null;
+
+ this.params = null;
+
+ if (!zlibLimiter) {
+ const concurrency =
+ this._options.concurrencyLimit !== undefined
+ ? this._options.concurrencyLimit
+ : 10;
+ zlibLimiter = new Limiter(concurrency);
+ }
+ }
+
+ /**
+ * @type {String}
+ */
+ static get extensionName() {
+ return 'permessage-deflate';
+ }
+
+ /**
+ * Create an extension negotiation offer.
+ *
+ * @return {Object} Extension parameters
+ * @public
+ */
+ offer() {
+ const params = {};
+
+ if (this._options.serverNoContextTakeover) {
+ params.server_no_context_takeover = true;
+ }
+ if (this._options.clientNoContextTakeover) {
+ params.client_no_context_takeover = true;
+ }
+ if (this._options.serverMaxWindowBits) {
+ params.server_max_window_bits = this._options.serverMaxWindowBits;
+ }
+ if (this._options.clientMaxWindowBits) {
+ params.client_max_window_bits = this._options.clientMaxWindowBits;
+ } else if (this._options.clientMaxWindowBits == null) {
+ params.client_max_window_bits = true;
+ }
+
+ return params;
+ }
+
+ /**
+ * Accept an extension negotiation offer/response.
+ *
+ * @param {Array} configurations The extension negotiation offers/reponse
+ * @return {Object} Accepted configuration
+ * @public
+ */
+ accept(configurations) {
+ configurations = this.normalizeParams(configurations);
+
+ this.params = this._isServer
+ ? this.acceptAsServer(configurations)
+ : this.acceptAsClient(configurations);
+
+ return this.params;
+ }
+
+ /**
+ * Releases all resources used by the extension.
+ *
+ * @public
+ */
+ cleanup() {
+ if (this._inflate) {
+ this._inflate.close();
+ this._inflate = null;
+ }
+
+ if (this._deflate) {
+ const callback = this._deflate[kCallback];
+
+ this._deflate.close();
+ this._deflate = null;
+
+ if (callback) {
+ callback(
+ new Error(
+ 'The deflate stream was closed while data was being processed'
+ )
+ );
+ }
+ }
+ }
+
+ /**
+ * Accept an extension negotiation offer.
+ *
+ * @param {Array} offers The extension negotiation offers
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsServer(offers) {
+ const opts = this._options;
+ const accepted = offers.find((params) => {
+ if (
+ (opts.serverNoContextTakeover === false &&
+ params.server_no_context_takeover) ||
+ (params.server_max_window_bits &&
+ (opts.serverMaxWindowBits === false ||
+ (typeof opts.serverMaxWindowBits === 'number' &&
+ opts.serverMaxWindowBits > params.server_max_window_bits))) ||
+ (typeof opts.clientMaxWindowBits === 'number' &&
+ !params.client_max_window_bits)
+ ) {
+ return false;
+ }
+
+ return true;
+ });
+
+ if (!accepted) {
+ throw new Error('None of the extension offers can be accepted');
+ }
+
+ if (opts.serverNoContextTakeover) {
+ accepted.server_no_context_takeover = true;
+ }
+ if (opts.clientNoContextTakeover) {
+ accepted.client_no_context_takeover = true;
+ }
+ if (typeof opts.serverMaxWindowBits === 'number') {
+ accepted.server_max_window_bits = opts.serverMaxWindowBits;
+ }
+ if (typeof opts.clientMaxWindowBits === 'number') {
+ accepted.client_max_window_bits = opts.clientMaxWindowBits;
+ } else if (
+ accepted.client_max_window_bits === true ||
+ opts.clientMaxWindowBits === false
+ ) {
+ delete accepted.client_max_window_bits;
+ }
+
+ return accepted;
+ }
+
+ /**
+ * Accept the extension negotiation response.
+ *
+ * @param {Array} response The extension negotiation response
+ * @return {Object} Accepted configuration
+ * @private
+ */
+ acceptAsClient(response) {
+ const params = response[0];
+
+ if (
+ this._options.clientNoContextTakeover === false &&
+ params.client_no_context_takeover
+ ) {
+ throw new Error('Unexpected parameter "client_no_context_takeover"');
+ }
+
+ if (!params.client_max_window_bits) {
+ if (typeof this._options.clientMaxWindowBits === 'number') {
+ params.client_max_window_bits = this._options.clientMaxWindowBits;
+ }
+ } else if (
+ this._options.clientMaxWindowBits === false ||
+ (typeof this._options.clientMaxWindowBits === 'number' &&
+ params.client_max_window_bits > this._options.clientMaxWindowBits)
+ ) {
+ throw new Error(
+ 'Unexpected or invalid parameter "client_max_window_bits"'
+ );
+ }
+
+ return params;
+ }
+
+ /**
+ * Normalize parameters.
+ *
+ * @param {Array} configurations The extension negotiation offers/reponse
+ * @return {Array} The offers/response with normalized parameters
+ * @private
+ */
+ normalizeParams(configurations) {
+ configurations.forEach((params) => {
+ Object.keys(params).forEach((key) => {
+ let value = params[key];
+
+ if (value.length > 1) {
+ throw new Error(`Parameter "${key}" must have only a single value`);
+ }
+
+ value = value[0];
+
+ if (key === 'client_max_window_bits') {
+ if (value !== true) {
+ const num = +value;
+ if (!Number.isInteger(num) || num < 8 || num > 15) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ value = num;
+ } else if (!this._isServer) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ } else if (key === 'server_max_window_bits') {
+ const num = +value;
+ if (!Number.isInteger(num) || num < 8 || num > 15) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ value = num;
+ } else if (
+ key === 'client_no_context_takeover' ||
+ key === 'server_no_context_takeover'
+ ) {
+ if (value !== true) {
+ throw new TypeError(
+ `Invalid value for parameter "${key}": ${value}`
+ );
+ }
+ } else {
+ throw new Error(`Unknown parameter "${key}"`);
+ }
+
+ params[key] = value;
+ });
+ });
+
+ return configurations;
+ }
+
+ /**
+ * Decompress data. Concurrency limited.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @public
+ */
+ decompress(data, fin, callback) {
+ zlibLimiter.add((done) => {
+ this._decompress(data, fin, (err, result) => {
+ done();
+ callback(err, result);
+ });
+ });
+ }
+
+ /**
+ * Compress data. Concurrency limited.
+ *
+ * @param {Buffer} data Data to compress
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @public
+ */
+ compress(data, fin, callback) {
+ zlibLimiter.add((done) => {
+ this._compress(data, fin, (err, result) => {
+ done();
+ callback(err, result);
+ });
+ });
+ }
+
+ /**
+ * Decompress data.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @private
+ */
+ _decompress(data, fin, callback) {
+ const endpoint = this._isServer ? 'client' : 'server';
+
+ if (!this._inflate) {
+ const key = `${endpoint}_max_window_bits`;
+ const windowBits =
+ typeof this.params[key] !== 'number'
+ ? zlib.Z_DEFAULT_WINDOWBITS
+ : this.params[key];
+
+ this._inflate = zlib.createInflateRaw({
+ ...this._options.zlibInflateOptions,
+ windowBits
+ });
+ this._inflate[kPerMessageDeflate] = this;
+ this._inflate[kTotalLength] = 0;
+ this._inflate[kBuffers] = [];
+ this._inflate.on('error', inflateOnError);
+ this._inflate.on('data', inflateOnData);
+ }
+
+ this._inflate[kCallback] = callback;
+
+ this._inflate.write(data);
+ if (fin) this._inflate.write(TRAILER);
+
+ this._inflate.flush(() => {
+ const err = this._inflate[kError];
+
+ if (err) {
+ this._inflate.close();
+ this._inflate = null;
+ callback(err);
+ return;
+ }
+
+ const data = bufferUtil.concat(
+ this._inflate[kBuffers],
+ this._inflate[kTotalLength]
+ );
+
+ if (this._inflate._readableState.endEmitted) {
+ this._inflate.close();
+ this._inflate = null;
+ } else {
+ this._inflate[kTotalLength] = 0;
+ this._inflate[kBuffers] = [];
+
+ if (fin && this.params[`${endpoint}_no_context_takeover`]) {
+ this._inflate.reset();
+ }
+ }
+
+ callback(null, data);
+ });
+ }
+
+ /**
+ * Compress data.
+ *
+ * @param {Buffer} data Data to compress
+ * @param {Boolean} fin Specifies whether or not this is the last fragment
+ * @param {Function} callback Callback
+ * @private
+ */
+ _compress(data, fin, callback) {
+ const endpoint = this._isServer ? 'server' : 'client';
+
+ if (!this._deflate) {
+ const key = `${endpoint}_max_window_bits`;
+ const windowBits =
+ typeof this.params[key] !== 'number'
+ ? zlib.Z_DEFAULT_WINDOWBITS
+ : this.params[key];
+
+ this._deflate = zlib.createDeflateRaw({
+ ...this._options.zlibDeflateOptions,
+ windowBits
+ });
+
+ this._deflate[kTotalLength] = 0;
+ this._deflate[kBuffers] = [];
+
+ //
+ // An `'error'` event is emitted, only on Node.js < 10.0.0, if the
+ // `zlib.DeflateRaw` instance is closed while data is being processed.
+ // This can happen if `PerMessageDeflate#cleanup()` is called at the wrong
+ // time due to an abnormal WebSocket closure.
+ //
+ this._deflate.on('error', NOOP);
+ this._deflate.on('data', deflateOnData);
+ }
+
+ this._deflate[kCallback] = callback;
+
+ this._deflate.write(data);
+ this._deflate.flush(zlib.Z_SYNC_FLUSH, () => {
+ if (!this._deflate) {
+ //
+ // The deflate stream was closed while data was being processed.
+ //
+ return;
+ }
+
+ let data = bufferUtil.concat(
+ this._deflate[kBuffers],
+ this._deflate[kTotalLength]
+ );
+
+ if (fin) data = data.slice(0, data.length - 4);
+
+ //
+ // Ensure that the callback will not be called again in
+ // `PerMessageDeflate#cleanup()`.
+ //
+ this._deflate[kCallback] = null;
+
+ this._deflate[kTotalLength] = 0;
+ this._deflate[kBuffers] = [];
+
+ if (fin && this.params[`${endpoint}_no_context_takeover`]) {
+ this._deflate.reset();
+ }
+
+ callback(null, data);
+ });
+ }
+}
+
+module.exports = PerMessageDeflate;
+
+/**
+ * The listener of the `zlib.DeflateRaw` stream `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function deflateOnData(chunk) {
+ this[kBuffers].push(chunk);
+ this[kTotalLength] += chunk.length;
+}
+
+/**
+ * The listener of the `zlib.InflateRaw` stream `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function inflateOnData(chunk) {
+ this[kTotalLength] += chunk.length;
+
+ if (
+ this[kPerMessageDeflate]._maxPayload < 1 ||
+ this[kTotalLength] <= this[kPerMessageDeflate]._maxPayload
+ ) {
+ this[kBuffers].push(chunk);
+ return;
+ }
+
+ this[kError] = new RangeError('Max payload size exceeded');
+ this[kError].code = 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH';
+ this[kError][kStatusCode] = 1009;
+ this.removeListener('data', inflateOnData);
+ this.reset();
+}
+
+/**
+ * The listener of the `zlib.InflateRaw` stream `'error'` event.
+ *
+ * @param {Error} err The emitted error
+ * @private
+ */
+function inflateOnError(err) {
+ //
+ // There is no need to call `Zlib#close()` as the handle is automatically
+ // closed when an error is emitted.
+ //
+ this[kPerMessageDeflate]._inflate = null;
+ err[kStatusCode] = 1007;
+ this[kCallback](err);
+}
diff --git a/node_modules/ws/lib/receiver.js b/node_modules/ws/lib/receiver.js
new file mode 100644
index 0000000..1d2af76
--- /dev/null
+++ b/node_modules/ws/lib/receiver.js
@@ -0,0 +1,607 @@
+'use strict';
+
+const { Writable } = require('stream');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const {
+ BINARY_TYPES,
+ EMPTY_BUFFER,
+ kStatusCode,
+ kWebSocket
+} = require('./constants');
+const { concat, toArrayBuffer, unmask } = require('./buffer-util');
+const { isValidStatusCode, isValidUTF8 } = require('./validation');
+
+const GET_INFO = 0;
+const GET_PAYLOAD_LENGTH_16 = 1;
+const GET_PAYLOAD_LENGTH_64 = 2;
+const GET_MASK = 3;
+const GET_DATA = 4;
+const INFLATING = 5;
+
+/**
+ * HyBi Receiver implementation.
+ *
+ * @extends Writable
+ */
+class Receiver extends Writable {
+ /**
+ * Creates a Receiver instance.
+ *
+ * @param {String} [binaryType=nodebuffer] The type for binary data
+ * @param {Object} [extensions] An object containing the negotiated extensions
+ * @param {Boolean} [isServer=false] Specifies whether to operate in client or
+ * server mode
+ * @param {Number} [maxPayload=0] The maximum allowed message length
+ */
+ constructor(binaryType, extensions, isServer, maxPayload) {
+ super();
+
+ this._binaryType = binaryType || BINARY_TYPES[0];
+ this[kWebSocket] = undefined;
+ this._extensions = extensions || {};
+ this._isServer = !!isServer;
+ this._maxPayload = maxPayload | 0;
+
+ this._bufferedBytes = 0;
+ this._buffers = [];
+
+ this._compressed = false;
+ this._payloadLength = 0;
+ this._mask = undefined;
+ this._fragmented = 0;
+ this._masked = false;
+ this._fin = false;
+ this._opcode = 0;
+
+ this._totalPayloadLength = 0;
+ this._messageLength = 0;
+ this._fragments = [];
+
+ this._state = GET_INFO;
+ this._loop = false;
+ }
+
+ /**
+ * Implements `Writable.prototype._write()`.
+ *
+ * @param {Buffer} chunk The chunk of data to write
+ * @param {String} encoding The character encoding of `chunk`
+ * @param {Function} cb Callback
+ * @private
+ */
+ _write(chunk, encoding, cb) {
+ if (this._opcode === 0x08 && this._state == GET_INFO) return cb();
+
+ this._bufferedBytes += chunk.length;
+ this._buffers.push(chunk);
+ this.startLoop(cb);
+ }
+
+ /**
+ * Consumes `n` bytes from the buffered data.
+ *
+ * @param {Number} n The number of bytes to consume
+ * @return {Buffer} The consumed bytes
+ * @private
+ */
+ consume(n) {
+ this._bufferedBytes -= n;
+
+ if (n === this._buffers[0].length) return this._buffers.shift();
+
+ if (n < this._buffers[0].length) {
+ const buf = this._buffers[0];
+ this._buffers[0] = buf.slice(n);
+ return buf.slice(0, n);
+ }
+
+ const dst = Buffer.allocUnsafe(n);
+
+ do {
+ const buf = this._buffers[0];
+ const offset = dst.length - n;
+
+ if (n >= buf.length) {
+ dst.set(this._buffers.shift(), offset);
+ } else {
+ dst.set(new Uint8Array(buf.buffer, buf.byteOffset, n), offset);
+ this._buffers[0] = buf.slice(n);
+ }
+
+ n -= buf.length;
+ } while (n > 0);
+
+ return dst;
+ }
+
+ /**
+ * Starts the parsing loop.
+ *
+ * @param {Function} cb Callback
+ * @private
+ */
+ startLoop(cb) {
+ let err;
+ this._loop = true;
+
+ do {
+ switch (this._state) {
+ case GET_INFO:
+ err = this.getInfo();
+ break;
+ case GET_PAYLOAD_LENGTH_16:
+ err = this.getPayloadLength16();
+ break;
+ case GET_PAYLOAD_LENGTH_64:
+ err = this.getPayloadLength64();
+ break;
+ case GET_MASK:
+ this.getMask();
+ break;
+ case GET_DATA:
+ err = this.getData(cb);
+ break;
+ default:
+ // `INFLATING`
+ this._loop = false;
+ return;
+ }
+ } while (this._loop);
+
+ cb(err);
+ }
+
+ /**
+ * Reads the first two bytes of a frame.
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getInfo() {
+ if (this._bufferedBytes < 2) {
+ this._loop = false;
+ return;
+ }
+
+ const buf = this.consume(2);
+
+ if ((buf[0] & 0x30) !== 0x00) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV2 and RSV3 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_2_3'
+ );
+ }
+
+ const compressed = (buf[0] & 0x40) === 0x40;
+
+ if (compressed && !this._extensions[PerMessageDeflate.extensionName]) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ this._fin = (buf[0] & 0x80) === 0x80;
+ this._opcode = buf[0] & 0x0f;
+ this._payloadLength = buf[1] & 0x7f;
+
+ if (this._opcode === 0x00) {
+ if (compressed) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ if (!this._fragmented) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'invalid opcode 0',
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ this._opcode = this._fragmented;
+ } else if (this._opcode === 0x01 || this._opcode === 0x02) {
+ if (this._fragmented) {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid opcode ${this._opcode}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ this._compressed = compressed;
+ } else if (this._opcode > 0x07 && this._opcode < 0x0b) {
+ if (!this._fin) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'FIN must be set',
+ true,
+ 1002,
+ 'WS_ERR_EXPECTED_FIN'
+ );
+ }
+
+ if (compressed) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'RSV1 must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_RSV_1'
+ );
+ }
+
+ if (this._payloadLength > 0x7d) {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid payload length ${this._payloadLength}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
+ );
+ }
+ } else {
+ this._loop = false;
+ return error(
+ RangeError,
+ `invalid opcode ${this._opcode}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_OPCODE'
+ );
+ }
+
+ if (!this._fin && !this._fragmented) this._fragmented = this._opcode;
+ this._masked = (buf[1] & 0x80) === 0x80;
+
+ if (this._isServer) {
+ if (!this._masked) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'MASK must be set',
+ true,
+ 1002,
+ 'WS_ERR_EXPECTED_MASK'
+ );
+ }
+ } else if (this._masked) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'MASK must be clear',
+ true,
+ 1002,
+ 'WS_ERR_UNEXPECTED_MASK'
+ );
+ }
+
+ if (this._payloadLength === 126) this._state = GET_PAYLOAD_LENGTH_16;
+ else if (this._payloadLength === 127) this._state = GET_PAYLOAD_LENGTH_64;
+ else return this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+16).
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getPayloadLength16() {
+ if (this._bufferedBytes < 2) {
+ this._loop = false;
+ return;
+ }
+
+ this._payloadLength = this.consume(2).readUInt16BE(0);
+ return this.haveLength();
+ }
+
+ /**
+ * Gets extended payload length (7+64).
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ getPayloadLength64() {
+ if (this._bufferedBytes < 8) {
+ this._loop = false;
+ return;
+ }
+
+ const buf = this.consume(8);
+ const num = buf.readUInt32BE(0);
+
+ //
+ // The maximum safe integer in JavaScript is 2^53 - 1. An error is returned
+ // if payload length is greater than this number.
+ //
+ if (num > Math.pow(2, 53 - 32) - 1) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'Unsupported WebSocket frame: payload length > 2^53 - 1',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_DATA_PAYLOAD_LENGTH'
+ );
+ }
+
+ this._payloadLength = num * Math.pow(2, 32) + buf.readUInt32BE(4);
+ return this.haveLength();
+ }
+
+ /**
+ * Payload length has been read.
+ *
+ * @return {(RangeError|undefined)} A possible error
+ * @private
+ */
+ haveLength() {
+ if (this._payloadLength && this._opcode < 0x08) {
+ this._totalPayloadLength += this._payloadLength;
+ if (this._totalPayloadLength > this._maxPayload && this._maxPayload > 0) {
+ this._loop = false;
+ return error(
+ RangeError,
+ 'Max payload size exceeded',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
+ );
+ }
+ }
+
+ if (this._masked) this._state = GET_MASK;
+ else this._state = GET_DATA;
+ }
+
+ /**
+ * Reads mask bytes.
+ *
+ * @private
+ */
+ getMask() {
+ if (this._bufferedBytes < 4) {
+ this._loop = false;
+ return;
+ }
+
+ this._mask = this.consume(4);
+ this._state = GET_DATA;
+ }
+
+ /**
+ * Reads data bytes.
+ *
+ * @param {Function} cb Callback
+ * @return {(Error|RangeError|undefined)} A possible error
+ * @private
+ */
+ getData(cb) {
+ let data = EMPTY_BUFFER;
+
+ if (this._payloadLength) {
+ if (this._bufferedBytes < this._payloadLength) {
+ this._loop = false;
+ return;
+ }
+
+ data = this.consume(this._payloadLength);
+ if (this._masked) unmask(data, this._mask);
+ }
+
+ if (this._opcode > 0x07) return this.controlMessage(data);
+
+ if (this._compressed) {
+ this._state = INFLATING;
+ this.decompress(data, cb);
+ return;
+ }
+
+ if (data.length) {
+ //
+ // This message is not compressed so its lenght is the sum of the payload
+ // length of all fragments.
+ //
+ this._messageLength = this._totalPayloadLength;
+ this._fragments.push(data);
+ }
+
+ return this.dataMessage();
+ }
+
+ /**
+ * Decompresses data.
+ *
+ * @param {Buffer} data Compressed data
+ * @param {Function} cb Callback
+ * @private
+ */
+ decompress(data, cb) {
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+
+ perMessageDeflate.decompress(data, this._fin, (err, buf) => {
+ if (err) return cb(err);
+
+ if (buf.length) {
+ this._messageLength += buf.length;
+ if (this._messageLength > this._maxPayload && this._maxPayload > 0) {
+ return cb(
+ error(
+ RangeError,
+ 'Max payload size exceeded',
+ false,
+ 1009,
+ 'WS_ERR_UNSUPPORTED_MESSAGE_LENGTH'
+ )
+ );
+ }
+
+ this._fragments.push(buf);
+ }
+
+ const er = this.dataMessage();
+ if (er) return cb(er);
+
+ this.startLoop(cb);
+ });
+ }
+
+ /**
+ * Handles a data message.
+ *
+ * @return {(Error|undefined)} A possible error
+ * @private
+ */
+ dataMessage() {
+ if (this._fin) {
+ const messageLength = this._messageLength;
+ const fragments = this._fragments;
+
+ this._totalPayloadLength = 0;
+ this._messageLength = 0;
+ this._fragmented = 0;
+ this._fragments = [];
+
+ if (this._opcode === 2) {
+ let data;
+
+ if (this._binaryType === 'nodebuffer') {
+ data = concat(fragments, messageLength);
+ } else if (this._binaryType === 'arraybuffer') {
+ data = toArrayBuffer(concat(fragments, messageLength));
+ } else {
+ data = fragments;
+ }
+
+ this.emit('message', data);
+ } else {
+ const buf = concat(fragments, messageLength);
+
+ if (!isValidUTF8(buf)) {
+ this._loop = false;
+ return error(
+ Error,
+ 'invalid UTF-8 sequence',
+ true,
+ 1007,
+ 'WS_ERR_INVALID_UTF8'
+ );
+ }
+
+ this.emit('message', buf.toString());
+ }
+ }
+
+ this._state = GET_INFO;
+ }
+
+ /**
+ * Handles a control message.
+ *
+ * @param {Buffer} data Data to handle
+ * @return {(Error|RangeError|undefined)} A possible error
+ * @private
+ */
+ controlMessage(data) {
+ if (this._opcode === 0x08) {
+ this._loop = false;
+
+ if (data.length === 0) {
+ this.emit('conclude', 1005, '');
+ this.end();
+ } else if (data.length === 1) {
+ return error(
+ RangeError,
+ 'invalid payload length 1',
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CONTROL_PAYLOAD_LENGTH'
+ );
+ } else {
+ const code = data.readUInt16BE(0);
+
+ if (!isValidStatusCode(code)) {
+ return error(
+ RangeError,
+ `invalid status code ${code}`,
+ true,
+ 1002,
+ 'WS_ERR_INVALID_CLOSE_CODE'
+ );
+ }
+
+ const buf = data.slice(2);
+
+ if (!isValidUTF8(buf)) {
+ return error(
+ Error,
+ 'invalid UTF-8 sequence',
+ true,
+ 1007,
+ 'WS_ERR_INVALID_UTF8'
+ );
+ }
+
+ this.emit('conclude', code, buf.toString());
+ this.end();
+ }
+ } else if (this._opcode === 0x09) {
+ this.emit('ping', data);
+ } else {
+ this.emit('pong', data);
+ }
+
+ this._state = GET_INFO;
+ }
+}
+
+module.exports = Receiver;
+
+/**
+ * Builds an error object.
+ *
+ * @param {function(new:Error|RangeError)} ErrorCtor The error constructor
+ * @param {String} message The error message
+ * @param {Boolean} prefix Specifies whether or not to add a default prefix to
+ * `message`
+ * @param {Number} statusCode The status code
+ * @param {String} errorCode The exposed error code
+ * @return {(Error|RangeError)} The error
+ * @private
+ */
+function error(ErrorCtor, message, prefix, statusCode, errorCode) {
+ const err = new ErrorCtor(
+ prefix ? `Invalid WebSocket frame: ${message}` : message
+ );
+
+ Error.captureStackTrace(err, error);
+ err.code = errorCode;
+ err[kStatusCode] = statusCode;
+ return err;
+}
diff --git a/node_modules/ws/lib/sender.js b/node_modules/ws/lib/sender.js
new file mode 100644
index 0000000..441171c
--- /dev/null
+++ b/node_modules/ws/lib/sender.js
@@ -0,0 +1,409 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls$" }] */
+
+'use strict';
+
+const net = require('net');
+const tls = require('tls');
+const { randomFillSync } = require('crypto');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const { EMPTY_BUFFER } = require('./constants');
+const { isValidStatusCode } = require('./validation');
+const { mask: applyMask, toBuffer } = require('./buffer-util');
+
+const mask = Buffer.alloc(4);
+
+/**
+ * HyBi Sender implementation.
+ */
+class Sender {
+ /**
+ * Creates a Sender instance.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The connection socket
+ * @param {Object} [extensions] An object containing the negotiated extensions
+ */
+ constructor(socket, extensions) {
+ this._extensions = extensions || {};
+ this._socket = socket;
+
+ this._firstFragment = true;
+ this._compress = false;
+
+ this._bufferedBytes = 0;
+ this._deflating = false;
+ this._queue = [];
+ }
+
+ /**
+ * Frames a piece of data according to the HyBi WebSocket protocol.
+ *
+ * @param {Buffer} data The data to frame
+ * @param {Object} options Options object
+ * @param {Number} options.opcode The opcode
+ * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
+ * modified
+ * @param {Boolean} [options.fin=false] Specifies whether or not to set the
+ * FIN bit
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
+ * RSV1 bit
+ * @return {Buffer[]} The framed data as a list of `Buffer` instances
+ * @public
+ */
+ static frame(data, options) {
+ const merge = options.mask && options.readOnly;
+ let offset = options.mask ? 6 : 2;
+ let payloadLength = data.length;
+
+ if (data.length >= 65536) {
+ offset += 8;
+ payloadLength = 127;
+ } else if (data.length > 125) {
+ offset += 2;
+ payloadLength = 126;
+ }
+
+ const target = Buffer.allocUnsafe(merge ? data.length + offset : offset);
+
+ target[0] = options.fin ? options.opcode | 0x80 : options.opcode;
+ if (options.rsv1) target[0] |= 0x40;
+
+ target[1] = payloadLength;
+
+ if (payloadLength === 126) {
+ target.writeUInt16BE(data.length, 2);
+ } else if (payloadLength === 127) {
+ target.writeUInt32BE(0, 2);
+ target.writeUInt32BE(data.length, 6);
+ }
+
+ if (!options.mask) return [target, data];
+
+ randomFillSync(mask, 0, 4);
+
+ target[1] |= 0x80;
+ target[offset - 4] = mask[0];
+ target[offset - 3] = mask[1];
+ target[offset - 2] = mask[2];
+ target[offset - 1] = mask[3];
+
+ if (merge) {
+ applyMask(data, mask, target, offset, data.length);
+ return [target];
+ }
+
+ applyMask(data, mask, data, 0, data.length);
+ return [target, data];
+ }
+
+ /**
+ * Sends a close message to the other peer.
+ *
+ * @param {Number} [code] The status code component of the body
+ * @param {String} [data] The message component of the body
+ * @param {Boolean} [mask=false] Specifies whether or not to mask the message
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ close(code, data, mask, cb) {
+ let buf;
+
+ if (code === undefined) {
+ buf = EMPTY_BUFFER;
+ } else if (typeof code !== 'number' || !isValidStatusCode(code)) {
+ throw new TypeError('First argument must be a valid error code number');
+ } else if (data === undefined || data === '') {
+ buf = Buffer.allocUnsafe(2);
+ buf.writeUInt16BE(code, 0);
+ } else {
+ const length = Buffer.byteLength(data);
+
+ if (length > 123) {
+ throw new RangeError('The message must not be greater than 123 bytes');
+ }
+
+ buf = Buffer.allocUnsafe(2 + length);
+ buf.writeUInt16BE(code, 0);
+ buf.write(data, 2);
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doClose, buf, mask, cb]);
+ } else {
+ this.doClose(buf, mask, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a close message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doClose(data, mask, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x08,
+ mask,
+ readOnly: false
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a ping message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ ping(data, mask, cb) {
+ const buf = toBuffer(data);
+
+ if (buf.length > 125) {
+ throw new RangeError('The data size must not be greater than 125 bytes');
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doPing, buf, mask, toBuffer.readOnly, cb]);
+ } else {
+ this.doPing(buf, mask, toBuffer.readOnly, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a ping message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doPing(data, mask, readOnly, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x09,
+ mask,
+ readOnly
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a pong message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ pong(data, mask, cb) {
+ const buf = toBuffer(data);
+
+ if (buf.length > 125) {
+ throw new RangeError('The data size must not be greater than 125 bytes');
+ }
+
+ if (this._deflating) {
+ this.enqueue([this.doPong, buf, mask, toBuffer.readOnly, cb]);
+ } else {
+ this.doPong(buf, mask, toBuffer.readOnly, cb);
+ }
+ }
+
+ /**
+ * Frames and sends a pong message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [mask=false] Specifies whether or not to mask `data`
+ * @param {Boolean} [readOnly=false] Specifies whether `data` can be modified
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ doPong(data, mask, readOnly, cb) {
+ this.sendFrame(
+ Sender.frame(data, {
+ fin: true,
+ rsv1: false,
+ opcode: 0x0a,
+ mask,
+ readOnly
+ }),
+ cb
+ );
+ }
+
+ /**
+ * Sends a data message to the other peer.
+ *
+ * @param {*} data The message to send
+ * @param {Object} options Options object
+ * @param {Boolean} [options.compress=false] Specifies whether or not to
+ * compress `data`
+ * @param {Boolean} [options.binary=false] Specifies whether `data` is binary
+ * or text
+ * @param {Boolean} [options.fin=false] Specifies whether the fragment is the
+ * last one
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ send(data, options, cb) {
+ const buf = toBuffer(data);
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+ let opcode = options.binary ? 2 : 1;
+ let rsv1 = options.compress;
+
+ if (this._firstFragment) {
+ this._firstFragment = false;
+ if (rsv1 && perMessageDeflate) {
+ rsv1 = buf.length >= perMessageDeflate._threshold;
+ }
+ this._compress = rsv1;
+ } else {
+ rsv1 = false;
+ opcode = 0;
+ }
+
+ if (options.fin) this._firstFragment = true;
+
+ if (perMessageDeflate) {
+ const opts = {
+ fin: options.fin,
+ rsv1,
+ opcode,
+ mask: options.mask,
+ readOnly: toBuffer.readOnly
+ };
+
+ if (this._deflating) {
+ this.enqueue([this.dispatch, buf, this._compress, opts, cb]);
+ } else {
+ this.dispatch(buf, this._compress, opts, cb);
+ }
+ } else {
+ this.sendFrame(
+ Sender.frame(buf, {
+ fin: options.fin,
+ rsv1: false,
+ opcode,
+ mask: options.mask,
+ readOnly: toBuffer.readOnly
+ }),
+ cb
+ );
+ }
+ }
+
+ /**
+ * Dispatches a data message.
+ *
+ * @param {Buffer} data The message to send
+ * @param {Boolean} [compress=false] Specifies whether or not to compress
+ * `data`
+ * @param {Object} options Options object
+ * @param {Number} options.opcode The opcode
+ * @param {Boolean} [options.readOnly=false] Specifies whether `data` can be
+ * modified
+ * @param {Boolean} [options.fin=false] Specifies whether or not to set the
+ * FIN bit
+ * @param {Boolean} [options.mask=false] Specifies whether or not to mask
+ * `data`
+ * @param {Boolean} [options.rsv1=false] Specifies whether or not to set the
+ * RSV1 bit
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ dispatch(data, compress, options, cb) {
+ if (!compress) {
+ this.sendFrame(Sender.frame(data, options), cb);
+ return;
+ }
+
+ const perMessageDeflate = this._extensions[PerMessageDeflate.extensionName];
+
+ this._bufferedBytes += data.length;
+ this._deflating = true;
+ perMessageDeflate.compress(data, options.fin, (_, buf) => {
+ if (this._socket.destroyed) {
+ const err = new Error(
+ 'The socket was closed while data was being compressed'
+ );
+
+ if (typeof cb === 'function') cb(err);
+
+ for (let i = 0; i < this._queue.length; i++) {
+ const callback = this._queue[i][4];
+
+ if (typeof callback === 'function') callback(err);
+ }
+
+ return;
+ }
+
+ this._bufferedBytes -= data.length;
+ this._deflating = false;
+ options.readOnly = false;
+ this.sendFrame(Sender.frame(buf, options), cb);
+ this.dequeue();
+ });
+ }
+
+ /**
+ * Executes queued send operations.
+ *
+ * @private
+ */
+ dequeue() {
+ while (!this._deflating && this._queue.length) {
+ const params = this._queue.shift();
+
+ this._bufferedBytes -= params[1].length;
+ Reflect.apply(params[0], this, params.slice(1));
+ }
+ }
+
+ /**
+ * Enqueues a send operation.
+ *
+ * @param {Array} params Send operation parameters.
+ * @private
+ */
+ enqueue(params) {
+ this._bufferedBytes += params[1].length;
+ this._queue.push(params);
+ }
+
+ /**
+ * Sends a frame.
+ *
+ * @param {Buffer[]} list The frame to send
+ * @param {Function} [cb] Callback
+ * @private
+ */
+ sendFrame(list, cb) {
+ if (list.length === 2) {
+ this._socket.cork();
+ this._socket.write(list[0]);
+ this._socket.write(list[1], cb);
+ this._socket.uncork();
+ } else {
+ this._socket.write(list[0], cb);
+ }
+ }
+}
+
+module.exports = Sender;
diff --git a/node_modules/ws/lib/stream.js b/node_modules/ws/lib/stream.js
new file mode 100644
index 0000000..19e1bff
--- /dev/null
+++ b/node_modules/ws/lib/stream.js
@@ -0,0 +1,180 @@
+'use strict';
+
+const { Duplex } = require('stream');
+
+/**
+ * Emits the `'close'` event on a stream.
+ *
+ * @param {Duplex} stream The stream.
+ * @private
+ */
+function emitClose(stream) {
+ stream.emit('close');
+}
+
+/**
+ * The listener of the `'end'` event.
+ *
+ * @private
+ */
+function duplexOnEnd() {
+ if (!this.destroyed && this._writableState.finished) {
+ this.destroy();
+ }
+}
+
+/**
+ * The listener of the `'error'` event.
+ *
+ * @param {Error} err The error
+ * @private
+ */
+function duplexOnError(err) {
+ this.removeListener('error', duplexOnError);
+ this.destroy();
+ if (this.listenerCount('error') === 0) {
+ // Do not suppress the throwing behavior.
+ this.emit('error', err);
+ }
+}
+
+/**
+ * Wraps a `WebSocket` in a duplex stream.
+ *
+ * @param {WebSocket} ws The `WebSocket` to wrap
+ * @param {Object} [options] The options for the `Duplex` constructor
+ * @return {Duplex} The duplex stream
+ * @public
+ */
+function createWebSocketStream(ws, options) {
+ let resumeOnReceiverDrain = true;
+ let terminateOnDestroy = true;
+
+ function receiverOnDrain() {
+ if (resumeOnReceiverDrain) ws._socket.resume();
+ }
+
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ ws._receiver.removeAllListeners('drain');
+ ws._receiver.on('drain', receiverOnDrain);
+ });
+ } else {
+ ws._receiver.removeAllListeners('drain');
+ ws._receiver.on('drain', receiverOnDrain);
+ }
+
+ const duplex = new Duplex({
+ ...options,
+ autoDestroy: false,
+ emitClose: false,
+ objectMode: false,
+ writableObjectMode: false
+ });
+
+ ws.on('message', function message(msg) {
+ if (!duplex.push(msg)) {
+ resumeOnReceiverDrain = false;
+ ws._socket.pause();
+ }
+ });
+
+ ws.once('error', function error(err) {
+ if (duplex.destroyed) return;
+
+ // Prevent `ws.terminate()` from being called by `duplex._destroy()`.
+ //
+ // - If the `'error'` event is emitted before the `'open'` event, then
+ // `ws.terminate()` is a noop as no socket is assigned.
+ // - Otherwise, the error is re-emitted by the listener of the `'error'`
+ // event of the `Receiver` object. The listener already closes the
+ // connection by calling `ws.close()`. This allows a close frame to be
+ // sent to the other peer. If `ws.terminate()` is called right after this,
+ // then the close frame might not be sent.
+ terminateOnDestroy = false;
+ duplex.destroy(err);
+ });
+
+ ws.once('close', function close() {
+ if (duplex.destroyed) return;
+
+ duplex.push(null);
+ });
+
+ duplex._destroy = function (err, callback) {
+ if (ws.readyState === ws.CLOSED) {
+ callback(err);
+ process.nextTick(emitClose, duplex);
+ return;
+ }
+
+ let called = false;
+
+ ws.once('error', function error(err) {
+ called = true;
+ callback(err);
+ });
+
+ ws.once('close', function close() {
+ if (!called) callback(err);
+ process.nextTick(emitClose, duplex);
+ });
+
+ if (terminateOnDestroy) ws.terminate();
+ };
+
+ duplex._final = function (callback) {
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ duplex._final(callback);
+ });
+ return;
+ }
+
+ // If the value of the `_socket` property is `null` it means that `ws` is a
+ // client websocket and the handshake failed. In fact, when this happens, a
+ // socket is never assigned to the websocket. Wait for the `'error'` event
+ // that will be emitted by the websocket.
+ if (ws._socket === null) return;
+
+ if (ws._socket._writableState.finished) {
+ callback();
+ if (duplex._readableState.endEmitted) duplex.destroy();
+ } else {
+ ws._socket.once('finish', function finish() {
+ // `duplex` is not destroyed here because the `'end'` event will be
+ // emitted on `duplex` after this `'finish'` event. The EOF signaling
+ // `null` chunk is, in fact, pushed when the websocket emits `'close'`.
+ callback();
+ });
+ ws.close();
+ }
+ };
+
+ duplex._read = function () {
+ if (
+ (ws.readyState === ws.OPEN || ws.readyState === ws.CLOSING) &&
+ !resumeOnReceiverDrain
+ ) {
+ resumeOnReceiverDrain = true;
+ if (!ws._receiver._writableState.needDrain) ws._socket.resume();
+ }
+ };
+
+ duplex._write = function (chunk, encoding, callback) {
+ if (ws.readyState === ws.CONNECTING) {
+ ws.once('open', function open() {
+ duplex._write(chunk, encoding, callback);
+ });
+ return;
+ }
+
+ ws.send(chunk, callback);
+ };
+
+ duplex.on('end', duplexOnEnd);
+ duplex.on('error', duplexOnError);
+ return duplex;
+}
+
+module.exports = createWebSocketStream;
diff --git a/node_modules/ws/lib/validation.js b/node_modules/ws/lib/validation.js
new file mode 100644
index 0000000..169ac6f
--- /dev/null
+++ b/node_modules/ws/lib/validation.js
@@ -0,0 +1,104 @@
+'use strict';
+
+/**
+ * Checks if a status code is allowed in a close frame.
+ *
+ * @param {Number} code The status code
+ * @return {Boolean} `true` if the status code is valid, else `false`
+ * @public
+ */
+function isValidStatusCode(code) {
+ return (
+ (code >= 1000 &&
+ code <= 1014 &&
+ code !== 1004 &&
+ code !== 1005 &&
+ code !== 1006) ||
+ (code >= 3000 && code <= 4999)
+ );
+}
+
+/**
+ * Checks if a given buffer contains only correct UTF-8.
+ * Ported from https://www.cl.cam.ac.uk/%7Emgk25/ucs/utf8_check.c by
+ * Markus Kuhn.
+ *
+ * @param {Buffer} buf The buffer to check
+ * @return {Boolean} `true` if `buf` contains only correct UTF-8, else `false`
+ * @public
+ */
+function _isValidUTF8(buf) {
+ const len = buf.length;
+ let i = 0;
+
+ while (i < len) {
+ if ((buf[i] & 0x80) === 0) {
+ // 0xxxxxxx
+ i++;
+ } else if ((buf[i] & 0xe0) === 0xc0) {
+ // 110xxxxx 10xxxxxx
+ if (
+ i + 1 === len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i] & 0xfe) === 0xc0 // Overlong
+ ) {
+ return false;
+ }
+
+ i += 2;
+ } else if ((buf[i] & 0xf0) === 0xe0) {
+ // 1110xxxx 10xxxxxx 10xxxxxx
+ if (
+ i + 2 >= len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i + 2] & 0xc0) !== 0x80 ||
+ (buf[i] === 0xe0 && (buf[i + 1] & 0xe0) === 0x80) || // Overlong
+ (buf[i] === 0xed && (buf[i + 1] & 0xe0) === 0xa0) // Surrogate (U+D800 - U+DFFF)
+ ) {
+ return false;
+ }
+
+ i += 3;
+ } else if ((buf[i] & 0xf8) === 0xf0) {
+ // 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx
+ if (
+ i + 3 >= len ||
+ (buf[i + 1] & 0xc0) !== 0x80 ||
+ (buf[i + 2] & 0xc0) !== 0x80 ||
+ (buf[i + 3] & 0xc0) !== 0x80 ||
+ (buf[i] === 0xf0 && (buf[i + 1] & 0xf0) === 0x80) || // Overlong
+ (buf[i] === 0xf4 && buf[i + 1] > 0x8f) ||
+ buf[i] > 0xf4 // > U+10FFFF
+ ) {
+ return false;
+ }
+
+ i += 4;
+ } else {
+ return false;
+ }
+ }
+
+ return true;
+}
+
+try {
+ let isValidUTF8 = require('utf-8-validate');
+
+ /* istanbul ignore if */
+ if (typeof isValidUTF8 === 'object') {
+ isValidUTF8 = isValidUTF8.Validation.isValidUTF8; // utf-8-validate@<3.0.0
+ }
+
+ module.exports = {
+ isValidStatusCode,
+ isValidUTF8(buf) {
+ return buf.length < 150 ? _isValidUTF8(buf) : isValidUTF8(buf);
+ }
+ };
+} catch (e) /* istanbul ignore next */ {
+ module.exports = {
+ isValidStatusCode,
+ isValidUTF8: _isValidUTF8
+ };
+}
diff --git a/node_modules/ws/lib/websocket-server.js b/node_modules/ws/lib/websocket-server.js
new file mode 100644
index 0000000..fe7fdf5
--- /dev/null
+++ b/node_modules/ws/lib/websocket-server.js
@@ -0,0 +1,447 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^net|tls|https$" }] */
+
+'use strict';
+
+const EventEmitter = require('events');
+const http = require('http');
+const https = require('https');
+const net = require('net');
+const tls = require('tls');
+const { createHash } = require('crypto');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const WebSocket = require('./websocket');
+const { format, parse } = require('./extension');
+const { GUID, kWebSocket } = require('./constants');
+
+const keyRegex = /^[+/0-9A-Za-z]{22}==$/;
+
+const RUNNING = 0;
+const CLOSING = 1;
+const CLOSED = 2;
+
+/**
+ * Class representing a WebSocket server.
+ *
+ * @extends EventEmitter
+ */
+class WebSocketServer extends EventEmitter {
+ /**
+ * Create a `WebSocketServer` instance.
+ *
+ * @param {Object} options Configuration options
+ * @param {Number} [options.backlog=511] The maximum length of the queue of
+ * pending connections
+ * @param {Boolean} [options.clientTracking=true] Specifies whether or not to
+ * track clients
+ * @param {Function} [options.handleProtocols] A hook to handle protocols
+ * @param {String} [options.host] The hostname where to bind the server
+ * @param {Number} [options.maxPayload=104857600] The maximum allowed message
+ * size
+ * @param {Boolean} [options.noServer=false] Enable no server mode
+ * @param {String} [options.path] Accept only connections matching this path
+ * @param {(Boolean|Object)} [options.perMessageDeflate=false] Enable/disable
+ * permessage-deflate
+ * @param {Number} [options.port] The port where to bind the server
+ * @param {(http.Server|https.Server)} [options.server] A pre-created HTTP/S
+ * server to use
+ * @param {Function} [options.verifyClient] A hook to reject connections
+ * @param {Function} [callback] A listener for the `listening` event
+ */
+ constructor(options, callback) {
+ super();
+
+ options = {
+ maxPayload: 100 * 1024 * 1024,
+ perMessageDeflate: false,
+ handleProtocols: null,
+ clientTracking: true,
+ verifyClient: null,
+ noServer: false,
+ backlog: null, // use default (511 as implemented in net.js)
+ server: null,
+ host: null,
+ path: null,
+ port: null,
+ ...options
+ };
+
+ if (
+ (options.port == null && !options.server && !options.noServer) ||
+ (options.port != null && (options.server || options.noServer)) ||
+ (options.server && options.noServer)
+ ) {
+ throw new TypeError(
+ 'One and only one of the "port", "server", or "noServer" options ' +
+ 'must be specified'
+ );
+ }
+
+ if (options.port != null) {
+ this._server = http.createServer((req, res) => {
+ const body = http.STATUS_CODES[426];
+
+ res.writeHead(426, {
+ 'Content-Length': body.length,
+ 'Content-Type': 'text/plain'
+ });
+ res.end(body);
+ });
+ this._server.listen(
+ options.port,
+ options.host,
+ options.backlog,
+ callback
+ );
+ } else if (options.server) {
+ this._server = options.server;
+ }
+
+ if (this._server) {
+ const emitConnection = this.emit.bind(this, 'connection');
+
+ this._removeListeners = addListeners(this._server, {
+ listening: this.emit.bind(this, 'listening'),
+ error: this.emit.bind(this, 'error'),
+ upgrade: (req, socket, head) => {
+ this.handleUpgrade(req, socket, head, emitConnection);
+ }
+ });
+ }
+
+ if (options.perMessageDeflate === true) options.perMessageDeflate = {};
+ if (options.clientTracking) this.clients = new Set();
+ this.options = options;
+ this._state = RUNNING;
+ }
+
+ /**
+ * Returns the bound address, the address family name, and port of the server
+ * as reported by the operating system if listening on an IP socket.
+ * If the server is listening on a pipe or UNIX domain socket, the name is
+ * returned as a string.
+ *
+ * @return {(Object|String|null)} The address of the server
+ * @public
+ */
+ address() {
+ if (this.options.noServer) {
+ throw new Error('The server is operating in "noServer" mode');
+ }
+
+ if (!this._server) return null;
+ return this._server.address();
+ }
+
+ /**
+ * Close the server.
+ *
+ * @param {Function} [cb] Callback
+ * @public
+ */
+ close(cb) {
+ if (cb) this.once('close', cb);
+
+ if (this._state === CLOSED) {
+ process.nextTick(emitClose, this);
+ return;
+ }
+
+ if (this._state === CLOSING) return;
+ this._state = CLOSING;
+
+ //
+ // Terminate all associated clients.
+ //
+ if (this.clients) {
+ for (const client of this.clients) client.terminate();
+ }
+
+ const server = this._server;
+
+ if (server) {
+ this._removeListeners();
+ this._removeListeners = this._server = null;
+
+ //
+ // Close the http server if it was internally created.
+ //
+ if (this.options.port != null) {
+ server.close(emitClose.bind(undefined, this));
+ return;
+ }
+ }
+
+ process.nextTick(emitClose, this);
+ }
+
+ /**
+ * See if a given request should be handled by this server instance.
+ *
+ * @param {http.IncomingMessage} req Request object to inspect
+ * @return {Boolean} `true` if the request is valid, else `false`
+ * @public
+ */
+ shouldHandle(req) {
+ if (this.options.path) {
+ const index = req.url.indexOf('?');
+ const pathname = index !== -1 ? req.url.slice(0, index) : req.url;
+
+ if (pathname !== this.options.path) return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Handle a HTTP Upgrade request.
+ *
+ * @param {http.IncomingMessage} req The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @public
+ */
+ handleUpgrade(req, socket, head, cb) {
+ socket.on('error', socketOnError);
+
+ const key =
+ req.headers['sec-websocket-key'] !== undefined
+ ? req.headers['sec-websocket-key'].trim()
+ : false;
+ const version = +req.headers['sec-websocket-version'];
+ const extensions = {};
+
+ if (
+ req.method !== 'GET' ||
+ req.headers.upgrade.toLowerCase() !== 'websocket' ||
+ !key ||
+ !keyRegex.test(key) ||
+ (version !== 8 && version !== 13) ||
+ !this.shouldHandle(req)
+ ) {
+ return abortHandshake(socket, 400);
+ }
+
+ if (this.options.perMessageDeflate) {
+ const perMessageDeflate = new PerMessageDeflate(
+ this.options.perMessageDeflate,
+ true,
+ this.options.maxPayload
+ );
+
+ try {
+ const offers = parse(req.headers['sec-websocket-extensions']);
+
+ if (offers[PerMessageDeflate.extensionName]) {
+ perMessageDeflate.accept(offers[PerMessageDeflate.extensionName]);
+ extensions[PerMessageDeflate.extensionName] = perMessageDeflate;
+ }
+ } catch (err) {
+ return abortHandshake(socket, 400);
+ }
+ }
+
+ //
+ // Optionally call external client verification handler.
+ //
+ if (this.options.verifyClient) {
+ const info = {
+ origin:
+ req.headers[`${version === 8 ? 'sec-websocket-origin' : 'origin'}`],
+ secure: !!(req.socket.authorized || req.socket.encrypted),
+ req
+ };
+
+ if (this.options.verifyClient.length === 2) {
+ this.options.verifyClient(info, (verified, code, message, headers) => {
+ if (!verified) {
+ return abortHandshake(socket, code || 401, message, headers);
+ }
+
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
+ });
+ return;
+ }
+
+ if (!this.options.verifyClient(info)) return abortHandshake(socket, 401);
+ }
+
+ this.completeUpgrade(key, extensions, req, socket, head, cb);
+ }
+
+ /**
+ * Upgrade the connection to WebSocket.
+ *
+ * @param {String} key The value of the `Sec-WebSocket-Key` header
+ * @param {Object} extensions The accepted extensions
+ * @param {http.IncomingMessage} req The request object
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Function} cb Callback
+ * @throws {Error} If called more than once with the same socket
+ * @private
+ */
+ completeUpgrade(key, extensions, req, socket, head, cb) {
+ //
+ // Destroy the socket if the client has already sent a FIN packet.
+ //
+ if (!socket.readable || !socket.writable) return socket.destroy();
+
+ if (socket[kWebSocket]) {
+ throw new Error(
+ 'server.handleUpgrade() was called more than once with the same ' +
+ 'socket, possibly due to a misconfiguration'
+ );
+ }
+
+ if (this._state > RUNNING) return abortHandshake(socket, 503);
+
+ const digest = createHash('sha1')
+ .update(key + GUID)
+ .digest('base64');
+
+ const headers = [
+ 'HTTP/1.1 101 Switching Protocols',
+ 'Upgrade: websocket',
+ 'Connection: Upgrade',
+ `Sec-WebSocket-Accept: ${digest}`
+ ];
+
+ const ws = new WebSocket(null);
+ let protocol = req.headers['sec-websocket-protocol'];
+
+ if (protocol) {
+ protocol = protocol.split(',').map(trim);
+
+ //
+ // Optionally call external protocol selection handler.
+ //
+ if (this.options.handleProtocols) {
+ protocol = this.options.handleProtocols(protocol, req);
+ } else {
+ protocol = protocol[0];
+ }
+
+ if (protocol) {
+ headers.push(`Sec-WebSocket-Protocol: ${protocol}`);
+ ws._protocol = protocol;
+ }
+ }
+
+ if (extensions[PerMessageDeflate.extensionName]) {
+ const params = extensions[PerMessageDeflate.extensionName].params;
+ const value = format({
+ [PerMessageDeflate.extensionName]: [params]
+ });
+ headers.push(`Sec-WebSocket-Extensions: ${value}`);
+ ws._extensions = extensions;
+ }
+
+ //
+ // Allow external modification/inspection of handshake headers.
+ //
+ this.emit('headers', headers, req);
+
+ socket.write(headers.concat('\r\n').join('\r\n'));
+ socket.removeListener('error', socketOnError);
+
+ ws.setSocket(socket, head, this.options.maxPayload);
+
+ if (this.clients) {
+ this.clients.add(ws);
+ ws.on('close', () => this.clients.delete(ws));
+ }
+
+ cb(ws, req);
+ }
+}
+
+module.exports = WebSocketServer;
+
+/**
+ * Add event listeners on an `EventEmitter` using a map of <event, listener>
+ * pairs.
+ *
+ * @param {EventEmitter} server The event emitter
+ * @param {Object.<String, Function>} map The listeners to add
+ * @return {Function} A function that will remove the added listeners when
+ * called
+ * @private
+ */
+function addListeners(server, map) {
+ for (const event of Object.keys(map)) server.on(event, map[event]);
+
+ return function removeListeners() {
+ for (const event of Object.keys(map)) {
+ server.removeListener(event, map[event]);
+ }
+ };
+}
+
+/**
+ * Emit a `'close'` event on an `EventEmitter`.
+ *
+ * @param {EventEmitter} server The event emitter
+ * @private
+ */
+function emitClose(server) {
+ server._state = CLOSED;
+ server.emit('close');
+}
+
+/**
+ * Handle premature socket errors.
+ *
+ * @private
+ */
+function socketOnError() {
+ this.destroy();
+}
+
+/**
+ * Close the connection when preconditions are not fulfilled.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The socket of the upgrade request
+ * @param {Number} code The HTTP response status code
+ * @param {String} [message] The HTTP response body
+ * @param {Object} [headers] Additional HTTP response headers
+ * @private
+ */
+function abortHandshake(socket, code, message, headers) {
+ if (socket.writable) {
+ message = message || http.STATUS_CODES[code];
+ headers = {
+ Connection: 'close',
+ 'Content-Type': 'text/html',
+ 'Content-Length': Buffer.byteLength(message),
+ ...headers
+ };
+
+ socket.write(
+ `HTTP/1.1 ${code} ${http.STATUS_CODES[code]}\r\n` +
+ Object.keys(headers)
+ .map((h) => `${h}: ${headers[h]}`)
+ .join('\r\n') +
+ '\r\n\r\n' +
+ message
+ );
+ }
+
+ socket.removeListener('error', socketOnError);
+ socket.destroy();
+}
+
+/**
+ * Remove whitespace characters from both ends of a string.
+ *
+ * @param {String} str The string
+ * @return {String} A new string representing `str` stripped of whitespace
+ * characters from both its beginning and end
+ * @private
+ */
+function trim(str) {
+ return str.trim();
+}
diff --git a/node_modules/ws/lib/websocket.js b/node_modules/ws/lib/websocket.js
new file mode 100644
index 0000000..1df8967
--- /dev/null
+++ b/node_modules/ws/lib/websocket.js
@@ -0,0 +1,1195 @@
+/* eslint no-unused-vars: ["error", { "varsIgnorePattern": "^Readable$" }] */
+
+'use strict';
+
+const EventEmitter = require('events');
+const https = require('https');
+const http = require('http');
+const net = require('net');
+const tls = require('tls');
+const { randomBytes, createHash } = require('crypto');
+const { Readable } = require('stream');
+const { URL } = require('url');
+
+const PerMessageDeflate = require('./permessage-deflate');
+const Receiver = require('./receiver');
+const Sender = require('./sender');
+const {
+ BINARY_TYPES,
+ EMPTY_BUFFER,
+ GUID,
+ kStatusCode,
+ kWebSocket,
+ NOOP
+} = require('./constants');
+const { addEventListener, removeEventListener } = require('./event-target');
+const { format, parse } = require('./extension');
+const { toBuffer } = require('./buffer-util');
+
+const readyStates = ['CONNECTING', 'OPEN', 'CLOSING', 'CLOSED'];
+const protocolVersions = [8, 13];
+const closeTimeout = 30 * 1000;
+
+/**
+ * Class representing a WebSocket.
+ *
+ * @extends EventEmitter
+ */
+class WebSocket extends EventEmitter {
+ /**
+ * Create a new `WebSocket`.
+ *
+ * @param {(String|URL)} address The URL to which to connect
+ * @param {(String|String[])} [protocols] The subprotocols
+ * @param {Object} [options] Connection options
+ */
+ constructor(address, protocols, options) {
+ super();
+
+ this._binaryType = BINARY_TYPES[0];
+ this._closeCode = 1006;
+ this._closeFrameReceived = false;
+ this._closeFrameSent = false;
+ this._closeMessage = '';
+ this._closeTimer = null;
+ this._extensions = {};
+ this._protocol = '';
+ this._readyState = WebSocket.CONNECTING;
+ this._receiver = null;
+ this._sender = null;
+ this._socket = null;
+
+ if (address !== null) {
+ this._bufferedAmount = 0;
+ this._isServer = false;
+ this._redirects = 0;
+
+ if (Array.isArray(protocols)) {
+ protocols = protocols.join(', ');
+ } else if (typeof protocols === 'object' && protocols !== null) {
+ options = protocols;
+ protocols = undefined;
+ }
+
+ initAsClient(this, address, protocols, options);
+ } else {
+ this._isServer = true;
+ }
+ }
+
+ /**
+ * This deviates from the WHATWG interface since ws doesn't support the
+ * required default "blob" type (instead we define a custom "nodebuffer"
+ * type).
+ *
+ * @type {String}
+ */
+ get binaryType() {
+ return this._binaryType;
+ }
+
+ set binaryType(type) {
+ if (!BINARY_TYPES.includes(type)) return;
+
+ this._binaryType = type;
+
+ //
+ // Allow to change `binaryType` on the fly.
+ //
+ if (this._receiver) this._receiver._binaryType = type;
+ }
+
+ /**
+ * @type {Number}
+ */
+ get bufferedAmount() {
+ if (!this._socket) return this._bufferedAmount;
+
+ return this._socket._writableState.length + this._sender._bufferedBytes;
+ }
+
+ /**
+ * @type {String}
+ */
+ get extensions() {
+ return Object.keys(this._extensions).join();
+ }
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onclose() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onclose(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onerror() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onerror(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onopen() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onopen(listener) {}
+
+ /**
+ * @type {Function}
+ */
+ /* istanbul ignore next */
+ get onmessage() {
+ return undefined;
+ }
+
+ /* istanbul ignore next */
+ set onmessage(listener) {}
+
+ /**
+ * @type {String}
+ */
+ get protocol() {
+ return this._protocol;
+ }
+
+ /**
+ * @type {Number}
+ */
+ get readyState() {
+ return this._readyState;
+ }
+
+ /**
+ * @type {String}
+ */
+ get url() {
+ return this._url;
+ }
+
+ /**
+ * Set up the socket and the internal resources.
+ *
+ * @param {(net.Socket|tls.Socket)} socket The network socket between the
+ * server and client
+ * @param {Buffer} head The first packet of the upgraded stream
+ * @param {Number} [maxPayload=0] The maximum allowed message size
+ * @private
+ */
+ setSocket(socket, head, maxPayload) {
+ const receiver = new Receiver(
+ this.binaryType,
+ this._extensions,
+ this._isServer,
+ maxPayload
+ );
+
+ this._sender = new Sender(socket, this._extensions);
+ this._receiver = receiver;
+ this._socket = socket;
+
+ receiver[kWebSocket] = this;
+ socket[kWebSocket] = this;
+
+ receiver.on('conclude', receiverOnConclude);
+ receiver.on('drain', receiverOnDrain);
+ receiver.on('error', receiverOnError);
+ receiver.on('message', receiverOnMessage);
+ receiver.on('ping', receiverOnPing);
+ receiver.on('pong', receiverOnPong);
+
+ socket.setTimeout(0);
+ socket.setNoDelay();
+
+ if (head.length > 0) socket.unshift(head);
+
+ socket.on('close', socketOnClose);
+ socket.on('data', socketOnData);
+ socket.on('end', socketOnEnd);
+ socket.on('error', socketOnError);
+
+ this._readyState = WebSocket.OPEN;
+ this.emit('open');
+ }
+
+ /**
+ * Emit the `'close'` event.
+ *
+ * @private
+ */
+ emitClose() {
+ if (!this._socket) {
+ this._readyState = WebSocket.CLOSED;
+ this.emit('close', this._closeCode, this._closeMessage);
+ return;
+ }
+
+ if (this._extensions[PerMessageDeflate.extensionName]) {
+ this._extensions[PerMessageDeflate.extensionName].cleanup();
+ }
+
+ this._receiver.removeAllListeners();
+ this._readyState = WebSocket.CLOSED;
+ this.emit('close', this._closeCode, this._closeMessage);
+ }
+
+ /**
+ * Start a closing handshake.
+ *
+ * +----------+ +-----------+ +----------+
+ * - - -|ws.close()|-->|close frame|-->|ws.close()|- - -
+ * | +----------+ +-----------+ +----------+ |
+ * +----------+ +-----------+ |
+ * CLOSING |ws.close()|<--|close frame|<--+-----+ CLOSING
+ * +----------+ +-----------+ |
+ * | | | +---+ |
+ * +------------------------+-->|fin| - - - -
+ * | +---+ | +---+
+ * - - - - -|fin|<---------------------+
+ * +---+
+ *
+ * @param {Number} [code] Status code explaining why the connection is closing
+ * @param {String} [data] A string explaining why the connection is closing
+ * @public
+ */
+ close(code, data) {
+ if (this.readyState === WebSocket.CLOSED) return;
+ if (this.readyState === WebSocket.CONNECTING) {
+ const msg = 'WebSocket was closed before the connection was established';
+ return abortHandshake(this, this._req, msg);
+ }
+
+ if (this.readyState === WebSocket.CLOSING) {
+ if (
+ this._closeFrameSent &&
+ (this._closeFrameReceived || this._receiver._writableState.errorEmitted)
+ ) {
+ this._socket.end();
+ }
+
+ return;
+ }
+
+ this._readyState = WebSocket.CLOSING;
+ this._sender.close(code, data, !this._isServer, (err) => {
+ //
+ // This error is handled by the `'error'` listener on the socket. We only
+ // want to know if the close frame has been sent here.
+ //
+ if (err) return;
+
+ this._closeFrameSent = true;
+
+ if (
+ this._closeFrameReceived ||
+ this._receiver._writableState.errorEmitted
+ ) {
+ this._socket.end();
+ }
+ });
+
+ //
+ // Specify a timeout for the closing handshake to complete.
+ //
+ this._closeTimer = setTimeout(
+ this._socket.destroy.bind(this._socket),
+ closeTimeout
+ );
+ }
+
+ /**
+ * Send a ping.
+ *
+ * @param {*} [data] The data to send
+ * @param {Boolean} [mask] Indicates whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when the ping is sent
+ * @public
+ */
+ ping(data, mask, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof data === 'function') {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === 'function') {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.ping(data || EMPTY_BUFFER, mask, cb);
+ }
+
+ /**
+ * Send a pong.
+ *
+ * @param {*} [data] The data to send
+ * @param {Boolean} [mask] Indicates whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when the pong is sent
+ * @public
+ */
+ pong(data, mask, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof data === 'function') {
+ cb = data;
+ data = mask = undefined;
+ } else if (typeof mask === 'function') {
+ cb = mask;
+ mask = undefined;
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ if (mask === undefined) mask = !this._isServer;
+ this._sender.pong(data || EMPTY_BUFFER, mask, cb);
+ }
+
+ /**
+ * Send a data message.
+ *
+ * @param {*} data The message to send
+ * @param {Object} [options] Options object
+ * @param {Boolean} [options.compress] Specifies whether or not to compress
+ * `data`
+ * @param {Boolean} [options.binary] Specifies whether `data` is binary or
+ * text
+ * @param {Boolean} [options.fin=true] Specifies whether the fragment is the
+ * last one
+ * @param {Boolean} [options.mask] Specifies whether or not to mask `data`
+ * @param {Function} [cb] Callback which is executed when data is written out
+ * @public
+ */
+ send(data, options, cb) {
+ if (this.readyState === WebSocket.CONNECTING) {
+ throw new Error('WebSocket is not open: readyState 0 (CONNECTING)');
+ }
+
+ if (typeof options === 'function') {
+ cb = options;
+ options = {};
+ }
+
+ if (typeof data === 'number') data = data.toString();
+
+ if (this.readyState !== WebSocket.OPEN) {
+ sendAfterClose(this, data, cb);
+ return;
+ }
+
+ const opts = {
+ binary: typeof data !== 'string',
+ mask: !this._isServer,
+ compress: true,
+ fin: true,
+ ...options
+ };
+
+ if (!this._extensions[PerMessageDeflate.extensionName]) {
+ opts.compress = false;
+ }
+
+ this._sender.send(data || EMPTY_BUFFER, opts, cb);
+ }
+
+ /**
+ * Forcibly close the connection.
+ *
+ * @public
+ */
+ terminate() {
+ if (this.readyState === WebSocket.CLOSED) return;
+ if (this.readyState === WebSocket.CONNECTING) {
+ const msg = 'WebSocket was closed before the connection was established';
+ return abortHandshake(this, this._req, msg);
+ }
+
+ if (this._socket) {
+ this._readyState = WebSocket.CLOSING;
+ this._socket.destroy();
+ }
+ }
+}
+
+/**
+ * @constant {Number} CONNECTING
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CONNECTING', {
+ enumerable: true,
+ value: readyStates.indexOf('CONNECTING')
+});
+
+/**
+ * @constant {Number} CONNECTING
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CONNECTING', {
+ enumerable: true,
+ value: readyStates.indexOf('CONNECTING')
+});
+
+/**
+ * @constant {Number} OPEN
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'OPEN', {
+ enumerable: true,
+ value: readyStates.indexOf('OPEN')
+});
+
+/**
+ * @constant {Number} OPEN
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'OPEN', {
+ enumerable: true,
+ value: readyStates.indexOf('OPEN')
+});
+
+/**
+ * @constant {Number} CLOSING
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CLOSING', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSING')
+});
+
+/**
+ * @constant {Number} CLOSING
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CLOSING', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSING')
+});
+
+/**
+ * @constant {Number} CLOSED
+ * @memberof WebSocket
+ */
+Object.defineProperty(WebSocket, 'CLOSED', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSED')
+});
+
+/**
+ * @constant {Number} CLOSED
+ * @memberof WebSocket.prototype
+ */
+Object.defineProperty(WebSocket.prototype, 'CLOSED', {
+ enumerable: true,
+ value: readyStates.indexOf('CLOSED')
+});
+
+[
+ 'binaryType',
+ 'bufferedAmount',
+ 'extensions',
+ 'protocol',
+ 'readyState',
+ 'url'
+].forEach((property) => {
+ Object.defineProperty(WebSocket.prototype, property, { enumerable: true });
+});
+
+//
+// Add the `onopen`, `onerror`, `onclose`, and `onmessage` attributes.
+// See https://html.spec.whatwg.org/multipage/comms.html#the-websocket-interface
+//
+['open', 'error', 'close', 'message'].forEach((method) => {
+ Object.defineProperty(WebSocket.prototype, `on${method}`, {
+ enumerable: true,
+ get() {
+ const listeners = this.listeners(method);
+ for (let i = 0; i < listeners.length; i++) {
+ if (listeners[i]._listener) return listeners[i]._listener;
+ }
+
+ return undefined;
+ },
+ set(listener) {
+ const listeners = this.listeners(method);
+ for (let i = 0; i < listeners.length; i++) {
+ //
+ // Remove only the listeners added via `addEventListener`.
+ //
+ if (listeners[i]._listener) this.removeListener(method, listeners[i]);
+ }
+ this.addEventListener(method, listener);
+ }
+ });
+});
+
+WebSocket.prototype.addEventListener = addEventListener;
+WebSocket.prototype.removeEventListener = removeEventListener;
+
+module.exports = WebSocket;
+
+/**
+ * Initialize a WebSocket client.
+ *
+ * @param {WebSocket} websocket The client to initialize
+ * @param {(String|URL)} address The URL to which to connect
+ * @param {String} [protocols] The subprotocols
+ * @param {Object} [options] Connection options
+ * @param {(Boolean|Object)} [options.perMessageDeflate=true] Enable/disable
+ * permessage-deflate
+ * @param {Number} [options.handshakeTimeout] Timeout in milliseconds for the
+ * handshake request
+ * @param {Number} [options.protocolVersion=13] Value of the
+ * `Sec-WebSocket-Version` header
+ * @param {String} [options.origin] Value of the `Origin` or
+ * `Sec-WebSocket-Origin` header
+ * @param {Number} [options.maxPayload=104857600] The maximum allowed message
+ * size
+ * @param {Boolean} [options.followRedirects=false] Whether or not to follow
+ * redirects
+ * @param {Number} [options.maxRedirects=10] The maximum number of redirects
+ * allowed
+ * @private
+ */
+function initAsClient(websocket, address, protocols, options) {
+ const opts = {
+ protocolVersion: protocolVersions[1],
+ maxPayload: 100 * 1024 * 1024,
+ perMessageDeflate: true,
+ followRedirects: false,
+ maxRedirects: 10,
+ ...options,
+ createConnection: undefined,
+ socketPath: undefined,
+ hostname: undefined,
+ protocol: undefined,
+ timeout: undefined,
+ method: undefined,
+ host: undefined,
+ path: undefined,
+ port: undefined
+ };
+
+ if (!protocolVersions.includes(opts.protocolVersion)) {
+ throw new RangeError(
+ `Unsupported protocol version: ${opts.protocolVersion} ` +
+ `(supported versions: ${protocolVersions.join(', ')})`
+ );
+ }
+
+ let parsedUrl;
+
+ if (address instanceof URL) {
+ parsedUrl = address;
+ websocket._url = address.href;
+ } else {
+ parsedUrl = new URL(address);
+ websocket._url = address;
+ }
+
+ const isUnixSocket = parsedUrl.protocol === 'ws+unix:';
+
+ if (!parsedUrl.host && (!isUnixSocket || !parsedUrl.pathname)) {
+ const err = new Error(`Invalid URL: ${websocket.url}`);
+
+ if (websocket._redirects === 0) {
+ throw err;
+ } else {
+ emitErrorAndClose(websocket, err);
+ return;
+ }
+ }
+
+ const isSecure =
+ parsedUrl.protocol === 'wss:' || parsedUrl.protocol === 'https:';
+ const defaultPort = isSecure ? 443 : 80;
+ const key = randomBytes(16).toString('base64');
+ const get = isSecure ? https.get : http.get;
+ let perMessageDeflate;
+
+ opts.createConnection = isSecure ? tlsConnect : netConnect;
+ opts.defaultPort = opts.defaultPort || defaultPort;
+ opts.port = parsedUrl.port || defaultPort;
+ opts.host = parsedUrl.hostname.startsWith('[')
+ ? parsedUrl.hostname.slice(1, -1)
+ : parsedUrl.hostname;
+ opts.headers = {
+ 'Sec-WebSocket-Version': opts.protocolVersion,
+ 'Sec-WebSocket-Key': key,
+ Connection: 'Upgrade',
+ Upgrade: 'websocket',
+ ...opts.headers
+ };
+ opts.path = parsedUrl.pathname + parsedUrl.search;
+ opts.timeout = opts.handshakeTimeout;
+
+ if (opts.perMessageDeflate) {
+ perMessageDeflate = new PerMessageDeflate(
+ opts.perMessageDeflate !== true ? opts.perMessageDeflate : {},
+ false,
+ opts.maxPayload
+ );
+ opts.headers['Sec-WebSocket-Extensions'] = format({
+ [PerMessageDeflate.extensionName]: perMessageDeflate.offer()
+ });
+ }
+ if (protocols) {
+ opts.headers['Sec-WebSocket-Protocol'] = protocols;
+ }
+ if (opts.origin) {
+ if (opts.protocolVersion < 13) {
+ opts.headers['Sec-WebSocket-Origin'] = opts.origin;
+ } else {
+ opts.headers.Origin = opts.origin;
+ }
+ }
+ if (parsedUrl.username || parsedUrl.password) {
+ opts.auth = `${parsedUrl.username}:${parsedUrl.password}`;
+ }
+
+ if (isUnixSocket) {
+ const parts = opts.path.split(':');
+
+ opts.socketPath = parts[0];
+ opts.path = parts[1];
+ }
+
+ if (opts.followRedirects) {
+ if (websocket._redirects === 0) {
+ websocket._originalUnixSocket = isUnixSocket;
+ websocket._originalSecure = isSecure;
+ websocket._originalHostOrSocketPath = isUnixSocket
+ ? opts.socketPath
+ : parsedUrl.host;
+
+ const headers = options && options.headers;
+
+ //
+ // Shallow copy the user provided options so that headers can be changed
+ // without mutating the original object.
+ //
+ options = { ...options, headers: {} };
+
+ if (headers) {
+ for (const [key, value] of Object.entries(headers)) {
+ options.headers[key.toLowerCase()] = value;
+ }
+ }
+ } else {
+ const isSameHost = isUnixSocket
+ ? websocket._originalUnixSocket
+ ? opts.socketPath === websocket._originalHostOrSocketPath
+ : false
+ : websocket._originalUnixSocket
+ ? false
+ : parsedUrl.host === websocket._originalHostOrSocketPath;
+
+ if (!isSameHost || (websocket._originalSecure && !isSecure)) {
+ //
+ // Match curl 7.77.0 behavior and drop the following headers. These
+ // headers are also dropped when following a redirect to a subdomain.
+ //
+ delete opts.headers.authorization;
+ delete opts.headers.cookie;
+
+ if (!isSameHost) delete opts.headers.host;
+
+ opts.auth = undefined;
+ }
+ }
+
+ //
+ // Match curl 7.77.0 behavior and make the first `Authorization` header win.
+ // If the `Authorization` header is set, then there is nothing to do as it
+ // will take precedence.
+ //
+ if (opts.auth && !options.headers.authorization) {
+ options.headers.authorization =
+ 'Basic ' + Buffer.from(opts.auth).toString('base64');
+ }
+ }
+
+ let req = (websocket._req = get(opts));
+
+ if (opts.timeout) {
+ req.on('timeout', () => {
+ abortHandshake(websocket, req, 'Opening handshake has timed out');
+ });
+ }
+
+ req.on('error', (err) => {
+ if (req === null || req.aborted) return;
+
+ req = websocket._req = null;
+ emitErrorAndClose(websocket, err);
+ });
+
+ req.on('response', (res) => {
+ const location = res.headers.location;
+ const statusCode = res.statusCode;
+
+ if (
+ location &&
+ opts.followRedirects &&
+ statusCode >= 300 &&
+ statusCode < 400
+ ) {
+ if (++websocket._redirects > opts.maxRedirects) {
+ abortHandshake(websocket, req, 'Maximum redirects exceeded');
+ return;
+ }
+
+ req.abort();
+
+ let addr;
+
+ try {
+ addr = new URL(location, address);
+ } catch (err) {
+ emitErrorAndClose(websocket, err);
+ return;
+ }
+
+ initAsClient(websocket, addr, protocols, options);
+ } else if (!websocket.emit('unexpected-response', req, res)) {
+ abortHandshake(
+ websocket,
+ req,
+ `Unexpected server response: ${res.statusCode}`
+ );
+ }
+ });
+
+ req.on('upgrade', (res, socket, head) => {
+ websocket.emit('upgrade', res);
+
+ //
+ // The user may have closed the connection from a listener of the `upgrade`
+ // event.
+ //
+ if (websocket.readyState !== WebSocket.CONNECTING) return;
+
+ req = websocket._req = null;
+
+ if (res.headers.upgrade.toLowerCase() !== 'websocket') {
+ abortHandshake(websocket, socket, 'Invalid Upgrade header');
+ return;
+ }
+
+ const digest = createHash('sha1')
+ .update(key + GUID)
+ .digest('base64');
+
+ if (res.headers['sec-websocket-accept'] !== digest) {
+ abortHandshake(websocket, socket, 'Invalid Sec-WebSocket-Accept header');
+ return;
+ }
+
+ const serverProt = res.headers['sec-websocket-protocol'];
+ const protList = (protocols || '').split(/, */);
+ let protError;
+
+ if (!protocols && serverProt) {
+ protError = 'Server sent a subprotocol but none was requested';
+ } else if (protocols && !serverProt) {
+ protError = 'Server sent no subprotocol';
+ } else if (serverProt && !protList.includes(serverProt)) {
+ protError = 'Server sent an invalid subprotocol';
+ }
+
+ if (protError) {
+ abortHandshake(websocket, socket, protError);
+ return;
+ }
+
+ if (serverProt) websocket._protocol = serverProt;
+
+ const secWebSocketExtensions = res.headers['sec-websocket-extensions'];
+
+ if (secWebSocketExtensions !== undefined) {
+ if (!perMessageDeflate) {
+ const message =
+ 'Server sent a Sec-WebSocket-Extensions header but no extension ' +
+ 'was requested';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ let extensions;
+
+ try {
+ extensions = parse(secWebSocketExtensions);
+ } catch (err) {
+ const message = 'Invalid Sec-WebSocket-Extensions header';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ const extensionNames = Object.keys(extensions);
+
+ if (extensionNames.length) {
+ if (
+ extensionNames.length !== 1 ||
+ extensionNames[0] !== PerMessageDeflate.extensionName
+ ) {
+ const message =
+ 'Server indicated an extension that was not requested';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ try {
+ perMessageDeflate.accept(extensions[PerMessageDeflate.extensionName]);
+ } catch (err) {
+ const message = 'Invalid Sec-WebSocket-Extensions header';
+ abortHandshake(websocket, socket, message);
+ return;
+ }
+
+ websocket._extensions[PerMessageDeflate.extensionName] =
+ perMessageDeflate;
+ }
+ }
+
+ websocket.setSocket(socket, head, opts.maxPayload);
+ });
+}
+
+/**
+ * Emit the `'error'` and `'close'` event.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {Error} The error to emit
+ * @private
+ */
+function emitErrorAndClose(websocket, err) {
+ websocket._readyState = WebSocket.CLOSING;
+ websocket.emit('error', err);
+ websocket.emitClose();
+}
+
+/**
+ * Create a `net.Socket` and initiate a connection.
+ *
+ * @param {Object} options Connection options
+ * @return {net.Socket} The newly created socket used to start the connection
+ * @private
+ */
+function netConnect(options) {
+ options.path = options.socketPath;
+ return net.connect(options);
+}
+
+/**
+ * Create a `tls.TLSSocket` and initiate a connection.
+ *
+ * @param {Object} options Connection options
+ * @return {tls.TLSSocket} The newly created socket used to start the connection
+ * @private
+ */
+function tlsConnect(options) {
+ options.path = undefined;
+
+ if (!options.servername && options.servername !== '') {
+ options.servername = net.isIP(options.host) ? '' : options.host;
+ }
+
+ return tls.connect(options);
+}
+
+/**
+ * Abort the handshake and emit an error.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {(http.ClientRequest|net.Socket|tls.Socket)} stream The request to
+ * abort or the socket to destroy
+ * @param {String} message The error message
+ * @private
+ */
+function abortHandshake(websocket, stream, message) {
+ websocket._readyState = WebSocket.CLOSING;
+
+ const err = new Error(message);
+ Error.captureStackTrace(err, abortHandshake);
+
+ if (stream.setHeader) {
+ stream.abort();
+
+ if (stream.socket && !stream.socket.destroyed) {
+ //
+ // On Node.js >= 14.3.0 `request.abort()` does not destroy the socket if
+ // called after the request completed. See
+ // https://github.com/websockets/ws/issues/1869.
+ //
+ stream.socket.destroy();
+ }
+
+ stream.once('abort', websocket.emitClose.bind(websocket));
+ websocket.emit('error', err);
+ } else {
+ stream.destroy(err);
+ stream.once('error', websocket.emit.bind(websocket, 'error'));
+ stream.once('close', websocket.emitClose.bind(websocket));
+ }
+}
+
+/**
+ * Handle cases where the `ping()`, `pong()`, or `send()` methods are called
+ * when the `readyState` attribute is `CLOSING` or `CLOSED`.
+ *
+ * @param {WebSocket} websocket The WebSocket instance
+ * @param {*} [data] The data to send
+ * @param {Function} [cb] Callback
+ * @private
+ */
+function sendAfterClose(websocket, data, cb) {
+ if (data) {
+ const length = toBuffer(data).length;
+
+ //
+ // The `_bufferedAmount` property is used only when the peer is a client and
+ // the opening handshake fails. Under these circumstances, in fact, the
+ // `setSocket()` method is not called, so the `_socket` and `_sender`
+ // properties are set to `null`.
+ //
+ if (websocket._socket) websocket._sender._bufferedBytes += length;
+ else websocket._bufferedAmount += length;
+ }
+
+ if (cb) {
+ const err = new Error(
+ `WebSocket is not open: readyState ${websocket.readyState} ` +
+ `(${readyStates[websocket.readyState]})`
+ );
+ cb(err);
+ }
+}
+
+/**
+ * The listener of the `Receiver` `'conclude'` event.
+ *
+ * @param {Number} code The status code
+ * @param {String} reason The reason for closing
+ * @private
+ */
+function receiverOnConclude(code, reason) {
+ const websocket = this[kWebSocket];
+
+ websocket._closeFrameReceived = true;
+ websocket._closeMessage = reason;
+ websocket._closeCode = code;
+
+ if (websocket._socket[kWebSocket] === undefined) return;
+
+ websocket._socket.removeListener('data', socketOnData);
+ process.nextTick(resume, websocket._socket);
+
+ if (code === 1005) websocket.close();
+ else websocket.close(code, reason);
+}
+
+/**
+ * The listener of the `Receiver` `'drain'` event.
+ *
+ * @private
+ */
+function receiverOnDrain() {
+ this[kWebSocket]._socket.resume();
+}
+
+/**
+ * The listener of the `Receiver` `'error'` event.
+ *
+ * @param {(RangeError|Error)} err The emitted error
+ * @private
+ */
+function receiverOnError(err) {
+ const websocket = this[kWebSocket];
+
+ if (websocket._socket[kWebSocket] !== undefined) {
+ websocket._socket.removeListener('data', socketOnData);
+
+ //
+ // On Node.js < 14.0.0 the `'error'` event is emitted synchronously. See
+ // https://github.com/websockets/ws/issues/1940.
+ //
+ process.nextTick(resume, websocket._socket);
+
+ websocket.close(err[kStatusCode]);
+ }
+
+ websocket.emit('error', err);
+}
+
+/**
+ * The listener of the `Receiver` `'finish'` event.
+ *
+ * @private
+ */
+function receiverOnFinish() {
+ this[kWebSocket].emitClose();
+}
+
+/**
+ * The listener of the `Receiver` `'message'` event.
+ *
+ * @param {(String|Buffer|ArrayBuffer|Buffer[])} data The message
+ * @private
+ */
+function receiverOnMessage(data) {
+ this[kWebSocket].emit('message', data);
+}
+
+/**
+ * The listener of the `Receiver` `'ping'` event.
+ *
+ * @param {Buffer} data The data included in the ping frame
+ * @private
+ */
+function receiverOnPing(data) {
+ const websocket = this[kWebSocket];
+
+ websocket.pong(data, !websocket._isServer, NOOP);
+ websocket.emit('ping', data);
+}
+
+/**
+ * The listener of the `Receiver` `'pong'` event.
+ *
+ * @param {Buffer} data The data included in the pong frame
+ * @private
+ */
+function receiverOnPong(data) {
+ this[kWebSocket].emit('pong', data);
+}
+
+/**
+ * Resume a readable stream
+ *
+ * @param {Readable} stream The readable stream
+ * @private
+ */
+function resume(stream) {
+ stream.resume();
+}
+
+/**
+ * The listener of the `net.Socket` `'close'` event.
+ *
+ * @private
+ */
+function socketOnClose() {
+ const websocket = this[kWebSocket];
+
+ this.removeListener('close', socketOnClose);
+ this.removeListener('data', socketOnData);
+ this.removeListener('end', socketOnEnd);
+
+ websocket._readyState = WebSocket.CLOSING;
+
+ let chunk;
+
+ //
+ // The close frame might not have been received or the `'end'` event emitted,
+ // for example, if the socket was destroyed due to an error. Ensure that the
+ // `receiver` stream is closed after writing any remaining buffered data to
+ // it. If the readable side of the socket is in flowing mode then there is no
+ // buffered data as everything has been already written and `readable.read()`
+ // will return `null`. If instead, the socket is paused, any possible buffered
+ // data will be read as a single chunk.
+ //
+ if (
+ !this._readableState.endEmitted &&
+ !websocket._closeFrameReceived &&
+ !websocket._receiver._writableState.errorEmitted &&
+ (chunk = websocket._socket.read()) !== null
+ ) {
+ websocket._receiver.write(chunk);
+ }
+
+ websocket._receiver.end();
+
+ this[kWebSocket] = undefined;
+
+ clearTimeout(websocket._closeTimer);
+
+ if (
+ websocket._receiver._writableState.finished ||
+ websocket._receiver._writableState.errorEmitted
+ ) {
+ websocket.emitClose();
+ } else {
+ websocket._receiver.on('error', receiverOnFinish);
+ websocket._receiver.on('finish', receiverOnFinish);
+ }
+}
+
+/**
+ * The listener of the `net.Socket` `'data'` event.
+ *
+ * @param {Buffer} chunk A chunk of data
+ * @private
+ */
+function socketOnData(chunk) {
+ if (!this[kWebSocket]._receiver.write(chunk)) {
+ this.pause();
+ }
+}
+
+/**
+ * The listener of the `net.Socket` `'end'` event.
+ *
+ * @private
+ */
+function socketOnEnd() {
+ const websocket = this[kWebSocket];
+
+ websocket._readyState = WebSocket.CLOSING;
+ websocket._receiver.end();
+ this.end();
+}
+
+/**
+ * The listener of the `net.Socket` `'error'` event.
+ *
+ * @private
+ */
+function socketOnError() {
+ const websocket = this[kWebSocket];
+
+ this.removeListener('error', socketOnError);
+ this.on('error', NOOP);
+
+ if (websocket) {
+ websocket._readyState = WebSocket.CLOSING;
+ this.destroy();
+ }
+}
diff --git a/node_modules/ws/package.json b/node_modules/ws/package.json
new file mode 100644
index 0000000..832203f
--- /dev/null
+++ b/node_modules/ws/package.json
@@ -0,0 +1,56 @@
+{
+ "name": "ws",
+ "version": "7.5.9",
+ "description": "Simple to use, blazing fast and thoroughly tested websocket client and server for Node.js",
+ "keywords": [
+ "HyBi",
+ "Push",
+ "RFC-6455",
+ "WebSocket",
+ "WebSockets",
+ "real-time"
+ ],
+ "homepage": "https://github.com/websockets/ws",
+ "bugs": "https://github.com/websockets/ws/issues",
+ "repository": "websockets/ws",
+ "author": "Einar Otto Stangvik <einaros@gmail.com> (http://2x.io)",
+ "license": "MIT",
+ "main": "index.js",
+ "browser": "browser.js",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "files": [
+ "browser.js",
+ "index.js",
+ "lib/*.js"
+ ],
+ "scripts": {
+ "test": "nyc --reporter=lcov --reporter=text mocha --throw-deprecation test/*.test.js",
+ "integration": "mocha --throw-deprecation test/*.integration.js",
+ "lint": "eslint --ignore-path .gitignore . && prettier --check --ignore-path .gitignore \"**/*.{json,md,yaml,yml}\""
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ },
+ "devDependencies": {
+ "benchmark": "^2.1.4",
+ "bufferutil": "^4.0.1",
+ "eslint": "^7.2.0",
+ "eslint-config-prettier": "^8.1.0",
+ "eslint-plugin-prettier": "^4.0.0",
+ "mocha": "^7.0.0",
+ "nyc": "^15.0.0",
+ "prettier": "^2.0.5",
+ "utf-8-validate": "^5.0.2"
+ }
+}
diff --git a/org.cockpit-project.podman.metainfo.xml b/org.cockpit-project.podman.metainfo.xml
new file mode 100644
index 0000000..69054c3
--- /dev/null
+++ b/org.cockpit-project.podman.metainfo.xml
@@ -0,0 +1,16 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<component type="addon">
+ <id>org.cockpit_project.podman</id>
+ <metadata_license>CC0-1.0</metadata_license>
+ <name>Podman</name>
+ <summary>
+ Cockpit component for Podman containers
+ </summary>
+ <description>
+ <p>
+ The Cockpit user interface for Podman containers.
+ </p>
+ </description>
+ <extends>org.cockpit_project.cockpit</extends>
+ <launchable type="cockpit-manifest">podman</launchable>
+</component>
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..5986326
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,5427 @@
+{
+ "name": "podman",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "node_modules/@aashutoshrathi/word-wrap": {
+ "version": "1.2.6",
+ "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz",
+ "integrity": "sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/@babel/code-frame": {
+ "version": "7.23.5",
+ "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz",
+ "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/highlight": "^7.23.4",
+ "chalk": "^2.4.2"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^3.2.1",
+ "escape-string-regexp": "^1.0.5",
+ "supports-color": "^5.3.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-convert": {
+ "version": "1.9.3",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/code-frame/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/code-frame/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.22.20",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz",
+ "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==",
+ "dev": true,
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight": {
+ "version": "7.23.4",
+ "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz",
+ "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==",
+ "dev": true,
+ "dependencies": {
+ "@babel/helper-validator-identifier": "^7.22.20",
+ "chalk": "^2.4.2",
+ "js-tokens": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/ansi-styles": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz",
+ "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==",
+ "dev": true,
+ "dependencies": {
+ "color-convert": "^1.9.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/chalk": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz",
+ "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==",
+ "dev": true,
+ "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",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz",
+ "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==",
+ "dev": true,
+ "dependencies": {
+ "color-name": "1.1.3"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/color-name": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz",
+ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==",
+ "dev": true
+ },
+ "node_modules/@babel/highlight/node_modules/escape-string-regexp": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
+ "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.8.0"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/has-flag": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz",
+ "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/highlight/node_modules/supports-color": {
+ "version": "5.5.0",
+ "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz",
+ "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.24.0",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.0.tgz",
+ "integrity": "sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==",
+ "dev": true,
+ "dependencies": {
+ "regenerator-runtime": "^0.14.0"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@bufbuild/protobuf": {
+ "version": "1.7.2",
+ "resolved": "https://registry.npmjs.org/@bufbuild/protobuf/-/protobuf-1.7.2.tgz",
+ "integrity": "sha512-i5GE2Dk5ekdlK1TR7SugY4LWRrKSfb5T1Qn4unpIMbfxoeGKERKQ59HG3iYewacGD10SR7UzevfPnh6my4tNmQ==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/@csstools/css-parser-algorithms": {
+ "version": "2.6.0",
+ "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-2.6.0.tgz",
+ "integrity": "sha512-YfEHq0eRH98ffb5/EsrrDspVWAuph6gDggAE74ZtjecsmyyWpW768hOyiONa8zwWGbIWYfa2Xp4tRTrpQQ00CQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-tokenizer": "^2.2.3"
+ }
+ },
+ "node_modules/@csstools/css-tokenizer": {
+ "version": "2.2.3",
+ "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-2.2.3.tgz",
+ "integrity": "sha512-pp//EvZ9dUmGuGtG1p+n17gTHEOqu9jO+FiCUjNN3BDmyhdA2Jq9QsVeR7K8/2QCK17HSsioPlTW9ZkzoWb3Lg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ }
+ },
+ "node_modules/@csstools/media-query-list-parser": {
+ "version": "2.1.8",
+ "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-2.1.8.tgz",
+ "integrity": "sha512-DiD3vG5ciNzeuTEoh74S+JMjQDs50R3zlxHnBnfd04YYfA/kh2KiBCGhzqLxlJcNq+7yNQ3stuZZYLX6wK/U2g==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "@csstools/css-parser-algorithms": "^2.6.0",
+ "@csstools/css-tokenizer": "^2.2.3"
+ }
+ },
+ "node_modules/@csstools/selector-specificity": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-3.0.2.tgz",
+ "integrity": "sha512-RpHaZ1h9LE7aALeQXmXrJkRG84ZxIsctEN2biEUmFyKpzFM3zZ35eUMcIzZFsw/2olQE6v69+esEqU2f1MKycg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/csstools"
+ },
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/csstools"
+ }
+ ],
+ "engines": {
+ "node": "^14 || ^16 || >=18"
+ },
+ "peerDependencies": {
+ "postcss-selector-parser": "^6.0.13"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.20.1.tgz",
+ "integrity": "sha512-5gRPk7pKuaIB+tmH+yKd2aQTRpqlf1E4f/mC+tawIm/CGJemZcHZpp2ic8oD83nKgUPMEd0fNanrnFljiruuyA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@eslint-community/eslint-utils": {
+ "version": "4.4.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz",
+ "integrity": "sha512-1/sA4dwrzBAyeUoQ6oxahHKmrZvsnLCg4RfxW3ZFGGmQkSNQPFNLV9CUEFQP1x9EYXHTo5p6xdhZM1Ne9p/AfA==",
+ "dev": true,
+ "dependencies": {
+ "eslint-visitor-keys": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0"
+ }
+ },
+ "node_modules/@eslint-community/regexpp": {
+ "version": "4.10.0",
+ "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.10.0.tgz",
+ "integrity": "sha512-Cu96Sd2By9mCNTx2iyKOmq10v22jUVQv0lQnlGNy16oE9589yE+QADPbrMGCkA51cKZSg3Pu/aTJVTGfL/qjUA==",
+ "dev": true,
+ "engines": {
+ "node": "^12.0.0 || ^14.0.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@eslint/eslintrc": {
+ "version": "2.1.4",
+ "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz",
+ "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^6.12.4",
+ "debug": "^4.3.2",
+ "espree": "^9.6.0",
+ "globals": "^13.19.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": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/@eslint/js": {
+ "version": "8.57.0",
+ "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.0.tgz",
+ "integrity": "sha512-Ys+3g2TaW7gADOJzPt83SJtCDhMjndcDMFVQ/Tj9iA1BfJzFKD9mAUXT3OenpuPHbI6P/myECxRJrofUsDx/5g==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "node_modules/@humanwhocodes/config-array": {
+ "version": "0.11.14",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.11.14.tgz",
+ "integrity": "sha512-3T8LkOmg45BV5FICb15QQMsyUSWrQ8AygVfC7ZG32zOalnqrilm018ZVCw0eapXux8FtA33q8PSRSstjee3jSg==",
+ "dev": true,
+ "dependencies": {
+ "@humanwhocodes/object-schema": "^2.0.2",
+ "debug": "^4.3.1",
+ "minimatch": "^3.0.5"
+ },
+ "engines": {
+ "node": ">=10.10.0"
+ }
+ },
+ "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/object-schema": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.2.tgz",
+ "integrity": "sha512-6EwiSjwWYP7pTckG6I5eyFANjPhmPjUX9JRLUSfNPC7FX7zK9gyZAfUEaECL6ALTpGX5AjnBq3C9XmVWPitNpw==",
+ "dev": true
+ },
+ "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/@patternfly/patternfly": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/patternfly/-/patternfly-5.2.1.tgz",
+ "integrity": "sha512-n5xFjyj1J4eIFZ7XeU6K44POKRAuDlO5yALPbn084y+jPy1j861AaQ+zIUbzCi4IzBlHrvoXVKij7p1zy7Ditg=="
+ },
+ "node_modules/@patternfly/react-core": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-core/-/react-core-5.2.1.tgz",
+ "integrity": "sha512-SWQHALhcjxjmwcIJ6V3tG6V7a2M0WkkUbc6F8mSPk6l9q6j3f+WvZ9HqgzVA+h+Q12UbtIrlQvgUx7pAxZekkg==",
+ "dependencies": {
+ "@patternfly/react-icons": "^5.2.1",
+ "@patternfly/react-styles": "^5.2.1",
+ "@patternfly/react-tokens": "^5.2.1",
+ "focus-trap": "7.5.2",
+ "react-dropzone": "^14.2.3",
+ "tslib": "^2.5.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-icons": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-icons/-/react-icons-5.2.1.tgz",
+ "integrity": "sha512-aeJ0X+U2NDe8UmI5eQiT0iuR/wmUq97UkDtx3HoZcpRb9T6eUBfysllxjRqHS8rOOspdU8OWq+CUhQ/E2ZDibg==",
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-styles": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-styles/-/react-styles-5.2.1.tgz",
+ "integrity": "sha512-GT96hzI1QenBhq6Pfc51kxnj9aVLjL1zSLukKZXcYVe0HPOy0BFm90bT1Fo4e/z7V9cDYw4SqSX1XLc3O4jsTw=="
+ },
+ "node_modules/@patternfly/react-table": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-table/-/react-table-5.2.1.tgz",
+ "integrity": "sha512-Kcuxhh8RjcHBwLMxdnhIAGsHKjh2t5OSC8BvRSaz2hlLCFqsQf73SALjs2w8IHHnzwSZ1fTBo4js2vPPjML3gg==",
+ "dependencies": {
+ "@patternfly/react-core": "^5.2.1",
+ "@patternfly/react-icons": "^5.2.1",
+ "@patternfly/react-styles": "^5.2.1",
+ "@patternfly/react-tokens": "^5.2.1",
+ "lodash": "^4.17.19",
+ "tslib": "^2.5.0"
+ },
+ "peerDependencies": {
+ "react": "^17 || ^18",
+ "react-dom": "^17 || ^18"
+ }
+ },
+ "node_modules/@patternfly/react-tokens": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@patternfly/react-tokens/-/react-tokens-5.2.1.tgz",
+ "integrity": "sha512-8GYz/jnJTGAWUJt5eRAW5dtyiHPKETeFJBPGHaUQnvi/t1ZAkoy8i4Kd/RlHsDC7ktiu813SKCmlzwBwldAHKg=="
+ },
+ "node_modules/@types/json5": {
+ "version": "0.0.29",
+ "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz",
+ "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==",
+ "dev": true
+ },
+ "node_modules/@ungap/structured-clone": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz",
+ "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==",
+ "dev": true
+ },
+ "node_modules/abort-controller": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
+ "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
+ "dev": true,
+ "dependencies": {
+ "event-target-shim": "^5.0.0"
+ },
+ "engines": {
+ "node": ">=6.5"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.11.3",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz",
+ "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==",
+ "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/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/ansi-escapes": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-6.2.0.tgz",
+ "integrity": "sha512-kzRaCqXnpzWs+3z5ABPQiVke+iq0KXkHo8xiWV4RPTi5Yli0l97BEQuhXV1s7+aSU/fu1kUuxgS4MsQ0fRuygw==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/ansi-escapes/node_modules/type-fest": {
+ "version": "3.13.1",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz",
+ "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==",
+ "dev": true,
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "normalize-path": "^3.0.0",
+ "picomatch": "^2.0.4"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "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
+ },
+ "node_modules/aria-query": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz",
+ "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "node_modules/array-buffer-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.1.tgz",
+ "integrity": "sha512-ahC5W1xgou+KTXix4sAO8Ki12Q+jf4i0+tmk3sC+zgcynshkHxzpXdImBehiUYKKKDwvfFiJl1tZt6ewscS1Mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "is-array-buffer": "^3.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-includes": {
+ "version": "3.1.7",
+ "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.7.tgz",
+ "integrity": "sha512-dlcsNBIiWhPkHdOEEKnehA+RNUWDc4UqFtnIXU4uuYDPtA4LDkr7qip2p0VvFAEXNDr0yWZ9PJyIRiGjRLQzwQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "is-string": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array-union": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
+ "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/array.prototype.filter": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.3.tgz",
+ "integrity": "sha512-VizNcj/RGJiUyQBgzwxzE5oHdeuXY5hSbbmKMlphj1cy1Vl7Pn2asCGbSrru6hSQjmCzqTBPVWAF/whmEOVHbw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "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/array.prototype.findlast": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.4.tgz",
+ "integrity": "sha512-BMtLxpV+8BD+6ZPFIWmnUBpQoy+A+ujcg4rhp2iwCRJYA7PEh2MS4NL3lz8EiDlLrJPp2hg9qWihr5pd//jcGw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.findlastindex": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.4.tgz",
+ "integrity": "sha512-hzvSHUshSpCflDR1QMUBLHGHP1VIEBegT4pix9H/Z92Xw3ySoy6c2qh7lJWTJnRJ8JCZ9bJNCgTyYaJGcJu6xQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.3.0",
+ "es-shim-unscopables": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flat": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz",
+ "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.flatmap": {
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz",
+ "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/array.prototype.toreversed": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/array.prototype.toreversed/-/array.prototype.toreversed-1.1.2.tgz",
+ "integrity": "sha512-wwDCoT4Ck4Cz7sLtgUmzR5UV3YF5mFHUlbChCzZBQZ+0m2cl/DH3tKgvphv1nKgFsJ48oCSg6p91q2Vm0I/ZMA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "es-shim-unscopables": "^1.0.0"
+ }
+ },
+ "node_modules/array.prototype.tosorted": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.3.tgz",
+ "integrity": "sha512-/DdH4TiTmOKzyQbp/eadcCVexiCb36xJg7HshYOYJnNZFDj33GEv0P7GxsynpShhq4OLYJzbGcBDkLsDt7MnNg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.1.0",
+ "es-shim-unscopables": "^1.0.2"
+ }
+ },
+ "node_modules/arraybuffer.prototype.slice": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz",
+ "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.2.1",
+ "get-intrinsic": "^1.2.3",
+ "is-array-buffer": "^3.0.4",
+ "is-shared-array-buffer": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/ast-types-flow": {
+ "version": "0.0.8",
+ "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz",
+ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==",
+ "dev": true
+ },
+ "node_modules/astral-regex": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz",
+ "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/asynciterator.prototype": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/asynciterator.prototype/-/asynciterator.prototype-1.0.0.tgz",
+ "integrity": "sha512-wwHYEIS0Q80f5mosx3L/dfG5t5rjEa9Ft51GTaNt862EnpyGHpgz2RkZvLPp1oF5TnAiTohkEKVEu8pQPJI7Vg==",
+ "dev": true,
+ "dependencies": {
+ "has-symbols": "^1.0.3"
+ }
+ },
+ "node_modules/attr-accept": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/attr-accept/-/attr-accept-2.2.2.tgz",
+ "integrity": "sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==",
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/available-typed-arrays": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz",
+ "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==",
+ "dev": true,
+ "dependencies": {
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/axe-core": {
+ "version": "4.7.0",
+ "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.7.0.tgz",
+ "integrity": "sha512-M0JtH+hlOL5pLQwHOLNYZaXuhqmvS8oExsqB1SBYgA4Dk7u/xx+YdGHXaK5pyUfed5mYXdlYiphWq3G8cRi5JQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/axobject-query": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-3.2.1.tgz",
+ "integrity": "sha512-jsyHu61e6N4Vbz/v18DHwWYKK0bSWLqn47eeDSKPB7m8tqMHF9YJ+mhIk2lVteyZrY8tnSj/jHOv4YiTCuCJgg==",
+ "dev": true,
+ "dependencies": {
+ "dequal": "^2.0.3"
+ }
+ },
+ "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==",
+ "dev": true
+ },
+ "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/binary-extensions": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
+ "integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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/braces": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
+ "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
+ "dev": true,
+ "dependencies": {
+ "fill-range": "^7.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "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.2.1"
+ }
+ },
+ "node_modules/buffer-builder": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/buffer-builder/-/buffer-builder-0.2.0.tgz",
+ "integrity": "sha512-7VPMEPuYznPSoR21NE1zvd2Xna6c/CloiZCfcMXR1Jny6PjX0N4Nsa38zcBFo/FMK+BlA+FLKbJCQ0i2yxp+Xg==",
+ "dev": true,
+ "peer": true
+ },
+ "node_modules/builtin-modules": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
+ "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/builtins": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz",
+ "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "semver": "^7.0.0"
+ }
+ },
+ "node_modules/builtins/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/call-bind": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
+ "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "set-function-length": "^1.2.1"
+ },
+ "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,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/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/chrome-remote-interface": {
+ "version": "0.33.0",
+ "resolved": "https://registry.npmjs.org/chrome-remote-interface/-/chrome-remote-interface-0.33.0.tgz",
+ "integrity": "sha512-tv/SgeBfShXk43fwFpQ9wnS7mOCPzETnzDXTNxCb6TqKOiOeIfbrJz+2NAp8GmzwizpKa058wnU1Te7apONaYg==",
+ "dev": true,
+ "dependencies": {
+ "commander": "2.11.x",
+ "ws": "^7.2.0"
+ },
+ "bin": {
+ "chrome-remote-interface": "bin/client.js"
+ }
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true
+ },
+ "node_modules/colord": {
+ "version": "2.9.3",
+ "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz",
+ "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==",
+ "dev": true
+ },
+ "node_modules/commander": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.11.0.tgz",
+ "integrity": "sha512-b0553uYA5YAEGgyYIGYROzKQ7X5RAqedkfjiZxwi0kL1g3bOaBNNZfYkzt/CL0umgD5wc9Jec2FbB98CjkMRvQ==",
+ "dev": true
+ },
+ "node_modules/concat-map": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
+ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
+ "dev": true
+ },
+ "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/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/cross-spawn": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz",
+ "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==",
+ "dev": true,
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/css-functions-list": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.1.tgz",
+ "integrity": "sha512-Nj5YcaGgBtuUmn1D7oHqPW0c9iui7xsTsj5lIX8ZgevdfhmjFfKB3r8moHJtNJnctnYXJyYX5I1pp90HM4TPgQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12 || >=16"
+ }
+ },
+ "node_modules/css-tree": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz",
+ "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==",
+ "dev": true,
+ "dependencies": {
+ "mdn-data": "2.0.30",
+ "source-map-js": "^1.0.1"
+ },
+ "engines": {
+ "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0"
+ }
+ },
+ "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/damerau-levenshtein": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz",
+ "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==",
+ "dev": true
+ },
+ "node_modules/date-fns": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.4.0.tgz",
+ "integrity": "sha512-Akz4R8J9MXBsOgF1QeWeCsbv6pntT5KCPjU0Q9prBxVmWJYPLhwAIsNg3b0QAdr0ttiozYLD3L/af7Ra0jqYXw==",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/kossnocorp"
+ }
+ },
+ "node_modules/debug": {
+ "version": "4.3.4",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
+ "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "2.1.2"
+ },
+ "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/define-data-property": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz",
+ "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "gopd": "^1.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/define-properties": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz",
+ "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.0.1",
+ "has-property-descriptors": "^1.0.0",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/dequal": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
+ "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/dir-glob": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz",
+ "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==",
+ "dev": true,
+ "dependencies": {
+ "path-type": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/docker-names": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/docker-names/-/docker-names-1.2.1.tgz",
+ "integrity": "sha512-uh42tvWBp10fnMyJ9z0YL9kql+iolxEnQ+pGZANj+gcg/N2NxjrdHbMXT2Y2Y07A8Jf7KJp/6LkElPCfukCdWg=="
+ },
+ "node_modules/doctrine": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz",
+ "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "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/encoding": {
+ "version": "0.1.13",
+ "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz",
+ "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==",
+ "dev": true,
+ "dependencies": {
+ "iconv-lite": "^0.6.2"
+ }
+ },
+ "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/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,
+ "dependencies": {
+ "is-arrayish": "^0.2.1"
+ }
+ },
+ "node_modules/es-abstract": {
+ "version": "1.22.5",
+ "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.22.5.tgz",
+ "integrity": "sha512-oW69R+4q2wG+Hc3KZePPZxOiisRIqfKBVo/HLx94QcJeWGU/8sZhCvc829rd1kS366vlJbzBfXf9yWwf0+Ko7w==",
+ "dev": true,
+ "dependencies": {
+ "array-buffer-byte-length": "^1.0.1",
+ "arraybuffer.prototype.slice": "^1.0.3",
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "es-define-property": "^1.0.0",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.3",
+ "es-to-primitive": "^1.2.1",
+ "function.prototype.name": "^1.1.6",
+ "get-intrinsic": "^1.2.4",
+ "get-symbol-description": "^1.0.2",
+ "globalthis": "^1.0.3",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.3",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.1",
+ "internal-slot": "^1.0.7",
+ "is-array-buffer": "^3.0.4",
+ "is-callable": "^1.2.7",
+ "is-negative-zero": "^2.0.3",
+ "is-regex": "^1.1.4",
+ "is-shared-array-buffer": "^1.0.3",
+ "is-string": "^1.0.7",
+ "is-typed-array": "^1.1.13",
+ "is-weakref": "^1.0.2",
+ "object-inspect": "^1.13.1",
+ "object-keys": "^1.1.1",
+ "object.assign": "^4.1.5",
+ "regexp.prototype.flags": "^1.5.2",
+ "safe-array-concat": "^1.1.0",
+ "safe-regex-test": "^1.0.3",
+ "string.prototype.trim": "^1.2.8",
+ "string.prototype.trimend": "^1.0.7",
+ "string.prototype.trimstart": "^1.0.7",
+ "typed-array-buffer": "^1.0.2",
+ "typed-array-byte-length": "^1.0.1",
+ "typed-array-byte-offset": "^1.0.2",
+ "typed-array-length": "^1.0.5",
+ "unbox-primitive": "^1.0.2",
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/es-array-method-boxes-properly": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz",
+ "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==",
+ "dev": true
+ },
+ "node_modules/es-define-property": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz",
+ "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4"
+ },
+ "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-iterator-helpers": {
+ "version": "1.0.17",
+ "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.0.17.tgz",
+ "integrity": "sha512-lh7BsUqelv4KUbR5a/ZTaGGIMLCjPGPqJ6q+Oq24YP0RdyptX1uzm4vvaqzk7Zx3bpl/76YLTTDj9L7uYQ92oQ==",
+ "dev": true,
+ "dependencies": {
+ "asynciterator.prototype": "^1.0.0",
+ "call-bind": "^1.0.7",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.4",
+ "es-errors": "^1.3.0",
+ "es-set-tostringtag": "^2.0.2",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "globalthis": "^1.0.3",
+ "has-property-descriptors": "^1.0.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.7",
+ "iterator.prototype": "^1.1.2",
+ "safe-array-concat": "^1.1.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-set-tostringtag": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.0.3.tgz",
+ "integrity": "sha512-3T8uNMC3OQTHkFUsFq8r/BwAXLHvU/9O9mE0fBc/MY5iq/8H7ncvO947LmYA6ldWw9Uh8Yhf25zu6n7nML5QWQ==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.2.4",
+ "has-tostringtag": "^1.0.2",
+ "hasown": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/es-shim-unscopables": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.0.2.tgz",
+ "integrity": "sha512-J3yBRXCzDu4ULnQwxyToo/OjdMx6akgVC7K6few0a7F/0wLtmKKN7I73AH5T2836UuXRqN7Qg+IIUw/+YJksRw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ }
+ },
+ "node_modules/es-to-primitive": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz",
+ "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==",
+ "dev": true,
+ "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/esbuild": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.20.1.tgz",
+ "integrity": "sha512-OJwEgrpWm/PCMsLVWXKqvcjme3bHNpOgN7Tb6cQnR5n0TPbQx1/Xrn7rqM+wn17bYeT6MGB5sn1Bh5YiGi70nA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.20.1",
+ "@esbuild/android-arm": "0.20.1",
+ "@esbuild/android-arm64": "0.20.1",
+ "@esbuild/android-x64": "0.20.1",
+ "@esbuild/darwin-arm64": "0.20.1",
+ "@esbuild/darwin-x64": "0.20.1",
+ "@esbuild/freebsd-arm64": "0.20.1",
+ "@esbuild/freebsd-x64": "0.20.1",
+ "@esbuild/linux-arm": "0.20.1",
+ "@esbuild/linux-arm64": "0.20.1",
+ "@esbuild/linux-ia32": "0.20.1",
+ "@esbuild/linux-loong64": "0.20.1",
+ "@esbuild/linux-mips64el": "0.20.1",
+ "@esbuild/linux-ppc64": "0.20.1",
+ "@esbuild/linux-riscv64": "0.20.1",
+ "@esbuild/linux-s390x": "0.20.1",
+ "@esbuild/linux-x64": "0.20.1",
+ "@esbuild/netbsd-x64": "0.20.1",
+ "@esbuild/openbsd-x64": "0.20.1",
+ "@esbuild/sunos-x64": "0.20.1",
+ "@esbuild/win32-arm64": "0.20.1",
+ "@esbuild/win32-ia32": "0.20.1",
+ "@esbuild/win32-x64": "0.20.1"
+ }
+ },
+ "node_modules/esbuild-plugin-copy": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-copy/-/esbuild-plugin-copy-2.1.1.tgz",
+ "integrity": "sha512-Bk66jpevTcV8KMFzZI1P7MZKZ+uDcrZm2G2egZ2jNIvVnivDpodZI+/KnpL3Jnap0PBdIHU7HwFGB8r+vV5CVw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^4.1.2",
+ "chokidar": "^3.5.3",
+ "fs-extra": "^10.0.1",
+ "globby": "^11.0.3"
+ },
+ "peerDependencies": {
+ "esbuild": ">= 0.14.0"
+ }
+ },
+ "node_modules/esbuild-plugin-replace": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/esbuild-plugin-replace/-/esbuild-plugin-replace-1.4.0.tgz",
+ "integrity": "sha512-lP3ZAyzyRa5JXoOd59lJbRKNObtK8pJ/RO7o6vdjwLi71GfbL32NR22ZuS7/cLZkr10/L1lutoLma8E4DLngYg==",
+ "dev": true,
+ "dependencies": {
+ "magic-string": "^0.25.7"
+ }
+ },
+ "node_modules/esbuild-sass-plugin": {
+ "version": "3.1.0",
+ "resolved": "https://registry.npmjs.org/esbuild-sass-plugin/-/esbuild-sass-plugin-3.1.0.tgz",
+ "integrity": "sha512-LX/PhMuA7KskPDT8yB10/o3C3fTKVWEzcfzGnGH0wqjZm3FEtm4d6dCxUn+252kuWZAgFOGzGOnBv1FpzClJrA==",
+ "dev": true,
+ "dependencies": {
+ "resolve": "^1.22.8",
+ "sass": "^1.71.1"
+ },
+ "peerDependencies": {
+ "esbuild": "^0.20.1",
+ "sass-embedded": "^1.71.1"
+ }
+ },
+ "node_modules/esbuild-wasm": {
+ "version": "0.20.1",
+ "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.20.1.tgz",
+ "integrity": "sha512-6v/WJubRsjxBbQdz6izgvx7LsVFvVaGmSdwrFHmEzoVgfXL89hkKPoQHsnVI2ngOkcBUQT9kmAM1hVL1k/Av4A==",
+ "dev": true,
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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": "8.57.0",
+ "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.0.tgz",
+ "integrity": "sha512-dZ6+mexnaTIbSBZWgou51U6OmzIhYM2VcNdtiTtI7qPNZm35Akpr0f6vtw3w1Kmn5PYo+tZVfh13WrhpS6oLqQ==",
+ "dev": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.2.0",
+ "@eslint-community/regexpp": "^4.6.1",
+ "@eslint/eslintrc": "^2.1.4",
+ "@eslint/js": "8.57.0",
+ "@humanwhocodes/config-array": "^0.11.14",
+ "@humanwhocodes/module-importer": "^1.0.1",
+ "@nodelib/fs.walk": "^1.2.8",
+ "@ungap/structured-clone": "^1.2.0",
+ "ajv": "^6.12.4",
+ "chalk": "^4.0.0",
+ "cross-spawn": "^7.0.2",
+ "debug": "^4.3.2",
+ "doctrine": "^3.0.0",
+ "escape-string-regexp": "^4.0.0",
+ "eslint-scope": "^7.2.2",
+ "eslint-visitor-keys": "^3.4.3",
+ "espree": "^9.6.1",
+ "esquery": "^1.4.2",
+ "esutils": "^2.0.2",
+ "fast-deep-equal": "^3.1.3",
+ "file-entry-cache": "^6.0.1",
+ "find-up": "^5.0.0",
+ "glob-parent": "^6.0.2",
+ "globals": "^13.19.0",
+ "graphemer": "^1.4.0",
+ "ignore": "^5.2.0",
+ "imurmurhash": "^0.1.4",
+ "is-glob": "^4.0.0",
+ "is-path-inside": "^3.0.3",
+ "js-yaml": "^4.1.0",
+ "json-stable-stringify-without-jsonify": "^1.0.1",
+ "levn": "^0.4.1",
+ "lodash.merge": "^4.6.2",
+ "minimatch": "^3.1.2",
+ "natural-compare": "^1.4.0",
+ "optionator": "^0.9.3",
+ "strip-ansi": "^6.0.1",
+ "text-table": "^0.2.0"
+ },
+ "bin": {
+ "eslint": "bin/eslint.js"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/eslint-compat-utils": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/eslint-compat-utils/-/eslint-compat-utils-0.1.2.tgz",
+ "integrity": "sha512-Jia4JDldWnFNIru1Ehx1H5s9/yxiRHY/TimCuUc0jNexew3cF1gI6CYZil1ociakfWO3rRqFjl1mskBblB3RYg==",
+ "dev": true,
+ "peer": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "peerDependencies": {
+ "eslint": ">=6.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard": {
+ "version": "17.1.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard/-/eslint-config-standard-17.1.0.tgz",
+ "integrity": "sha512-IwHwmaBNtDK4zDHQukFDW5u/aTb8+meQWZvNFWkiGmbWjD6bqyuSSBxxXKkCftCUzc1zwCH2m/baCNDLGmuO5Q==",
+ "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"
+ }
+ ],
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^8.0.1",
+ "eslint-plugin-import": "^2.25.2",
+ "eslint-plugin-n": "^15.0.0 || ^16.0.0 ",
+ "eslint-plugin-promise": "^6.0.0"
+ }
+ },
+ "node_modules/eslint-config-standard-jsx": {
+ "version": "11.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-jsx/-/eslint-config-standard-jsx-11.0.0.tgz",
+ "integrity": "sha512-+1EV/R0JxEK1L0NGolAr8Iktm3Rgotx3BKwgaX+eAuSX8D952LULKtjgZD3F+e6SvibONnhLwoTi9DPxN5LvvQ==",
+ "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"
+ }
+ ],
+ "peerDependencies": {
+ "eslint": "^8.8.0",
+ "eslint-plugin-react": "^7.28.0"
+ }
+ },
+ "node_modules/eslint-config-standard-react": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/eslint-config-standard-react/-/eslint-config-standard-react-13.0.0.tgz",
+ "integrity": "sha512-HrVPGj8UncHfV+BsdJTuJpVsomn6AIrke3Af2Fh4XFvQQDU+iO6N2ZL+UsC+scExft4fU3uf7fJwj7PKWnXJDA==",
+ "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"
+ }
+ ],
+ "peerDependencies": {
+ "eslint": "^8.8.0",
+ "eslint-plugin-react": "^7.28.0",
+ "eslint-plugin-react-hooks": "^4.6.0"
+ }
+ },
+ "node_modules/eslint-import-resolver-node": {
+ "version": "0.3.9",
+ "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz",
+ "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7",
+ "is-core-module": "^2.13.0",
+ "resolve": "^1.22.4"
+ }
+ },
+ "node_modules/eslint-import-resolver-node/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-module-utils": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.1.tgz",
+ "integrity": "sha512-rXDXR3h7cs7dy9RNpUlQf80nX31XWJEyGq1tRMo+6GsO5VmTe4UTwtmonAD4ZkAsrfMVDA2wlGJ3790Ys+D49Q==",
+ "dev": true,
+ "dependencies": {
+ "debug": "^3.2.7"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependenciesMeta": {
+ "eslint": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/eslint-module-utils/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-es-x": {
+ "version": "7.5.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz",
+ "integrity": "sha512-ODswlDSO0HJDzXU0XvgZ3lF3lS3XAZEossh15Q2UHjwrJggWeBoKqqEsLTZLXl+dh5eOAozG0zRcYtuE35oTuQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.1.2",
+ "@eslint-community/regexpp": "^4.6.0",
+ "eslint-compat-utils": "^0.1.2"
+ },
+ "engines": {
+ "node": "^14.18.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ota-meshi"
+ },
+ "peerDependencies": {
+ "eslint": ">=8"
+ }
+ },
+ "node_modules/eslint-plugin-import": {
+ "version": "2.29.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz",
+ "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlastindex": "^1.2.3",
+ "array.prototype.flat": "^1.3.2",
+ "array.prototype.flatmap": "^1.3.2",
+ "debug": "^3.2.7",
+ "doctrine": "^2.1.0",
+ "eslint-import-resolver-node": "^0.3.9",
+ "eslint-module-utils": "^2.8.0",
+ "hasown": "^2.0.0",
+ "is-core-module": "^2.13.1",
+ "is-glob": "^4.0.3",
+ "minimatch": "^3.1.2",
+ "object.fromentries": "^2.0.7",
+ "object.groupby": "^1.0.1",
+ "object.values": "^1.1.7",
+ "semver": "^6.3.1",
+ "tsconfig-paths": "^3.15.0"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/debug": {
+ "version": "3.2.7",
+ "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
+ "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
+ "dev": true,
+ "dependencies": {
+ "ms": "^2.1.1"
+ }
+ },
+ "node_modules/eslint-plugin-import/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-jsx-a11y": {
+ "version": "6.8.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.8.0.tgz",
+ "integrity": "sha512-Hdh937BS3KdwwbBaKd5+PLCOmYY6U4f2h9Z2ktwtNKvIdIEu137rjYbcb9ApSbVJfWxANNuiKTD/9tOKjK9qOA==",
+ "dev": true,
+ "dependencies": {
+ "@babel/runtime": "^7.23.2",
+ "aria-query": "^5.3.0",
+ "array-includes": "^3.1.7",
+ "array.prototype.flatmap": "^1.3.2",
+ "ast-types-flow": "^0.0.8",
+ "axe-core": "=4.7.0",
+ "axobject-query": "^3.2.1",
+ "damerau-levenshtein": "^1.0.8",
+ "emoji-regex": "^9.2.2",
+ "es-iterator-helpers": "^1.0.15",
+ "hasown": "^2.0.0",
+ "jsx-ast-utils": "^3.3.5",
+ "language-tags": "^1.0.9",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.7",
+ "object.fromentries": "^2.0.7"
+ },
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-n": {
+ "version": "16.6.2",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-n/-/eslint-plugin-n-16.6.2.tgz",
+ "integrity": "sha512-6TyDmZ1HXoFQXnhCTUjVFULReoBPOAjpuiKELMkeP40yffI/1ZRO+d9ug/VC6fqISo2WkuIBk3cvuRPALaWlOQ==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@eslint-community/eslint-utils": "^4.4.0",
+ "builtins": "^5.0.1",
+ "eslint-plugin-es-x": "^7.5.0",
+ "get-tsconfig": "^4.7.0",
+ "globals": "^13.24.0",
+ "ignore": "^5.2.4",
+ "is-builtin-module": "^3.2.1",
+ "is-core-module": "^2.12.1",
+ "minimatch": "^3.1.2",
+ "resolve": "^1.22.2",
+ "semver": "^7.5.3"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/mysticatea"
+ },
+ "peerDependencies": {
+ "eslint": ">=7.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-n/node_modules/semver": {
+ "version": "7.6.0",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.0.tgz",
+ "integrity": "sha512-EnwXhrlwXMk9gKu5/flx5sv/an57AkRplG3hTK68W7FRDN+k+OWBj65M7719OkA82XLBxrcX0KSHj+X5COhOVg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "lru-cache": "^6.0.0"
+ },
+ "bin": {
+ "semver": "bin/semver.js"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/eslint-plugin-promise": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-promise/-/eslint-plugin-promise-6.1.1.tgz",
+ "integrity": "sha512-tjqWDwVZQo7UIPMeDReOpUgHCmCiH+ePnVT+5zVapL0uuHnegBUs2smM13CzOs2Xb5+MHMRFTs9v24yjba4Oig==",
+ "dev": true,
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "peerDependencies": {
+ "eslint": "^7.0.0 || ^8.0.0"
+ }
+ },
+ "node_modules/eslint-plugin-react": {
+ "version": "7.34.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.34.0.tgz",
+ "integrity": "sha512-MeVXdReleBTdkz/bvcQMSnCXGi+c9kvy51IpinjnJgutl3YTHWsDdke7Z1ufZpGfDG8xduBDKyjtB9JH1eBKIQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.7",
+ "array.prototype.findlast": "^1.2.4",
+ "array.prototype.flatmap": "^1.3.2",
+ "array.prototype.toreversed": "^1.1.2",
+ "array.prototype.tosorted": "^1.1.3",
+ "doctrine": "^2.1.0",
+ "es-iterator-helpers": "^1.0.17",
+ "estraverse": "^5.3.0",
+ "jsx-ast-utils": "^2.4.1 || ^3.0.0",
+ "minimatch": "^3.1.2",
+ "object.entries": "^1.1.7",
+ "object.fromentries": "^2.0.7",
+ "object.hasown": "^1.1.3",
+ "object.values": "^1.1.7",
+ "prop-types": "^15.8.1",
+ "resolve": "^2.0.0-next.5",
+ "semver": "^6.3.1",
+ "string.prototype.matchall": "^4.0.10"
+ },
+ "engines": {
+ "node": ">=4"
+ },
+ "peerDependencies": {
+ "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8"
+ }
+ },
+ "node_modules/eslint-plugin-react-hooks": {
+ "version": "4.6.0",
+ "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.0.tgz",
+ "integrity": "sha512-oFc7Itz9Qxh2x4gNHStv3BqJq54ExXmfC+a1NjAta66IAN87Wu0R/QArgIS9qKzX3dXKPI9H5crl9QchNMY9+g==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "peerDependencies": {
+ "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/doctrine": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz",
+ "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==",
+ "dev": true,
+ "dependencies": {
+ "esutils": "^2.0.2"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/eslint-plugin-react/node_modules/resolve": {
+ "version": "2.0.0-next.5",
+ "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.5.tgz",
+ "integrity": "sha512-U7WjGVG9sH8tvjW5SmGbQuui75FiyjAX72HX15DwBBwF9dNiQZRQAg9nnPhYy+TUnE0+VcrttuvNI8oSxZcocA==",
+ "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/eslint-scope": {
+ "version": "7.2.2",
+ "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
+ "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==",
+ "dev": true,
+ "dependencies": {
+ "esrecurse": "^4.3.0",
+ "estraverse": "^5.2.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.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/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/espree": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz",
+ "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==",
+ "dev": true,
+ "dependencies": {
+ "acorn": "^8.9.0",
+ "acorn-jsx": "^5.3.2",
+ "eslint-visitor-keys": "^3.4.1"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/eslint"
+ }
+ },
+ "node_modules/esquery": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.5.0.tgz",
+ "integrity": "sha512-YQLXUplAwJgCydQ78IMJywZCceoqk1oH01OERdSAJc/7U2AylwjhSCLDEtqwg811idIS/9fIU5GjG73IgjKMVg==",
+ "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/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/event-target-shim": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
+ "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "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/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-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/fastest-levenshtein": {
+ "version": "1.0.16",
+ "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz",
+ "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4.9.1"
+ }
+ },
+ "node_modules/fastq": {
+ "version": "1.17.1",
+ "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz",
+ "integrity": "sha512-sRVD3lWVIXWg6By68ZN7vho9a1pQcN/WBFaAAsDDFzlJjvoGx0P8z7V1t72grFJfJhu3YPZBuu25f7Kaw2jN1w==",
+ "dev": true,
+ "dependencies": {
+ "reusify": "^1.0.4"
+ }
+ },
+ "node_modules/file-entry-cache": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz",
+ "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==",
+ "dev": true,
+ "dependencies": {
+ "flat-cache": "^3.0.4"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/file-selector": {
+ "version": "0.6.0",
+ "resolved": "https://registry.npmjs.org/file-selector/-/file-selector-0.6.0.tgz",
+ "integrity": "sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==",
+ "dependencies": {
+ "tslib": "^2.4.0"
+ },
+ "engines": {
+ "node": ">= 12"
+ }
+ },
+ "node_modules/fill-range": {
+ "version": "7.0.1",
+ "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
+ "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
+ "dev": true,
+ "dependencies": {
+ "to-regex-range": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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-cache": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz",
+ "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==",
+ "dev": true,
+ "dependencies": {
+ "flatted": "^3.2.9",
+ "keyv": "^4.5.3",
+ "rimraf": "^3.0.2"
+ },
+ "engines": {
+ "node": "^10.12.0 || >=12.0.0"
+ }
+ },
+ "node_modules/flatted": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz",
+ "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==",
+ "dev": true
+ },
+ "node_modules/focus-trap": {
+ "version": "7.5.2",
+ "resolved": "https://registry.npmjs.org/focus-trap/-/focus-trap-7.5.2.tgz",
+ "integrity": "sha512-p6vGNNWLDGwJCiEjkSK6oERj/hEyI9ITsSwIUICBoKLlWiTWXJRfQibCwcoi50rTZdbi87qDtUlMCmQwsGSgPw==",
+ "dependencies": {
+ "tabbable": "^6.2.0"
+ }
+ },
+ "node_modules/for-each": {
+ "version": "0.3.3",
+ "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.3.tgz",
+ "integrity": "sha512-jqYfLp7mo9vIyQf8ykW2v7A+2N4QjeCeI5+Dz9XraiO1ign81wjiH7Fb9vSOWvQfNtmSa4H2RoQTrrXivdUZmw==",
+ "dev": true,
+ "dependencies": {
+ "is-callable": "^1.1.3"
+ }
+ },
+ "node_modules/fs-extra": {
+ "version": "10.1.0",
+ "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.1.0.tgz",
+ "integrity": "sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==",
+ "dev": true,
+ "dependencies": {
+ "graceful-fs": "^4.2.0",
+ "jsonfile": "^6.0.1",
+ "universalify": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "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==",
+ "dev": true
+ },
+ "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/function.prototype.name": {
+ "version": "1.1.6",
+ "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz",
+ "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "functions-have-names": "^1.2.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/functions-have-names": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz",
+ "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.2.0.tgz",
+ "integrity": "sha512-2nk+7SIVb14QrgXFHcm84tD4bKQz0RxPuMT8Ag5KPOq7J5fEmAg0UbXdTOSHqNuHSU28k55qnceesxXRZGzKWA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/get-intrinsic": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz",
+ "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "has-proto": "^1.0.1",
+ "has-symbols": "^1.0.3",
+ "hasown": "^2.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-symbol-description": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz",
+ "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/get-tsconfig": {
+ "version": "4.7.3",
+ "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.7.3.tgz",
+ "integrity": "sha512-ZvkrzoUA0PQZM6fy6+/Hce561s+faD1rsNwhnO5FelNjyy7EMGJ3Rz1AQ8GYDWjhRs/7dBLOEJvhK8MiEJOAFg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "resolve-pkg-maps": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1"
+ }
+ },
+ "node_modules/gettext-parser": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-8.0.0.tgz",
+ "integrity": "sha512-eFmhDi2xQ+2reMRY2AbJ2oa10uFOl1oyGbAKdCZiNOk94NJHi7aN0OBELSC9v35ZAPQdr+uRBi93/Gu4SlBdrA==",
+ "dev": true,
+ "dependencies": {
+ "content-type": "^1.0.5",
+ "encoding": "^0.1.13",
+ "readable-stream": "^4.5.2",
+ "safe-buffer": "^5.2.1"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/glob": {
+ "version": "7.2.3",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz",
+ "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==",
+ "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/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/global-modules": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz",
+ "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==",
+ "dev": true,
+ "dependencies": {
+ "global-prefix": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz",
+ "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==",
+ "dev": true,
+ "dependencies": {
+ "ini": "^1.3.5",
+ "kind-of": "^6.0.2",
+ "which": "^1.3.1"
+ },
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/global-prefix/node_modules/which": {
+ "version": "1.3.1",
+ "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz",
+ "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==",
+ "dev": true,
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "which": "bin/which"
+ }
+ },
+ "node_modules/globals": {
+ "version": "13.24.0",
+ "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz",
+ "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==",
+ "dev": true,
+ "dependencies": {
+ "type-fest": "^0.20.2"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globalthis": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.3.tgz",
+ "integrity": "sha512-sFdI5LyBiNTHjRd7cGPWapiHWMOXKyuBNX/cWJ3NfzrZQVa8GI/8cofCl74AOVqq9W5kNmguTIzJ/1s2gyI9wA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/globby": {
+ "version": "11.1.0",
+ "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz",
+ "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==",
+ "dev": true,
+ "dependencies": {
+ "array-union": "^2.1.0",
+ "dir-glob": "^3.0.1",
+ "fast-glob": "^3.2.9",
+ "ignore": "^5.2.0",
+ "merge2": "^1.4.1",
+ "slash": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/globjoin": {
+ "version": "0.1.4",
+ "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz",
+ "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==",
+ "dev": true
+ },
+ "node_modules/gopd": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz",
+ "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==",
+ "dev": true,
+ "dependencies": {
+ "get-intrinsic": "^1.1.3"
+ },
+ "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==",
+ "dev": true
+ },
+ "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/has-bigints": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
+ "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/has-property-descriptors": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz",
+ "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==",
+ "dev": true,
+ "dependencies": {
+ "es-define-property": "^1.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-proto": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz",
+ "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/has-symbols": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz",
+ "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==",
+ "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/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/html-tags": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz",
+ "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/htmlparser": {
+ "version": "1.7.7",
+ "resolved": "https://registry.npmjs.org/htmlparser/-/htmlparser-1.7.7.tgz",
+ "integrity": "sha512-zpK66ifkT0fauyFh2Mulrq4AqGTucxGtOhZ8OjkbSfcCpkqQEI8qRkY0tSQSJNAQ4HUZkgWaU4fK4EH6SVH9PQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.1.33"
+ }
+ },
+ "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/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.1",
+ "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.1.tgz",
+ "integrity": "sha512-5Fytz/IraMjqpwfd34ke28PTVMjZjJG2MPn5t7OE4eUCUNf8BAa7b5WUS9/Qvr6mwOQS7Mk6vdsMno5he+T8Xw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/immutable": {
+ "version": "4.3.5",
+ "resolved": "https://registry.npmjs.org/immutable/-/immutable-4.3.5.tgz",
+ "integrity": "sha512-8eabxkth9gZatlwl5TBuJnCsoTADlL6ftEr7A4qgdaTsPyreilDSnUk57SO+jfKcNtxPa22U5KK6DSeAYhpBJw==",
+ "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,
+ "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==",
+ "dev": true,
+ "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==",
+ "dev": true
+ },
+ "node_modules/ini": {
+ "version": "1.3.8",
+ "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz",
+ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==",
+ "dev": true
+ },
+ "node_modules/internal-slot": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz",
+ "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==",
+ "dev": true,
+ "dependencies": {
+ "es-errors": "^1.3.0",
+ "hasown": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/ipaddr.js": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.1.0.tgz",
+ "integrity": "sha512-LlbxQ7xKzfBusov6UMi4MFpEg0m+mAm9xyNGEduwXMEDuf4WfzB/RZwMVYEd7IKGvh4IUkEXYxtAVu9T3OelJQ==",
+ "engines": {
+ "node": ">= 10"
+ }
+ },
+ "node_modules/irregular-plurals": {
+ "version": "3.5.0",
+ "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz",
+ "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-array-buffer": {
+ "version": "3.0.4",
+ "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz",
+ "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "get-intrinsic": "^1.2.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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
+ },
+ "node_modules/is-async-function": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz",
+ "integrity": "sha512-Y1JXKrfykRJGdlDwdKlLpLyMIiWqWvuSd17TvZk68PLAOGOoF4Xyav1z0Xhoi+gCYjZVeC5SI+hYFOfvXmGRCA==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-bigint": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz",
+ "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==",
+ "dev": true,
+ "dependencies": {
+ "has-bigints": "^1.0.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "binary-extensions": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/is-boolean-object": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz",
+ "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==",
+ "dev": true,
+ "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-builtin-module": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
+ "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "builtin-modules": "^3.3.0"
+ },
+ "engines": {
+ "node": ">=6"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-callable": {
+ "version": "1.2.7",
+ "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz",
+ "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-core-module": {
+ "version": "2.13.1",
+ "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz",
+ "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==",
+ "dev": true,
+ "dependencies": {
+ "hasown": "^2.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-date-object": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz",
+ "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-extglob": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
+ "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-finalizationregistry": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz",
+ "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/is-generator-function": {
+ "version": "1.0.10",
+ "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.0.10.tgz",
+ "integrity": "sha512-jsEjy9l3yiXEQ+PsXdmBwEPcOxaXWLspKdplFUVI9vq1iZgIekeC0L167qeu86czQaxed3q/Uzuw0swL0irL8A==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "is-extglob": "^2.1.1"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/is-map": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz",
+ "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-negative-zero": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz",
+ "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.12.0"
+ }
+ },
+ "node_modules/is-number-object": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz",
+ "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==",
+ "dev": true,
+ "dependencies": {
+ "has-tostringtag": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-path-inside": {
+ "version": "3.0.3",
+ "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz",
+ "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "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.1.4",
+ "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
+ "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==",
+ "dev": true,
+ "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-set": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz",
+ "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-shared-array-buffer": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz",
+ "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-string": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz",
+ "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==",
+ "dev": true,
+ "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",
+ "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz",
+ "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==",
+ "dev": true,
+ "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.13",
+ "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz",
+ "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==",
+ "dev": true,
+ "dependencies": {
+ "which-typed-array": "^1.1.14"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-unicode-supported": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz",
+ "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/is-weakmap": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz",
+ "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakref": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz",
+ "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/is-weakset": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz",
+ "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/isarray": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz",
+ "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==",
+ "dev": true
+ },
+ "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/iterator.prototype": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.2.tgz",
+ "integrity": "sha512-DR33HMMr8EzwuRL8Y9D3u2BMj8+RqSE850jfGu59kS7tbmPLzGkZmVSfyCFSDxuZiEY6Rzt3T2NA/qU+NwVj1w==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "reflect.getprototypeof": "^1.0.4",
+ "set-function-name": "^2.0.1"
+ }
+ },
+ "node_modules/jed": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/jed/-/jed-1.1.1.tgz",
+ "integrity": "sha512-z35ZSEcXHxLW4yumw0dF6L464NT36vmx3wxJw8MDpraBcWuNVgUPZgPJKcu1HekNgwlMFNqol7i/IpSbjhqwqA==",
+ "dev": true
+ },
+ "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,
+ "dependencies": {
+ "argparse": "^2.0.1"
+ },
+ "bin": {
+ "js-yaml": "bin/js-yaml.js"
+ }
+ },
+ "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": "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/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/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": "1.0.2",
+ "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz",
+ "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==",
+ "dev": true,
+ "dependencies": {
+ "minimist": "^1.2.0"
+ },
+ "bin": {
+ "json5": "lib/cli.js"
+ }
+ },
+ "node_modules/jsonfile": {
+ "version": "6.1.0",
+ "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz",
+ "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==",
+ "dev": true,
+ "dependencies": {
+ "universalify": "^2.0.0"
+ },
+ "optionalDependencies": {
+ "graceful-fs": "^4.1.6"
+ }
+ },
+ "node_modules/jsx-ast-utils": {
+ "version": "3.3.5",
+ "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
+ "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==",
+ "dev": true,
+ "dependencies": {
+ "array-includes": "^3.1.6",
+ "array.prototype.flat": "^1.3.1",
+ "object.assign": "^4.1.4",
+ "object.values": "^1.1.6"
+ },
+ "engines": {
+ "node": ">=4.0"
+ }
+ },
+ "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/known-css-properties": {
+ "version": "0.29.0",
+ "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.29.0.tgz",
+ "integrity": "sha512-Ne7wqW7/9Cz54PDt4I3tcV+hAyat8ypyOGzYRJQfdxnnjeWsTxt1cy8pjvvKeI5kfXuyvULyeeAvwvvtAX3ayQ==",
+ "dev": true
+ },
+ "node_modules/language-subtag-registry": {
+ "version": "0.3.22",
+ "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.22.tgz",
+ "integrity": "sha512-tN0MCzyWnoz/4nHS6uxdlFWoUZT7ABptwKPQ52Ea7URk6vll88bWBVhodtnlfEuCcKWNGoc+uGbw1cwa9IKh/w==",
+ "dev": true
+ },
+ "node_modules/language-tags": {
+ "version": "1.0.9",
+ "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz",
+ "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==",
+ "dev": true,
+ "dependencies": {
+ "language-subtag-registry": "^0.3.20"
+ },
+ "engines": {
+ "node": ">=0.10"
+ }
+ },
+ "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/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
+ },
+ "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.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/lodash.truncate": {
+ "version": "4.4.2",
+ "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz",
+ "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==",
+ "dev": true
+ },
+ "node_modules/log-symbols": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz",
+ "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==",
+ "dev": true,
+ "dependencies": {
+ "chalk": "^5.3.0",
+ "is-unicode-supported": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/log-symbols/node_modules/chalk": {
+ "version": "5.3.0",
+ "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.3.0.tgz",
+ "integrity": "sha512-dLitG79d+GV1Nb/VYcCDFivJeK1hiukt9QjRNVOsUtTy1rR1YJsmpGGTZ3qJos+uw7WmWF4wUwBd9jxjocFC2w==",
+ "dev": true,
+ "engines": {
+ "node": "^12.17.0 || ^14.13 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/chalk?sponsor=1"
+ }
+ },
+ "node_modules/loose-envify": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz",
+ "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
+ "dependencies": {
+ "js-tokens": "^3.0.0 || ^4.0.0"
+ },
+ "bin": {
+ "loose-envify": "cli.js"
+ }
+ },
+ "node_modules/lru-cache": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
+ "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "yallist": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.25.9",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz",
+ "integrity": "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ==",
+ "dev": true,
+ "dependencies": {
+ "sourcemap-codec": "^1.4.8"
+ }
+ },
+ "node_modules/mathml-tag-names": {
+ "version": "2.1.3",
+ "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz",
+ "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==",
+ "dev": true,
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/wooorm"
+ }
+ },
+ "node_modules/mdn-data": {
+ "version": "2.0.30",
+ "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz",
+ "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==",
+ "dev": true
+ },
+ "node_modules/meow": {
+ "version": "13.2.0",
+ "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz",
+ "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==",
+ "dev": true,
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "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/micromatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
+ "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "dev": true,
+ "dependencies": {
+ "braces": "^3.0.2",
+ "picomatch": "^2.3.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
+ "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/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/ms": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
+ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==",
+ "dev": true
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.7",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
+ "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
+ "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/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/normalize-path": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
+ "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "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.1",
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz",
+ "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==",
+ "dev": true,
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object-keys": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz",
+ "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.assign": {
+ "version": "4.1.5",
+ "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz",
+ "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "object-keys": "^1.1.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.entries": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.7.tgz",
+ "integrity": "sha512-jCBs/0plmPsOnrKAfFQXRG2NFjlhZgjjcBLSmTnEhU8U6vVTsVe8ANeQJCHTl3gSsI4J+0emOoCgoKlmQPMgmA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/object.fromentries": {
+ "version": "2.0.7",
+ "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.7.tgz",
+ "integrity": "sha512-UPbPHML6sL8PI/mOqPwsH4G6iyXcCGzLin8KvEPenOZN5lpCNBZZQ+V62vdjB1mQHrmqGQt5/OJzemUA+KJmEA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.groupby": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.2.tgz",
+ "integrity": "sha512-bzBq58S+x+uo0VjurFT0UktpKHOZmv4/xePiOA1nbB9pMqpGK7rUPNgf+1YC+7mE+0HzhTMqNUuCqvKhj6FnBw==",
+ "dev": true,
+ "dependencies": {
+ "array.prototype.filter": "^1.0.3",
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.0.0"
+ }
+ },
+ "node_modules/object.hasown": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz",
+ "integrity": "sha512-fFI4VcYpRHvSLXxP7yiZOMAd331cPfd2p7PFDVbgUsYOfCT3tICVqXWngbjr4m49OvsBwUBQ6O2uQoJvy3RexA==",
+ "dev": true,
+ "dependencies": {
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/object.values": {
+ "version": "1.1.7",
+ "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.7.tgz",
+ "integrity": "sha512-aU6xnDFYT3x17e/f0IiiwlGPTy2jzMySGfUB4fq6z7CV8l85CWHDk5ErhyhpfDHhrOMwGFhSQkhMGHaIotA6Ng==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "wrappy": "1"
+ }
+ },
+ "node_modules/optionator": {
+ "version": "0.9.3",
+ "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.3.tgz",
+ "integrity": "sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==",
+ "dev": true,
+ "dependencies": {
+ "@aashutoshrathi/word-wrap": "^1.2.3",
+ "deep-is": "^0.1.3",
+ "fast-levenshtein": "^2.0.6",
+ "levn": "^0.4.1",
+ "prelude-ls": "^1.2.1",
+ "type-check": "^0.4.0"
+ },
+ "engines": {
+ "node": ">= 0.8.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/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,
+ "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,
+ "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/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==",
+ "dev": true,
+ "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==",
+ "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-type": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz",
+ "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
+ "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
+ "dev": true
+ },
+ "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/plur": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/plur/-/plur-5.1.0.tgz",
+ "integrity": "sha512-VP/72JeXqak2KiOzjgKtQen5y3IZHn+9GOuLDafPv0eXa47xq0At93XahYBs26MsifCQ4enGKwbjBTKgb9QJXg==",
+ "dev": true,
+ "dependencies": {
+ "irregular-plurals": "^3.3.0"
+ },
+ "engines": {
+ "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/possible-typed-array-names": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz",
+ "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.4.35",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.35.tgz",
+ "integrity": "sha512-u5U8qYpBCpN13BsiEB0CbR1Hhh4Gc0zLFuedrHJKMctHCHAGrMdG0PRM/KErzAL3CU6/eckEtmHNB3x6e3c0vA==",
+ "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.0.0",
+ "source-map-js": "^1.0.2"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "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-resolve-nested-selector": {
+ "version": "0.1.1",
+ "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz",
+ "integrity": "sha512-HvExULSwLqHLgUy1rl3ANIqCsvMS0WHss2UOsXhXnQaZ9VCc2oBvIpXrl00IUFT5ZDITME0o6oiXeiHr2SAIfw==",
+ "dev": true
+ },
+ "node_modules/postcss-safe-parser": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.0.tgz",
+ "integrity": "sha512-ovehqRNVCpuFzbXoTb4qLtyzK3xn3t/CUBxOs8LsnQjQrShaB4lKiHoVqY8ANaC0hBMHq5QVWk77rwGklFUDrg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": ">=18.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.31"
+ }
+ },
+ "node_modules/postcss-scss": {
+ "version": "4.0.9",
+ "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz",
+ "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss-scss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "engines": {
+ "node": ">=12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.4.29"
+ }
+ },
+ "node_modules/postcss-selector-parser": {
+ "version": "6.0.15",
+ "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.15.tgz",
+ "integrity": "sha512-rEYkQOMUCEMhsKbK66tbEU9QVIxbhN18YiniAwA7XQYTVBqrBy+P2p5JcdqsHgKM2zWylp8d7J6eszocfds5Sw==",
+ "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/process": {
+ "version": "0.11.10",
+ "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
+ "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
+ "dev": true,
+ "engines": {
+ "node": ">= 0.6.0"
+ }
+ },
+ "node_modules/prop-types": {
+ "version": "15.8.1",
+ "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
+ "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==",
+ "dependencies": {
+ "loose-envify": "^1.4.0",
+ "object-assign": "^4.1.1",
+ "react-is": "^16.13.1"
+ }
+ },
+ "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/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/react": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
+ "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ },
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "18.2.0",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
+ "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dependencies": {
+ "loose-envify": "^1.1.0",
+ "scheduler": "^0.23.0"
+ },
+ "peerDependencies": {
+ "react": "^18.2.0"
+ }
+ },
+ "node_modules/react-dropzone": {
+ "version": "14.2.3",
+ "resolved": "https://registry.npmjs.org/react-dropzone/-/react-dropzone-14.2.3.tgz",
+ "integrity": "sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==",
+ "dependencies": {
+ "attr-accept": "^2.2.2",
+ "file-selector": "^0.6.0",
+ "prop-types": "^15.8.1"
+ },
+ "engines": {
+ "node": ">= 10.13"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8 || 18.0.0"
+ }
+ },
+ "node_modules/react-is": {
+ "version": "16.13.1",
+ "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz",
+ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="
+ },
+ "node_modules/readable-stream": {
+ "version": "4.5.2",
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
+ "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
+ "dev": true,
+ "dependencies": {
+ "abort-controller": "^3.0.0",
+ "buffer": "^6.0.3",
+ "events": "^3.3.0",
+ "process": "^0.11.10",
+ "string_decoder": "^1.3.0"
+ },
+ "engines": {
+ "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
+ }
+ },
+ "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/reflect.getprototypeof": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.5.tgz",
+ "integrity": "sha512-62wgfC8dJWrmxv44CA36pLDnP6KKl3Vhxb7PL+8+qrrFMMoJij4vgiMP8zV4O8+CBMXY1mHxI5fITGHXFHVmQQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.5",
+ "define-properties": "^1.2.1",
+ "es-abstract": "^1.22.3",
+ "es-errors": "^1.0.0",
+ "get-intrinsic": "^1.2.3",
+ "globalthis": "^1.0.3",
+ "which-builtin-type": "^1.1.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/regexp.prototype.flags": {
+ "version": "1.5.2",
+ "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz",
+ "integrity": "sha512-NcDiDkTLuPR+++OCKB0nWafEmhg/Da8aUPLPMQbK+bxKKCm1/S5he+AqYa4PlMCVBalb4/yxIRub6qkEx5yJbw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "define-properties": "^1.2.1",
+ "es-errors": "^1.3.0",
+ "set-function-name": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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/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,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "node_modules/resolve-pkg-maps": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
+ "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
+ "peer": true,
+ "funding": {
+ "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
+ }
+ },
+ "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/rimraf": {
+ "version": "3.0.2",
+ "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz",
+ "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==",
+ "dev": true,
+ "dependencies": {
+ "glob": "^7.1.3"
+ },
+ "bin": {
+ "rimraf": "bin.js"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/isaacs"
+ }
+ },
+ "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/rxjs": {
+ "version": "7.8.1",
+ "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz",
+ "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "tslib": "^2.1.0"
+ }
+ },
+ "node_modules/safe-array-concat": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz",
+ "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "get-intrinsic": "^1.2.4",
+ "has-symbols": "^1.0.3",
+ "isarray": "^2.0.5"
+ },
+ "engines": {
+ "node": ">=0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "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/safe-regex-test": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz",
+ "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.6",
+ "es-errors": "^1.3.0",
+ "is-regex": "^1.1.4"
+ },
+ "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==",
+ "dev": true
+ },
+ "node_modules/sass": {
+ "version": "1.71.1",
+ "resolved": "https://registry.npmjs.org/sass/-/sass-1.71.1.tgz",
+ "integrity": "sha512-wovtnV2PxzteLlfNzbgm1tFXPLoZILYAMJtvoXXkD7/+1uP41eKkIt1ypWq5/q2uT94qHjXehEYfmjKOvjL9sg==",
+ "dev": true,
+ "dependencies": {
+ "chokidar": ">=3.0.0 <4.0.0",
+ "immutable": "^4.0.0",
+ "source-map-js": ">=0.6.2 <2.0.0"
+ },
+ "bin": {
+ "sass": "sass.js"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/sass-embedded": {
+ "version": "1.71.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded/-/sass-embedded-1.71.1.tgz",
+ "integrity": "sha512-nOmqErO1zd1wjvTbDscLZZ3fv5JPeQfaKuo0UCjYm7qPbpQcycp0l3nFZHxovjLjCetJ9IrLOADdznFYKV0f1A==",
+ "dev": true,
+ "peer": true,
+ "dependencies": {
+ "@bufbuild/protobuf": "^1.0.0",
+ "buffer-builder": "^0.2.0",
+ "immutable": "^4.0.0",
+ "rxjs": "^7.4.0",
+ "supports-color": "^8.1.1",
+ "varint": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=16.0.0"
+ },
+ "optionalDependencies": {
+ "sass-embedded-android-arm": "1.71.1",
+ "sass-embedded-android-arm64": "1.71.1",
+ "sass-embedded-android-ia32": "1.71.1",
+ "sass-embedded-android-x64": "1.71.1",
+ "sass-embedded-darwin-arm64": "1.71.1",
+ "sass-embedded-darwin-x64": "1.71.1",
+ "sass-embedded-linux-arm": "1.71.1",
+ "sass-embedded-linux-arm64": "1.71.1",
+ "sass-embedded-linux-ia32": "1.71.1",
+ "sass-embedded-linux-musl-arm": "1.71.1",
+ "sass-embedded-linux-musl-arm64": "1.71.1",
+ "sass-embedded-linux-musl-ia32": "1.71.1",
+ "sass-embedded-linux-musl-x64": "1.71.1",
+ "sass-embedded-linux-x64": "1.71.1",
+ "sass-embedded-win32-ia32": "1.71.1",
+ "sass-embedded-win32-x64": "1.71.1"
+ }
+ },
+ "node_modules/sass-embedded-linux-musl-x64": {
+ "version": "1.71.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-musl-x64/-/sass-embedded-linux-musl-x64-1.71.1.tgz",
+ "integrity": "sha512-uVfYms/lf4QVSvtQXkSm+Bq3wVsvkRMI30ca82rRwpwebxSaTbUr+uLnskh8QvbyfsbMyrzZQU0SCrO3uCua1A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/sass-embedded-linux-x64": {
+ "version": "1.71.1",
+ "resolved": "https://registry.npmjs.org/sass-embedded-linux-x64/-/sass-embedded-linux-x64-1.71.1.tgz",
+ "integrity": "sha512-7BXniYic16+MQx0InyH8OXburLPGMRYRWf0l/t/fRkNkUHWFl7NQPAX0yvj73c/PKOdaYEUY6isNB4OGUGtZHQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "peer": true,
+ "bin": {
+ "sass": "dart-sass/sass"
+ },
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/sass-embedded/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/scheduler": {
+ "version": "0.23.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
+ "integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dependencies": {
+ "loose-envify": "^1.1.0"
+ }
+ },
+ "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/set-function-length": {
+ "version": "1.2.2",
+ "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz",
+ "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "function-bind": "^1.1.2",
+ "get-intrinsic": "^1.2.4",
+ "gopd": "^1.0.1",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/set-function-name": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz",
+ "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==",
+ "dev": true,
+ "dependencies": {
+ "define-data-property": "^1.1.4",
+ "es-errors": "^1.3.0",
+ "functions-have-names": "^1.2.3",
+ "has-property-descriptors": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "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/side-channel": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz",
+ "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "get-intrinsic": "^1.2.4",
+ "object-inspect": "^1.13.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/sizzle": {
+ "version": "2.3.10",
+ "resolved": "https://registry.npmjs.org/sizzle/-/sizzle-2.3.10.tgz",
+ "integrity": "sha512-kPGev+SiByuzi/YPDTqCwdKLWCaN9+14ve86yH0gP6Efue04xjLYWJrcLC6y1buFyIVXkwHNXPsOTEd1MYVPbQ==",
+ "dev": true
+ },
+ "node_modules/slash": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz",
+ "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/slice-ansi": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz",
+ "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==",
+ "dev": true,
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "astral-regex": "^2.0.0",
+ "is-fullwidth-code-point": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/slice-ansi?sponsor=1"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
+ "integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
+ "dev": true,
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/sourcemap-codec": {
+ "version": "1.4.8",
+ "resolved": "https://registry.npmjs.org/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz",
+ "integrity": "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA==",
+ "deprecated": "Please use @jridgewell/sourcemap-codec instead",
+ "dev": true
+ },
+ "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==",
+ "dev": true,
+ "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==",
+ "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/string-width/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/string.prototype.matchall": {
+ "version": "4.0.10",
+ "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.10.tgz",
+ "integrity": "sha512-rGXbGmOEosIQi6Qva94HUjgPs9vKW+dkG7Y8Q5O2OYkWL6wFaTRZO8zM4mhP94uX55wgyrXzfS2aGtGzUL7EJQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1",
+ "get-intrinsic": "^1.2.1",
+ "has-symbols": "^1.0.3",
+ "internal-slot": "^1.0.5",
+ "regexp.prototype.flags": "^1.5.0",
+ "set-function-name": "^2.0.0",
+ "side-channel": "^1.0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trim": {
+ "version": "1.2.8",
+ "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.8.tgz",
+ "integrity": "sha512-lfjY4HcixfQXOfaqCvcBuOIapyaroTXhbkfJN3gcB1OtyupngWK4sEET9Knd0cXd28kTUqu/kHoV4HKSJdnjiQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimend": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.7.tgz",
+ "integrity": "sha512-Ni79DqeB72ZFq1uH/L6zJ+DKZTkOtPIHovb3YZHQViE+HDouuU4mBrLOLDn5Dde3RF8qw5qVETEjhu9locMLvA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/string.prototype.trimstart": {
+ "version": "1.0.7",
+ "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.7.tgz",
+ "integrity": "sha512-NGhtDFu3jCEm7B4Fy0DpLewdJQOZcQ0rGbwQ/+stjnrp2i+rlKeCvos9hOIeCmqwratM47OBxY7uFZzjxHXmrg==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.2",
+ "define-properties": "^1.2.0",
+ "es-abstract": "^1.22.1"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-bom": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
+ "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==",
+ "dev": true,
+ "engines": {
+ "node": ">=4"
+ }
+ },
+ "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/stylelint": {
+ "version": "16.2.1",
+ "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.2.1.tgz",
+ "integrity": "sha512-SfIMGFK+4n7XVAyv50CpVfcGYWG4v41y6xG7PqOgQSY8M/PgdK0SQbjWFblxjJZlN9jNq879mB4BCZHJRIJ1hA==",
+ "dev": true,
+ "dependencies": {
+ "@csstools/css-parser-algorithms": "^2.5.0",
+ "@csstools/css-tokenizer": "^2.2.3",
+ "@csstools/media-query-list-parser": "^2.1.7",
+ "@csstools/selector-specificity": "^3.0.1",
+ "balanced-match": "^2.0.0",
+ "colord": "^2.9.3",
+ "cosmiconfig": "^9.0.0",
+ "css-functions-list": "^3.2.1",
+ "css-tree": "^2.3.1",
+ "debug": "^4.3.4",
+ "fast-glob": "^3.3.2",
+ "fastest-levenshtein": "^1.0.16",
+ "file-entry-cache": "^8.0.0",
+ "global-modules": "^2.0.0",
+ "globby": "^11.1.0",
+ "globjoin": "^0.1.4",
+ "html-tags": "^3.3.1",
+ "ignore": "^5.3.0",
+ "imurmurhash": "^0.1.4",
+ "is-plain-object": "^5.0.0",
+ "known-css-properties": "^0.29.0",
+ "mathml-tag-names": "^2.1.3",
+ "meow": "^13.1.0",
+ "micromatch": "^4.0.5",
+ "normalize-path": "^3.0.0",
+ "picocolors": "^1.0.0",
+ "postcss": "^8.4.33",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-safe-parser": "^7.0.0",
+ "postcss-selector-parser": "^6.0.15",
+ "postcss-value-parser": "^4.2.0",
+ "resolve-from": "^5.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^7.1.0",
+ "supports-hyperlinks": "^3.0.0",
+ "svg-tags": "^1.0.0",
+ "table": "^6.8.1",
+ "write-file-atomic": "^5.0.1"
+ },
+ "bin": {
+ "stylelint": "bin/stylelint.mjs"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/stylelint"
+ }
+ },
+ "node_modules/stylelint-config-recommended": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.0.tgz",
+ "integrity": "sha512-jSkx290CglS8StmrLp2TxAppIajzIBZKYm3IxT89Kg6fGlxbPiTiyH9PS5YUuVAFwaJLl1ikiXX0QWjI0jmgZQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.0.0"
+ }
+ },
+ "node_modules/stylelint-config-recommended-scss": {
+ "version": "14.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.0.0.tgz",
+ "integrity": "sha512-HDvpoOAQ1RpF+sPbDOT2Q2/YrBDEJDnUymmVmZ7mMCeNiFSdhRdyGEimBkz06wsN+HaFwUh249gDR+I9JR7Onw==",
+ "dev": true,
+ "dependencies": {
+ "postcss-scss": "^4.0.9",
+ "stylelint-config-recommended": "^14.0.0",
+ "stylelint-scss": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3",
+ "stylelint": "^16.0.2"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint-config-standard": {
+ "version": "36.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-standard/-/stylelint-config-standard-36.0.0.tgz",
+ "integrity": "sha512-3Kjyq4d62bYFp/Aq8PMKDwlgUyPU4nacXsjDLWJdNPRUgpuxALu1KnlAHIj36cdtxViVhXexZij65yM0uNIHug==",
+ "dev": true,
+ "dependencies": {
+ "stylelint-config-recommended": "^14.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.1.0"
+ }
+ },
+ "node_modules/stylelint-config-standard-scss": {
+ "version": "13.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-config-standard-scss/-/stylelint-config-standard-scss-13.0.0.tgz",
+ "integrity": "sha512-WaLvkP689qSYUpJQPCo30TFJSSc3VzvvoWnrgp+7PpVby5o8fRUY1cZcP0sePZfjrFl9T8caGhcKg0GO34VDiQ==",
+ "dev": true,
+ "dependencies": {
+ "stylelint-config-recommended-scss": "^14.0.0",
+ "stylelint-config-standard": "^36.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "postcss": "^8.3.3",
+ "stylelint": "^16.1.0"
+ },
+ "peerDependenciesMeta": {
+ "postcss": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/stylelint-formatter-pretty": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/stylelint-formatter-pretty/-/stylelint-formatter-pretty-4.0.0.tgz",
+ "integrity": "sha512-tVuAEhvdTcLzlupqPEPhpBoszX3hB6AnI/OSqEIZOxRatHDHSlu/MaU13MUDzEPOgdoFfDzsVqhp4j2DltaIvg==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "kofi",
+ "url": "https://ko-fi.com/mrcgrtz"
+ },
+ {
+ "type": "liberapay",
+ "url": "https://liberapay.com/mrcgrtz/"
+ }
+ ],
+ "dependencies": {
+ "ansi-escapes": "^6.2.0",
+ "log-symbols": "^6.0.0",
+ "picocolors": "^1.0.0",
+ "plur": "^5.1.0",
+ "string-width": "^7.0.0",
+ "supports-hyperlinks": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=16.0.0"
+ }
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/emoji-regex": {
+ "version": "10.3.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.3.0.tgz",
+ "integrity": "sha512-QpLs9D9v9kArv4lfDEgg1X/gN5XLnf/A6l9cs8SPZLRZR3ZkY9+kwIQTxm+fsSej5UMYGE8fdoaZVIBlqG0XTw==",
+ "dev": true
+ },
+ "node_modules/stylelint-formatter-pretty/node_modules/string-width": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.1.0.tgz",
+ "integrity": "sha512-SEIJCWiX7Kg4c129n48aDRwLbFb2LJmXXFrWBG4NGaRtMQ3myKPKbwrD1BKqQn74oCoNMBVrfDEr5M9YxCsrkw==",
+ "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/stylelint-formatter-pretty/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/stylelint-scss": {
+ "version": "6.2.1",
+ "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.2.1.tgz",
+ "integrity": "sha512-ZoGLbVb1keZYRVGQlhB8G6sZOoNqw61whzzzGFWp05N12ErqLFfBv3JPrXiMLZaW98sBS7K/vUQhRnvUj4vwdw==",
+ "dev": true,
+ "dependencies": {
+ "known-css-properties": "^0.29.0",
+ "postcss-media-query-parser": "^0.2.3",
+ "postcss-resolve-nested-selector": "^0.1.1",
+ "postcss-selector-parser": "^6.0.15",
+ "postcss-value-parser": "^4.2.0"
+ },
+ "engines": {
+ "node": ">=18.12.0"
+ },
+ "peerDependencies": {
+ "stylelint": "^16.0.2"
+ }
+ },
+ "node_modules/stylelint-use-logical-spec": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/stylelint-use-logical-spec/-/stylelint-use-logical-spec-5.0.1.tgz",
+ "integrity": "sha512-UfLB4LW6iG4r3cXxjxkiHQrFyhWFqt8FpNNngD+TyvgMWSokk5TYwTvBHS3atUvZhOogllTOe/PUrGE+4z84AA==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.0.0"
+ },
+ "peerDependencies": {
+ "stylelint": ">=11 < 17"
+ }
+ },
+ "node_modules/stylelint/node_modules/ansi-regex": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.0.1.tgz",
+ "integrity": "sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==",
+ "dev": true,
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/stylelint/node_modules/balanced-match": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz",
+ "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==",
+ "dev": true
+ },
+ "node_modules/stylelint/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/stylelint/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/stylelint/node_modules/resolve-from": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz",
+ "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==",
+ "dev": true,
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/stylelint/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/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==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/supports-hyperlinks": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.0.0.tgz",
+ "integrity": "sha512-QBDPHyPQDRTy9ku4URNGY5Lah8PAaXs6tAAwp55sL5WCsSW7GIfdf6W5ixfziW+t7wh3GVvHyHHyQ1ESsoRvaA==",
+ "dev": true,
+ "dependencies": {
+ "has-flag": "^4.0.0",
+ "supports-color": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=14.18"
+ }
+ },
+ "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/svg-tags": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz",
+ "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==",
+ "dev": true
+ },
+ "node_modules/tabbable": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
+ "integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew=="
+ },
+ "node_modules/table": {
+ "version": "6.8.1",
+ "resolved": "https://registry.npmjs.org/table/-/table-6.8.1.tgz",
+ "integrity": "sha512-Y4X9zqrCftUhMeH2EptSSERdVKt/nEdijTOacGD/97EKjhQ/Qs8RTlEGABSJNNN8lac9kheH+af7yAkEWlgneA==",
+ "dev": true,
+ "dependencies": {
+ "ajv": "^8.0.1",
+ "lodash.truncate": "^4.4.2",
+ "slice-ansi": "^4.0.0",
+ "string-width": "^4.2.3",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
+ "node_modules/table/node_modules/ajv": {
+ "version": "8.12.0",
+ "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+ "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
+ "dev": true,
+ "dependencies": {
+ "fast-deep-equal": "^3.1.1",
+ "json-schema-traverse": "^1.0.0",
+ "require-from-string": "^2.0.2",
+ "uri-js": "^4.2.2"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/epoberezkin"
+ }
+ },
+ "node_modules/table/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/text-table": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz",
+ "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==",
+ "dev": true
+ },
+ "node_modules/throttle-debounce": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz",
+ "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==",
+ "engines": {
+ "node": ">=12.22"
+ }
+ },
+ "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==",
+ "dev": true,
+ "dependencies": {
+ "is-number": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
+ "node_modules/tsconfig-paths": {
+ "version": "3.15.0",
+ "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz",
+ "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==",
+ "dev": true,
+ "dependencies": {
+ "@types/json5": "^0.0.29",
+ "json5": "^1.0.2",
+ "minimist": "^1.2.6",
+ "strip-bom": "^3.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.6.2",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz",
+ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
+ },
+ "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.20.2",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz",
+ "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==",
+ "dev": true,
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-array-buffer": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz",
+ "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "es-errors": "^1.3.0",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ }
+ },
+ "node_modules/typed-array-byte-length": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz",
+ "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-byte-offset": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz",
+ "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/typed-array-length": {
+ "version": "1.0.5",
+ "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.5.tgz",
+ "integrity": "sha512-yMi0PlwuznKHxKmcpoOdeLwxBoVPkqZxd7q2FgMkmD3bNwvF5VW0+UlUQ1k1vmktTu4Yu13Q0RIxEP8+B+wloA==",
+ "dev": true,
+ "dependencies": {
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-proto": "^1.0.3",
+ "is-typed-array": "^1.1.13",
+ "possible-typed-array-names": "^1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/unbox-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz",
+ "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==",
+ "dev": true,
+ "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/universalify": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz",
+ "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==",
+ "dev": true,
+ "engines": {
+ "node": ">= 10.0.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/util-deprecate": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
+ "dev": true
+ },
+ "node_modules/varint": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/varint/-/varint-6.0.0.tgz",
+ "integrity": "sha512-cXEIW6cfr15lFv563k4GuVuW/fiwjknytD37jIOLSdSWuOI6WnO/oKwmP2FQTU2l01LP8/M5TSAJpzUaGe3uWg==",
+ "dev": true,
+ "peer": true
+ },
+ "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/which-boxed-primitive": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz",
+ "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==",
+ "dev": true,
+ "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-builtin-type": {
+ "version": "1.1.3",
+ "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.3.tgz",
+ "integrity": "sha512-YmjsSMDBYsM1CaFiayOVT06+KJeXf0o5M/CAd4o1lTadFAtacTUM49zoYxr/oroopFDfhvN6iEcBxUyc3gvKmw==",
+ "dev": true,
+ "dependencies": {
+ "function.prototype.name": "^1.1.5",
+ "has-tostringtag": "^1.0.0",
+ "is-async-function": "^2.0.0",
+ "is-date-object": "^1.0.5",
+ "is-finalizationregistry": "^1.0.2",
+ "is-generator-function": "^1.0.10",
+ "is-regex": "^1.1.4",
+ "is-weakref": "^1.0.2",
+ "isarray": "^2.0.5",
+ "which-boxed-primitive": "^1.0.2",
+ "which-collection": "^1.0.1",
+ "which-typed-array": "^1.1.9"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-collection": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz",
+ "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==",
+ "dev": true,
+ "dependencies": {
+ "is-map": "^2.0.3",
+ "is-set": "^2.0.3",
+ "is-weakmap": "^2.0.2",
+ "is-weakset": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "node_modules/which-typed-array": {
+ "version": "1.1.15",
+ "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz",
+ "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==",
+ "dev": true,
+ "dependencies": {
+ "available-typed-arrays": "^1.0.7",
+ "call-bind": "^1.0.7",
+ "for-each": "^0.3.3",
+ "gopd": "^1.0.1",
+ "has-tostringtag": "^1.0.2"
+ },
+ "engines": {
+ "node": ">= 0.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/ljharb"
+ }
+ },
+ "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==",
+ "dev": true
+ },
+ "node_modules/write-file-atomic": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz",
+ "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==",
+ "dev": true,
+ "dependencies": {
+ "imurmurhash": "^0.1.4",
+ "signal-exit": "^4.0.1"
+ },
+ "engines": {
+ "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
+ }
+ },
+ "node_modules/ws": {
+ "version": "7.5.9",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.9.tgz",
+ "integrity": "sha512-F+P9Jil7UiSKSkppIiD94dN07AwvFixvLIj1Og1Rl9GGMuNipJnV9JzjD6XuqmAeiswGvUmNLjr5cFuXwNS77Q==",
+ "dev": true,
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/xterm": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/xterm/-/xterm-5.1.0.tgz",
+ "integrity": "sha512-LovENH4WDzpwynj+OTkLyZgJPeDom9Gra4DMlGAgz6pZhIDCQ+YuO7yfwanY+gVbn/mmZIStNOnVRU/ikQuAEQ=="
+ },
+ "node_modules/xterm-addon-canvas": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/xterm-addon-canvas/-/xterm-addon-canvas-0.4.0.tgz",
+ "integrity": "sha512-iTC8CdjX9+hGX7jiEuiDMXzHsY/FKJdVnbjep5xjRXNu7RKOk15xuecIkJ7HZORqMVPpr4DGS3jyd9XUoBuxqw==",
+ "peerDependencies": {
+ "xterm": "^5.0.0"
+ }
+ },
+ "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,
+ "peer": true
+ },
+ "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"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b560542
--- /dev/null
+++ b/package.json
@@ -0,0 +1,61 @@
+{
+ "name": "podman",
+ "description": "Cockpit UI for Podman Containers",
+ "type": "module",
+ "main": "index.js",
+ "repository": "git@github.com:cockpit-project/cockpit-podman.git",
+ "author": "",
+ "license": "LGPL-2.1",
+ "scripts": {
+ "watch": "./build.js -w",
+ "build": "./build.js",
+ "eslint": "eslint --ext .jsx --ext .js src/",
+ "eslint:fix": "eslint --fix --ext .jsx --ext .js src/",
+ "stylelint": "stylelint src/*{.css,scss}",
+ "stylelint:fix": "stylelint --fix src/*{.css,scss}"
+ },
+ "devDependencies": {
+ "argparse": "2.0.1",
+ "chrome-remote-interface": "^0.33.0",
+ "esbuild": "0.20.1",
+ "esbuild-plugin-copy": "2.1.1",
+ "esbuild-plugin-replace": "1.4.0",
+ "esbuild-sass-plugin": "3.1.0",
+ "esbuild-wasm": "0.20.1",
+ "eslint": "8.57.0",
+ "eslint-config-standard": "17.1.0",
+ "eslint-config-standard-jsx": "11.0.0",
+ "eslint-config-standard-react": "13.0.0",
+ "eslint-plugin-import": "2.29.1",
+ "eslint-plugin-jsx-a11y": "6.8.0",
+ "eslint-plugin-promise": "6.1.1",
+ "eslint-plugin-react": "7.34.0",
+ "eslint-plugin-react-hooks": "4.6.0",
+ "gettext-parser": "8.0.0",
+ "htmlparser": "1.7.7",
+ "jed": "1.1.1",
+ "sass": "1.71.1",
+ "sizzle": "2.3.10",
+ "stylelint": "16.2.1",
+ "stylelint-config-standard-scss": "13.0.0",
+ "stylelint-formatter-pretty": "4.0.0",
+ "stylelint-use-logical-spec": "5.0.1"
+ },
+ "dependencies": {
+ "@patternfly/patternfly": "5.2.1",
+ "@patternfly/react-core": "5.2.1",
+ "@patternfly/react-icons": "5.2.1",
+ "@patternfly/react-styles": "5.2.1",
+ "@patternfly/react-table": "5.2.1",
+ "@patternfly/react-tokens": "5.2.1",
+ "date-fns": "3.4.0",
+ "docker-names": "1.2.1",
+ "ipaddr.js": "2.1.0",
+ "prop-types": "15.8.1",
+ "react": "18.2.0",
+ "react-dom": "18.2.0",
+ "throttle-debounce": "5.0.0",
+ "xterm": "5.1.0",
+ "xterm-addon-canvas": "0.4.0"
+ }
+}
diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD
new file mode 100644
index 0000000..f530e28
--- /dev/null
+++ b/packaging/arch/PKGBUILD
@@ -0,0 +1,15 @@
+pkgname=cockpit-podman
+pkgver=85
+pkgrel=1
+pkgdesc='Cockpit UI for podman containers'
+arch=('any')
+url='https://github.com/cockpit-project/cockpit-podman'
+license=(LGPL)
+depends=(cockpit podman)
+source=("cockpit-podman-85.tar.xz")
+sha256sums=('SKIP')
+
+package() {
+ cd $pkgname
+ make DESTDIR="$pkgdir" install PREFIX=/usr
+}
diff --git a/packaging/debian/changelog b/packaging/debian/changelog
new file mode 100644
index 0000000..6be2be8
--- /dev/null
+++ b/packaging/debian/changelog
@@ -0,0 +1,5 @@
+cockpit-podman (85-1) UNRELEASED; urgency=medium
+
+ * Upstream build
+
+ -- Cockpit <cockpit-devel@lists.fedorahosted.org> Thu, 23 Jul 2020 09:31:21 +0200
diff --git a/packaging/debian/control b/packaging/debian/control
new file mode 100644
index 0000000..27a0055
--- /dev/null
+++ b/packaging/debian/control
@@ -0,0 +1,23 @@
+Source: cockpit-podman
+Section: admin
+Priority: optional
+Maintainer: Martin Pitt <mpitt@debian.org>
+Uploaders: Reinhard Tartler <siretart@tauware.de>
+Build-Depends: debhelper-compat (= 13),
+Standards-Version: 4.6.2
+Rules-Requires-Root: no
+Homepage: https://github.com/cockpit-project/cockpit-podman
+Vcs-Git: https://salsa.debian.org/debian/cockpit-podman.git
+Vcs-Browser: https://salsa.debian.org/debian/cockpit-podman
+
+Package: cockpit-podman
+Architecture: all
+Multi-Arch: foreign
+Depends: ${misc:Depends},
+ cockpit-bridge,
+ podman (>= 2.0.4),
+Description: Cockpit component for Podman containers
+ The Cockpit Web Console enables users to administer GNU/Linux servers using a
+ web browser.
+ .
+ This package adds an user interface for Podman containers.
diff --git a/packaging/debian/copyright b/packaging/debian/copyright
new file mode 100644
index 0000000..40efb9e
--- /dev/null
+++ b/packaging/debian/copyright
@@ -0,0 +1,38 @@
+Format: https://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
+Upstream-Name: cockpit-podman
+Source: https://github.com/cockpit-project/cockpit-podman
+Comment:
+ This does not directly cover the files in dist/*. These are "minified" and
+ compressed JavaScript/HTML files built from src/, lib/, po/, and node_modules/
+ with node, npm, and a bundler. node_modules/ is not shipped as part of the
+ upstream release tarballs, but can be reconstructed precisely through the
+ shipped package-lock.json with the command "npm install". Rebuilding files in
+ dist/ requires internet access as that process needs to download additional
+ npm modules from the Internet, thus upstream ships the pre-minified bundles
+ as part of the upstream release tarball so that the package can be built
+ without internet access and lots of extra unpackaged build dependencies.
+
+Files: *
+Copyright: 2016-2020 Red Hat, Inc.
+License: LGPL-2.1
+ This package is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+ .
+ This package is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+ .
+ You should have received a copy of the GNU General Public License
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
+ .
+ On Debian systems, the complete text of the GNU Lesser General
+ Public License can be found in "/usr/share/common-licenses/LGPL-2.1".
+
+Files: *.metainfo.xml
+Copyright: Copyright (C) 2018 Red Hat, Inc.
+License: CC0-1.0
+ On Debian systems, the complete text of the Creative Commons Zero v1.0
+ Universal Public License is in "/usr/share/common-licenses/LGPL-2.1".
diff --git a/packaging/debian/rules b/packaging/debian/rules
new file mode 100755
index 0000000..c349516
--- /dev/null
+++ b/packaging/debian/rules
@@ -0,0 +1,13 @@
+#!/usr/bin/make -f
+
+export PREFIX=/usr
+
+%:
+ dh $@
+
+override_dh_auto_clean:
+ # don't call `make clean`, in a release dist/ is precious
+ rm -f po/LINGUAS
+
+override_dh_auto_test:
+ # don't call `make check`, these are integration tests
diff --git a/packaging/debian/source/format b/packaging/debian/source/format
new file mode 100644
index 0000000..163aaf8
--- /dev/null
+++ b/packaging/debian/source/format
@@ -0,0 +1 @@
+3.0 (quilt)
diff --git a/packaging/debian/source/lintian-overrides b/packaging/debian/source/lintian-overrides
new file mode 100644
index 0000000..5c4b8cd
--- /dev/null
+++ b/packaging/debian/source/lintian-overrides
@@ -0,0 +1,2 @@
+# source contains NPM modules required for running browser integration tests
+cockpit-podman source: source-is-missing *node_modules/*
diff --git a/packaging/debian/upstream/metadata b/packaging/debian/upstream/metadata
new file mode 100644
index 0000000..d1cdcd6
--- /dev/null
+++ b/packaging/debian/upstream/metadata
@@ -0,0 +1,4 @@
+---
+Bug-Database: https://github.com/cockpit-project/cockpit-podman/issues
+Bug-Submit: https://github.com/cockpit-project/cockpit-podman/issues/new
+Repository-Browse: https://github.com/cockpit-project/cockpit-podman
diff --git a/packaging/debian/watch b/packaging/debian/watch
new file mode 100644
index 0000000..cfbae0b
--- /dev/null
+++ b/packaging/debian/watch
@@ -0,0 +1,5 @@
+version=4
+opts="searchmode=plain, \
+filenamemangle=s/.+\/@PACKAGE@-@ANY_VERSION@.tar.gz/@PACKAGE@-$1\.tar\.xz/" \
+https://api.github.com/repos/cockpit-project/@PACKAGE@/releases \
+https://github.com/cockpit-project/@PACKAGE@/releases/download/\d[\.\d]*/@PACKAGE@-@ANY_VERSION@.tar.xz
diff --git a/packit.yaml b/packit.yaml
new file mode 100644
index 0000000..30315f3
--- /dev/null
+++ b/packit.yaml
@@ -0,0 +1,80 @@
+upstream_project_url: https://github.com/cockpit-project/cockpit-podman
+# enable notification of failed downstream jobs as issues
+issue_repository: https://github.com/cockpit-project/cockpit-podman
+specfile_path: cockpit-podman.spec
+upstream_package_name: cockpit-podman
+downstream_package_name: cockpit-podman
+# use the nicely formatted release description from our upstream release, instead of git shortlog
+copy_upstream_release_description: true
+
+actions:
+ post-upstream-clone: make cockpit-podman.spec
+ create-archive: make dist
+
+srpm_build_deps:
+ - make
+ - npm
+
+jobs:
+ - job: copr_build
+ trigger: pull_request
+ targets:
+ - fedora-39
+ - fedora-40
+ - fedora-latest-aarch64
+ - fedora-development
+ - centos-stream-9-x86_64
+ - centos-stream-9-aarch64
+ - centos-stream-8-x86_64
+
+ - job: tests
+ trigger: pull_request
+ targets:
+ - fedora-39
+ - fedora-40
+ - fedora-latest-aarch64
+ - fedora-development
+ - centos-stream-9-x86_64
+ - centos-stream-9-aarch64
+ - centos-stream-8-x86_64
+
+ - job: copr_build
+ trigger: release
+ owner: "@cockpit"
+ project: "cockpit-preview"
+ preserve_project: True
+ actions:
+ post-upstream-clone: make cockpit-podman.spec
+ # HACK: tarball for releases (copr_build, koji, etc.), copying spec's Source0; this
+ # really should be the default, see https://github.com/packit/packit-service/issues/1505
+ create-archive:
+ - sh -exc "curl -L -O https://github.com/cockpit-project/cockpit-podman/releases/download/${PACKIT_PROJECT_VERSION}/${PACKIT_PROJECT_NAME_VERSION}.tar.xz"
+ - sh -exc "ls ${PACKIT_PROJECT_NAME_VERSION}.tar.xz"
+
+ - job: copr_build
+ trigger: commit
+ branch: "^main$"
+ owner: "@cockpit"
+ project: "main-builds"
+ preserve_project: True
+
+ - job: propose_downstream
+ trigger: release
+ dist_git_branches:
+ - fedora-development
+ - fedora-39
+ - fedora-40
+
+ - job: koji_build
+ trigger: commit
+ dist_git_branches:
+ - fedora-development
+ - fedora-39
+ - fedora-40
+
+ - job: bodhi_update
+ trigger: commit
+ dist_git_branches:
+ # rawhide updates are created automatically
+ - fedora-39
+ - fedora-40
diff --git a/pkg/lib/README b/pkg/lib/README
new file mode 100644
index 0000000..7677e53
--- /dev/null
+++ b/pkg/lib/README
@@ -0,0 +1,5 @@
+# Cockpit shared components
+
+This directory contains React components, JavaScript modules, esbuild
+plugins, and build tools which are shared between all Cockpit projects.
+External projects usually clone this directory into their own source tree.
diff --git a/pkg/lib/_global-variables.scss b/pkg/lib/_global-variables.scss
new file mode 100644
index 0000000..e2b9935
--- /dev/null
+++ b/pkg/lib/_global-variables.scss
@@ -0,0 +1,14 @@
+@import "@patternfly/patternfly/sass-utilities/all";
+
+/*
+ * PatternFly 4 adapting the lists too early.
+ * When PF4 has a breakpoint of 768px width, it's actually 1108 for us, as the sidebar is 340px.
+ * (This does use the intended content area, but there's a mismatch between content and browser width as we use iframes.)
+ * So redefine grid breakpoints
+ */
+$pf-v5-global--breakpoint--xs: 0 !default;
+// Do not override the sm breakpoint as for width < 768px the left nav is hidden
+$pf-v5-global--breakpoint--md: 428px !default;
+$pf-v5-global--breakpoint--lg: 652px !default;
+$pf-v5-global--breakpoint--xl: 860px !default;
+$pf-v5-global--breakpoint--2xl: 1110px !default;
diff --git a/pkg/lib/cockpit-components-context-menu.jsx b/pkg/lib/cockpit-components-context-menu.jsx
new file mode 100644
index 0000000..55c55de
--- /dev/null
+++ b/pkg/lib/cockpit-components-context-menu.jsx
@@ -0,0 +1,110 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+
+import { Menu, MenuContent } from "@patternfly/react-core/dist/esm/components/Menu";
+
+import "context-menu.scss";
+
+/*
+ * A context menu component
+ *
+ * It requires two properties:
+ * - parentId, area in which it listens to left button click
+ * - children, a MenuList to be rendered in the context menu
+ */
+export const ContextMenu = ({ parentId, children }) => {
+ const [visible, setVisible] = React.useState(false);
+ const [event, setEvent] = React.useState(null);
+ const root = React.useRef(null);
+
+ React.useEffect(() => {
+ const _handleContextMenu = (event) => {
+ event.preventDefault();
+
+ setVisible(true);
+ setEvent(event);
+ };
+
+ const _handleClick = (event) => {
+ if (event && event.button === 0) {
+ const wasOutside = !(event.target.contains === root.current);
+
+ if (wasOutside)
+ setVisible(false);
+ }
+ };
+
+ const parent = document.getElementById(parentId);
+ parent.addEventListener('contextmenu', _handleContextMenu);
+ document.addEventListener('click', _handleClick);
+
+ return () => {
+ parent.removeEventListener('contextmenu', _handleContextMenu);
+ document.removeEventListener('click', _handleClick);
+ };
+ }, [parentId]);
+
+ React.useEffect(() => {
+ if (!event)
+ return;
+
+ const clickX = event.clientX;
+ const clickY = event.clientY;
+ const screenW = window.innerWidth;
+ const screenH = window.innerHeight;
+ const rootW = root.current.offsetWidth;
+ const rootH = root.current.offsetHeight;
+
+ const right = (screenW - clickX) > rootW;
+ const left = !right;
+ const top = (screenH - clickY) > rootH;
+ const bottom = !top;
+
+ if (right) {
+ root.current.style.left = `${clickX + 5}px`;
+ }
+
+ if (left) {
+ root.current.style.left = `${clickX - rootW - 5}px`;
+ }
+
+ if (top) {
+ root.current.style.top = `${clickY + 5}px`;
+ }
+
+ if (bottom) {
+ root.current.style.top = `${clickY - rootH - 5}px`;
+ }
+ }, [event]);
+
+ return visible &&
+ <Menu ref={root} className="contextMenu">
+ <MenuContent ref={root}>
+ {children}
+ </MenuContent>
+ </Menu>;
+};
+
+ContextMenu.propTypes = {
+ parentId: PropTypes.string.isRequired,
+ children: PropTypes.any
+};
diff --git a/pkg/lib/cockpit-components-dialog.jsx b/pkg/lib/cockpit-components-dialog.jsx
new file mode 100644
index 0000000..773516d
--- /dev/null
+++ b/pkg/lib/cockpit-components-dialog.jsx
@@ -0,0 +1,372 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { createRoot } from "react-dom/client";
+import PropTypes from "prop-types";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack/index.js";
+import { HelpIcon, ExternalLinkAltIcon } from '@patternfly/react-icons';
+
+import "cockpit-components-dialog.scss";
+
+const _ = cockpit.gettext;
+
+/*
+ * React template for a Cockpit dialog footer
+ * It can wait for an action to complete,
+ * has a 'Cancel' button and an action button (defaults to 'OK')
+ * Expected props:
+ * - cancel_clicked optional
+ * Callback called when the dialog is canceled
+ * - cancel_button optional, defaults to 'Cancel' text styled as a link
+ * - list of actions, each an object with:
+ * - clicked
+ * Callback function that is expected to return a promise.
+ * parameter: callback to set the progress text
+ * - caption optional, defaults to 'Ok'
+ * - disabled optional, defaults to false
+ * - style defaults to 'secondary', other options: 'primary', 'danger'
+ * - idle_message optional, always show this message on the last row when idle
+ * - dialog_done optional, callback when dialog is finished (param true if success, false on cancel)
+ * - set_error: required, callback to set/clear error message from actions
+ */
+class DialogFooter extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ action_in_progress: false,
+ action_in_progress_promise: null,
+ action_progress_message: '',
+ action_progress_cancel: null,
+ action_canceled: false,
+ };
+ this.update_progress = this.update_progress.bind(this);
+ this.cancel_click = this.cancel_click.bind(this);
+ }
+
+ update_progress(msg, cancel) {
+ this.setState({ action_progress_message: msg, action_progress_cancel: cancel });
+ }
+
+ action_click(handler, caption, e) {
+ this.setState({
+ action_progress_message: '',
+ action_in_progress: true,
+ action_caption_in_progress: caption,
+ action_canceled: false,
+ });
+
+ const p = handler(this.update_progress)
+ .then(() => {
+ this.props.set_error(null);
+ this.setState({ action_in_progress: false });
+ if (this.props.dialog_done)
+ this.props.dialog_done(true);
+ })
+ .catch(error => {
+ if (this.state.action_canceled) {
+ if (this.props.dialog_done)
+ this.props.dialog_done(false);
+ } else {
+ this.props.set_error(error);
+ this.setState({ action_in_progress: false });
+ }
+ /* Always log global dialog errors for easier debugging */
+ if (error)
+ console.warn(error.message || error.toString());
+ });
+
+ if (p.progress)
+ p.progress(this.update_progress);
+
+ this.setState({ action_in_progress_promise: p });
+ if (e)
+ e.stopPropagation();
+ }
+
+ cancel_click(e) {
+ this.setState({ action_canceled: true });
+
+ if (this.props.cancel_clicked)
+ this.props.cancel_clicked();
+
+ // an action might be in progress, let that handler decide what to do if they added a cancel function
+ if (this.state.action_in_progress && this.state.action_progress_cancel) {
+ this.state.action_progress_cancel();
+ return;
+ }
+ if (this.state.action_in_progress && 'cancel' in this.state.action_in_progress_promise) {
+ this.state.action_in_progress_promise.cancel();
+ return;
+ }
+
+ if (this.props.dialog_done)
+ this.props.dialog_done(false);
+ if (e)
+ e.stopPropagation();
+ }
+
+ render() {
+ const cancel_text = this.props?.cancel_button?.text ?? _("Cancel");
+ const cancel_variant = this.props?.cancel_button?.variant ?? "link";
+
+ // If an action is in progress, show the spinner with its message and disable all actions.
+ // Cancel is only enabled when the action promise has a cancel method, or we get one
+ // via the progress reporting.
+
+ let wait_element;
+ let actions_disabled;
+ let cancel_disabled;
+ if (this.state.action_in_progress) {
+ actions_disabled = true;
+ if (!(this.state.action_in_progress_promise && this.state.action_in_progress_promise.cancel) && !this.state.action_progress_cancel)
+ cancel_disabled = true;
+ wait_element = <div className="dialog-wait-ct">
+ <span>{ this.state.action_progress_message }</span>
+ </div>;
+ } else if (this.props.idle_message) {
+ wait_element = <div className="dialog-wait-ct">
+ { this.props.idle_message }
+ </div>;
+ }
+
+ const action_buttons = this.props.actions.map(action => {
+ let caption;
+ if ('caption' in action)
+ caption = action.caption;
+ else
+ caption = _("Ok");
+
+ let variant = action.style || "secondary";
+ if (variant == "primary" && action.danger)
+ variant = "danger";
+
+ return (<Button
+ key={ caption }
+ className="apply"
+ variant={ variant }
+ isLoading={ this.state.action_in_progress && this.state.action_caption_in_progress == caption }
+ isDanger={ action.danger }
+ onClick={ this.action_click.bind(this, action.clicked, caption) }
+ isDisabled={ actions_disabled || action.disabled }
+ >{ caption }</Button>
+ );
+ });
+
+ return (
+ <>
+ { this.props.extra_element }
+ { action_buttons }
+ <Button variant={cancel_variant} className="cancel" onClick={this.cancel_click} isDisabled={cancel_disabled}>{ cancel_text }</Button>
+ { wait_element }
+ </>
+ );
+ }
+}
+
+DialogFooter.propTypes = {
+ cancel_clicked: PropTypes.func,
+ cancel_button: PropTypes.object,
+ actions: PropTypes.array.isRequired,
+ dialog_done: PropTypes.func,
+ set_error: PropTypes.func.isRequired,
+};
+
+/*
+ * React template for a Cockpit dialog
+ * The primary action button is disabled while its action is in progress (waiting for promise)
+ * Removes focus on other elements on showing
+ * Expected props:
+ * - title (string)
+ * - body (react element, top element should be of class modal-body)
+ * It is recommended for information gathering dialogs to pass references
+ * to the input components to the controller. That way, the controller can
+ * extract all necessary information (e.g. for input validation) when an
+ * action is triggered.
+ * - static_error optional, always show this error after the body element
+ * - footer (react element, top element should be of class modal-footer)
+ * - id optional, id that is assigned to the top level dialog node, but not the backdrop
+ * - variant: See PF4 Modal component's 'variant' property
+ * - titleIconVariant: See PF4 Modal component's 'titleIconVariant' property
+ * - showClose optional, specifies if 'X' button for closing the dialog is present
+ */
+class Dialog extends React.Component {
+ componentDidMount() {
+ // For the scenario that cockpit-storage is used inside anaconda Web UI
+ // We need to know if there is an open dialog in order to create the backdrop effect
+ // on the parent window
+ window.sessionStorage.setItem("cockpit_has_modal", true);
+
+ // if we used a button to open this, make sure it's not focused anymore
+ if (document.activeElement)
+ document.activeElement.blur();
+ }
+
+ componentWillUnmount() {
+ window.sessionStorage.setItem("cockpit_has_modal", false);
+ }
+
+ render() {
+ let help = null;
+ let footer = null;
+ if (this.props.helpLink)
+ footer = <a href={this.props.helpLink} target="_blank" rel="noopener noreferrer">{_("Learn more")} <ExternalLinkAltIcon /></a>;
+
+ if (this.props.helpMessage)
+ help = <Popover
+ bodyContent={this.props.helpMessage}
+ footerContent={footer}
+ >
+ <Button variant="plain" aria-label={_("Learn more")}>
+ <HelpIcon />
+ </Button>
+ </Popover>;
+
+ const error = this.props.error || this.props.static_error;
+ const error_alert = error && <Alert variant='danger' isInline title={error} />;
+
+ return (
+ <Modal position="top" variant={this.props.variant || "medium"}
+ titleIconVariant={this.props.titleIconVariant}
+ onEscapePress={() => undefined}
+ showClose={!!this.props.showClose}
+ id={this.props.id}
+ isOpen
+ help={help}
+ footer={this.props.footer} title={this.props.title}>
+ <Stack hasGutter>
+ { error_alert }
+ <StackItem>
+ { this.props.body }
+ </StackItem>
+ </Stack>
+ </Modal>
+ );
+ }
+}
+Dialog.propTypes = {
+ // TODO: fix following by refactoring the logic showing modal dialog (recently show_modal_dialog())
+ title: PropTypes.string, // is effectively required, but show_modal_dialog() provides initially no props and resets them later.
+ body: PropTypes.element, // is effectively required, see above
+ static_error: PropTypes.string,
+ error: PropTypes.string,
+ footer: PropTypes.element, // is effectively required, see above
+ id: PropTypes.string,
+ showClose: PropTypes.bool,
+};
+
+/* Create and show a dialog
+ * For this, create a containing DOM node at the body level
+ * The returned object has the following methods:
+ * - setFooterProps replace the current footerProps and render
+ * - setProps replace the current props and render
+ * - render render again using the stored props
+ * The DOM node and React metadata are freed once the dialog has closed
+ */
+export function show_modal_dialog(props, footerProps) {
+ const dialogName = 'cockpit_modal_dialog';
+ // don't allow nested dialogs, just close whatever is open
+ const curElement = document.getElementById(dialogName);
+ let root;
+ if (curElement) {
+ root = createRoot(curElement);
+ root.unmount();
+ curElement.remove();
+ }
+ // create an element to render into
+ const rootElement = document.createElement("div");
+ root = createRoot(rootElement);
+ rootElement.id = dialogName;
+ document.body.appendChild(rootElement);
+
+ // register our own on-close callback
+ let origCallback;
+ const closeCallback = function() {
+ if (origCallback)
+ origCallback.apply(this, arguments);
+ root.unmount();
+ rootElement.remove();
+ };
+
+ const dialogObj = { };
+ let error = null;
+ dialogObj.props = props;
+ dialogObj.footerProps = null;
+ dialogObj.render = function() {
+ dialogObj.props.footer = <DialogFooter {...dialogObj.footerProps} />;
+ // Don't render if we are no longer part of the document.
+ // This would be mostly harmless except that it will remove
+ // the input focus from whatever element has it, which is
+ // unpleasant and also disrupts the tests.
+ if (rootElement.offsetParent)
+ root.render(<Dialog {...dialogObj.props} error={error} />);
+ };
+ function updateFooterAndRender() {
+ if (dialogObj.props === null || dialogObj.props === undefined)
+ dialogObj.props = { };
+ dialogObj.props.footer = <DialogFooter {...dialogObj.footerProps} />;
+ dialogObj.render();
+ }
+ dialogObj.setFooterProps = function(footerProps) {
+ dialogObj.footerProps = footerProps;
+ if (dialogObj.footerProps.dialog_done != closeCallback) {
+ origCallback = dialogObj.footerProps.dialog_done;
+ dialogObj.footerProps.dialog_done = closeCallback;
+ }
+ dialogObj.footerProps.set_error = e => {
+ error = typeof e === 'object' && e !== null ? (e.message || e.toString()) : e;
+ dialogObj.render();
+ };
+ updateFooterAndRender();
+ };
+ dialogObj.setProps = function(props) {
+ dialogObj.props = props;
+ updateFooterAndRender();
+ };
+ dialogObj.setFooterProps(footerProps);
+ dialogObj.setProps(props);
+
+ // now actually render
+ dialogObj.render();
+
+ return dialogObj;
+}
+
+export function apply_modal_dialog(event) {
+ const dialog = event.target?.closest("[role=dialog]");
+ const button = dialog?.querySelector("button.apply");
+
+ if (button) {
+ const event = new MouseEvent('click', {
+ view: window,
+ bubbles: true,
+ cancelable: true,
+ button: 0
+ });
+ button.dispatchEvent(event);
+ }
+
+ event.preventDefault();
+ return false;
+}
diff --git a/pkg/lib/cockpit-components-dialog.scss b/pkg/lib/cockpit-components-dialog.scss
new file mode 100644
index 0000000..16e42a7
--- /dev/null
+++ b/pkg/lib/cockpit-components-dialog.scss
@@ -0,0 +1,8 @@
+.pf-v5-c-modal-box__body .scroll {
+ max-block-size: calc(75vh - 10rem);
+ overflow: auto;
+}
+
+.dialog-wait-ct-spinner {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
diff --git a/pkg/lib/cockpit-components-dropdown.jsx b/pkg/lib/cockpit-components-dropdown.jsx
new file mode 100644
index 0000000..14efa9f
--- /dev/null
+++ b/pkg/lib/cockpit-components-dropdown.jsx
@@ -0,0 +1,76 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState } from 'react';
+import PropTypes from "prop-types";
+
+import { MenuToggle } from "@patternfly/react-core/dist/esm/components/MenuToggle";
+import { Dropdown, DropdownList } from "@patternfly/react-core/dist/esm/components/Dropdown";
+
+import { EllipsisVIcon } from '@patternfly/react-icons';
+
+/*
+ * A dropdown with a Kebab button, commonly used in Cockpit pages provided as
+ * component so not all pages have to re-invent the wheel.
+ *
+ * This component expects a list of (non-deprecated!) DropdownItem's, if you
+ * require a separator between DropdownItem's use PatternFly's Divivder
+ * component.
+ */
+export const KebabDropdown = ({ dropdownItems, position, isDisabled, toggleButtonId, props }) => {
+ const [isKebabOpen, setKebabOpen] = useState(false);
+
+ return (
+ <Dropdown
+ {...props}
+ onOpenChange={isOpen => setKebabOpen(isOpen)}
+ onSelect={() => setKebabOpen(!isKebabOpen)}
+ toggle={(toggleRef) => (
+ <MenuToggle
+ id={toggleButtonId}
+ isDisabled={isDisabled}
+ ref={toggleRef}
+ variant="plain"
+ onClick={() => setKebabOpen(!isKebabOpen)}
+ isExpanded={isKebabOpen}
+ >
+ <EllipsisVIcon />
+ </MenuToggle>
+ )}
+ isOpen={isKebabOpen}
+ popperProps={{ position }}
+ >
+ <DropdownList>
+ {dropdownItems}
+ </DropdownList>
+ </Dropdown>
+ );
+};
+
+KebabDropdown.propTypes = {
+ dropdownItems: PropTypes.array.isRequired,
+ isDisabled: PropTypes.bool,
+ toggleButtonId: PropTypes.string,
+ position: PropTypes.oneOf(['right', 'left', 'center', 'start', 'end']),
+};
+
+KebabDropdown.defaultProps = {
+ isDisabled: false,
+ position: "end",
+};
diff --git a/pkg/lib/cockpit-components-dynamic-list.jsx b/pkg/lib/cockpit-components-dynamic-list.jsx
new file mode 100644
index 0000000..12c3760
--- /dev/null
+++ b/pkg/lib/cockpit-components-dynamic-list.jsx
@@ -0,0 +1,143 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { EmptyState, EmptyStateBody } from "@patternfly/react-core/dist/esm/components/EmptyState";
+import { FormFieldGroup, FormFieldGroupHeader } from "@patternfly/react-core/dist/esm/components/Form";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText";
+
+import './cockpit-components-dynamic-list.scss';
+
+/* Dynamic list with a variable number of rows. Each row is a custom component, usually an input field(s).
+ *
+ * Props:
+ * - emptyStateString
+ * - onChange
+ * - id
+ * - itemcomponent
+ * - formclass (optional)
+ * - options (optional)
+ * - onValidationChange: A handler function which updates the parent's component's validation object.
+ * Its parameter is an array the same structure as 'validationFailed'.
+ * - validationFailed: An array where each item represents a validation error of the corresponding row component index.
+ * A row is strictly mapped to an item of the array by its index.
+ * Example: Let's have a dynamic form, where each row consists of 2 fields: name and email. Then a validation array of
+ * these rows would look like this:
+ * [
+ * { name: "Name must not be empty }, // first row
+ * { }, // second row
+ * { name: "Name cannot containt number", email: "Email must contain '@'" } // third row
+ * ]
+ */
+export class DynamicListForm extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ list: [],
+ };
+ this.keyCounter = 0;
+ this.removeItem = this.removeItem.bind(this);
+ this.addItem = this.addItem.bind(this);
+ this.onItemChange = this.onItemChange.bind(this);
+ }
+
+ removeItem(idx) {
+ const validationFailedDelta = this.props.validationFailed ? [...this.props.validationFailed] : [];
+ // We also need to remove any error messages which the item (row) may have contained
+ validationFailedDelta.splice(idx, 1);
+ this.props.onValidationChange?.(validationFailedDelta);
+
+ this.setState(state => {
+ const items = [...state.list];
+ // keep the list structure, otherwise all the indexes shift and the ID/key mapping gets broken
+ delete items[idx];
+
+ return { list: items };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ addItem() {
+ this.setState(state => {
+ return { list: [...state.list, { key: this.keyCounter++, ...this.props.default }] };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ onItemChange(idx, field, value) {
+ this.setState(state => {
+ const items = [...state.list];
+ items[idx][field] = value || null;
+ return { list: items };
+ }, () => this.props.onChange(this.state.list));
+ }
+
+ render () {
+ const { id, label, actionLabel, formclass, emptyStateString, helperText, validationFailed, onValidationChange } = this.props;
+ const dialogValues = this.state;
+ return (
+ <FormFieldGroup header={
+ <FormFieldGroupHeader
+ titleText={{ text: label }}
+ actions={<Button variant="secondary" className="btn-add" onClick={this.addItem}>{actionLabel}</Button>}
+ />
+ } className={"dynamic-form-group " + formclass}>
+ {
+ dialogValues.list.some(item => item !== undefined)
+ ? <>
+ {dialogValues.list.map((item, idx) => {
+ if (item === undefined)
+ return null;
+
+ return React.cloneElement(this.props.itemcomponent, {
+ idx,
+ item,
+ id: id + "-" + idx,
+ key: idx,
+ onChange: this.onItemChange,
+ removeitem: this.removeItem,
+ additem: this.addItem,
+ options: this.props.options,
+ validationFailed: validationFailed && validationFailed[idx],
+ onValidationChange: value => {
+ // Dynamic list consists of multiple rows. Therefore validationFailed object is presented as an array where each item represents a row
+ // Each row/item then consists of key-value pairs, which represent a field name and it's validation error
+ const delta = validationFailed ? [...validationFailed] : [];
+ // Update validation of only a single row
+ delta[idx] = value;
+
+ // If a row doesn't contain any fields with errors anymore, we delete the item of the array
+ // Deleting an item of an array replaces an item with an "empty item".
+ // This guarantees that an array of validation errors maps to the correct rows
+ if (Object.keys(delta[idx]).length == 0)
+ delete delta[idx];
+
+ onValidationChange?.(delta);
+ },
+ });
+ })
+ }
+ {helperText &&
+ <HelperText>
+ <HelperTextItem>{helperText}</HelperTextItem>
+ </HelperText>
+ }
+ </>
+ : <EmptyState>
+ <EmptyStateBody>
+ {emptyStateString}
+ </EmptyStateBody>
+ </EmptyState>
+ }
+ </FormFieldGroup>
+ );
+ }
+}
+
+DynamicListForm.propTypes = {
+ emptyStateString: PropTypes.string.isRequired,
+ onChange: PropTypes.func.isRequired,
+ id: PropTypes.string.isRequired,
+ itemcomponent: PropTypes.object.isRequired,
+ formclass: PropTypes.string,
+ options: PropTypes.object,
+ validationFailed: PropTypes.array,
+ onValidationChange: PropTypes.func,
+};
diff --git a/pkg/lib/cockpit-components-dynamic-list.scss b/pkg/lib/cockpit-components-dynamic-list.scss
new file mode 100644
index 0000000..2016fda
--- /dev/null
+++ b/pkg/lib/cockpit-components-dynamic-list.scss
@@ -0,0 +1,39 @@
+@import "global-variables";
+
+.dynamic-form-group {
+ .pf-v5-c-empty-state {
+ padding: 0;
+ }
+
+ .pf-v5-c-form__label {
+ // Don't allow labels to wrap
+ white-space: nowrap;
+ }
+
+ .remove-button-group {
+ // Move 'Remove' button the the end of the row
+ grid-column: -1;
+ // Move 'Remove' button to the bottom of the line so as to align with the other form fields
+ display: flex;
+ align-items: flex-end;
+ }
+
+ // Set check to the same height as input widgets and vertically align
+ .pf-v5-c-form__group-control > .pf-v5-c-check {
+ // Set height to the same as inputs
+ // Font height is font size * line height (1rem * 1.5)
+ // Widgets have 5px padding, 1px border (top & bottom): (5 + 1) * 2 = 12
+ // This all equals to 36px
+ block-size: calc(var(--pf-v5-global--FontSize--md) * var(--pf-v5-global--LineHeight--md) + 12px);
+ align-content: center;
+ }
+
+ // We use FormFieldGroup PF component for the nested look and for ability to add buttons to the header
+ // However we want to save space and not add indent to the left so we need to override it
+ .pf-v5-c-form__field-group-body {
+ // Stretch content fully
+ --pf-v5-c-form__field-group-body--GridColumn: 1 / -1;
+ // Reduce padding at the top
+ --pf-v5-c-form__field-group-body--PaddingTop: var(--pf-v5-global--spacer--xs);
+ }
+}
diff --git a/pkg/lib/cockpit-components-empty-state.css b/pkg/lib/cockpit-components-empty-state.css
new file mode 100644
index 0000000..06ba60e
--- /dev/null
+++ b/pkg/lib/cockpit-components-empty-state.css
@@ -0,0 +1,3 @@
+.pf-v5-c-empty-state .pf-v5-c-button.pf-m-primary.slim {
+ margin: 0;
+}
diff --git a/pkg/lib/cockpit-components-empty-state.jsx b/pkg/lib/cockpit-components-empty-state.jsx
new file mode 100644
index 0000000..467ba2b
--- /dev/null
+++ b/pkg/lib/cockpit-components-empty-state.jsx
@@ -0,0 +1,62 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from 'prop-types';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { EmptyStateActions, EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateHeader, EmptyStateIcon, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import "./cockpit-components-empty-state.css";
+
+export const EmptyStatePanel = ({ title, paragraph, loading, icon, action, isActionInProgress, onAction, secondary, headingLevel }) => {
+ const slimType = title || paragraph ? "" : "slim";
+ return (
+ <EmptyState variant={EmptyStateVariant.full}>
+ <EmptyStateHeader titleText={title} headingLevel={headingLevel} icon={(loading || icon) && <EmptyStateIcon icon={loading ? Spinner : icon} />} />
+ <EmptyStateBody>
+ {paragraph}
+ </EmptyStateBody>
+ {(action || secondary) && <EmptyStateFooter>
+ { action && (typeof action == "string"
+ ? <Button variant="primary" className={slimType}
+ isLoading={isActionInProgress}
+ isDisabled={isActionInProgress}
+ onClick={onAction}>{action}</Button>
+ : action)}
+ { secondary && <EmptyStateActions>{secondary}</EmptyStateActions> }
+ </EmptyStateFooter>}
+ </EmptyState>
+ );
+};
+
+EmptyStatePanel.propTypes = {
+ loading: PropTypes.bool,
+ icon: PropTypes.oneOfType([PropTypes.string, PropTypes.object, PropTypes.func]),
+ title: PropTypes.string,
+ paragraph: PropTypes.node,
+ action: PropTypes.node,
+ isActionInProgress: PropTypes.bool,
+ onAction: PropTypes.func,
+ secondary: PropTypes.node,
+};
+
+EmptyStatePanel.defaultProps = {
+ headingLevel: "h1",
+ isActionInProgress: false,
+};
diff --git a/pkg/lib/cockpit-components-file-autocomplete.jsx b/pkg/lib/cockpit-components-file-autocomplete.jsx
new file mode 100644
index 0000000..afe4d15
--- /dev/null
+++ b/pkg/lib/cockpit-components-file-autocomplete.jsx
@@ -0,0 +1,212 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import PropTypes from "prop-types";
+import { debounce } from 'throttle-debounce';
+
+const _ = cockpit.gettext;
+
+export class FileAutoComplete extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ directory: '', // The current directory we list files/dirs from
+ displayFiles: [],
+ isOpen: false,
+ value: this.props.value || null,
+ };
+
+ this.typeaheadInputValue = "";
+ this.allowFilesUpdate = true;
+ this.updateFiles = this.updateFiles.bind(this);
+ this.finishUpdate = this.finishUpdate.bind(this);
+ this.onToggle = this.onToggle.bind(this);
+ this.clearSelection = this.clearSelection.bind(this);
+ this.onCreateOption = this.onCreateOption.bind(this);
+
+ this.onPathChange = (value) => {
+ if (!value) {
+ this.clearSelection();
+ return;
+ }
+
+ this.typeaheadInputValue = value;
+
+ const cb = (dirPath) => this.updateFiles(dirPath == '' ? '/' : dirPath);
+
+ let path = value;
+ if (value.lastIndexOf('/') == value.length - 1)
+ path = value.slice(0, value.length - 1);
+
+ const match = this.state.displayFiles
+ .find(entry => (entry.type == 'directory' && entry.path == path + '/') || (entry.type == 'file' && entry.path == path));
+
+ if (match) {
+ // If match file path is a prefix of another file, do not update current directory,
+ // since we cannot tell file/directory user wants to select
+ // https://bugzilla.redhat.com/show_bug.cgi?id=2097662
+ const isPrefix = this.state.displayFiles.filter(entry => entry.path.startsWith(value)).length > 1;
+ // If the inserted string corresponds to a directory listed in the results
+ // update the current directory and refetch results
+ if (match.type == 'directory' && !isPrefix)
+ cb(match.path);
+ else
+ this.setState({ value: match.path });
+ } else {
+ // If the inserted string's parent directory is not matching the `directory`
+ // in the state object we need to update the parent directory and recreate the displayFiles
+ const parentDir = value.slice(0, value.lastIndexOf('/'));
+
+ if (parentDir + '/' != this.state.directory) {
+ return this.updateFiles(parentDir + '/');
+ }
+ }
+ };
+ this.debouncedChange = debounce(300, this.onPathChange);
+ this.onPathChange(this.state.value);
+ }
+
+ componentWillUnmount() {
+ this.allowFilesUpdate = false;
+ }
+
+ onCreateOption(newValue) {
+ this.setState(prevState => ({
+ displayFiles: [...prevState.displayFiles, { type: "file", path: newValue }]
+ }));
+ }
+
+ updateFiles(path) {
+ if (this.state.directory == path)
+ return;
+
+ const channel = cockpit.channel({
+ payload: "fslist1",
+ path,
+ superuser: this.props.superuser,
+ watch: false,
+ });
+ const results = [];
+
+ channel.addEventListener("ready", () => {
+ this.finishUpdate(results, null, path);
+ });
+
+ channel.addEventListener("close", (ev, data) => {
+ this.finishUpdate(results, data.message, path);
+ });
+
+ channel.addEventListener("message", (ev, data) => {
+ const item = JSON.parse(data);
+ if (item && item.path && item.event == 'present') {
+ item.path = item.path + (item.type == 'directory' ? '/' : '');
+ results.push(item);
+ }
+ });
+ }
+
+ finishUpdate(results, error, directory) {
+ if (!this.allowFilesUpdate)
+ return;
+ results = results.sort((a, b) => a.path.localeCompare(b.path, { sensitivity: 'base' }));
+
+ const listItems = results.map(file => ({
+ type: file.type,
+ path: (directory == '' ? '/' : directory) + file.path
+ }));
+
+ if (directory) {
+ listItems.unshift({
+ type: "directory",
+ path: directory
+ });
+ }
+
+ if (error || !this.state.value)
+ this.props.onChange('', error);
+
+ if (!error)
+ this.setState({ displayFiles: listItems, directory });
+ this.setState({
+ error,
+ });
+ }
+
+ onToggle(_, isOpen) {
+ this.setState({ isOpen });
+ }
+
+ clearSelection() {
+ this.typeaheadInputValue = "";
+ this.updateFiles("/");
+ this.setState({
+ value: null,
+ isOpen: false
+ });
+ this.props.onChange('', null);
+ }
+
+ render() {
+ const placeholder = this.props.placeholder || _("Path to file");
+
+ const selectOptions = this.state.displayFiles
+ .map(option => <SelectOption key={option.path}
+ className={option.type}
+ value={option.path} />);
+ return (
+ <Select
+ variant="typeahead"
+ id={this.props.id}
+ isInputValuePersisted
+ onTypeaheadInputChanged={this.debouncedChange}
+ placeholderText={placeholder}
+ noResultsFoundText={this.state.error || _("No such file or directory")}
+ selections={this.state.value}
+ onSelect={(_, value) => {
+ this.setState({ value, isOpen: false });
+ this.debouncedChange(value);
+ this.props.onChange(value || '', null);
+ }}
+ onToggle={this.onToggle}
+ onClear={this.clearSelection}
+ isOpen={this.state.isOpen}
+ isCreatable={this.props.isOptionCreatable}
+ createText={_("Create")}
+ onCreateOption={this.onCreateOption}
+ menuAppendTo="parent">
+ {selectOptions}
+ </Select>
+ );
+ }
+}
+FileAutoComplete.propTypes = {
+ id: PropTypes.string,
+ placeholder: PropTypes.string,
+ superuser: PropTypes.string,
+ isOptionCreatable: PropTypes.bool,
+ onChange: PropTypes.func,
+ value: PropTypes.string,
+};
+FileAutoComplete.defaultProps = {
+ isOptionCreatable: false,
+ onChange: () => '',
+};
diff --git a/pkg/lib/cockpit-components-firewalld-request.jsx b/pkg/lib/cockpit-components-firewalld-request.jsx
new file mode 100644
index 0000000..517b8bb
--- /dev/null
+++ b/pkg/lib/cockpit-components-firewalld-request.jsx
@@ -0,0 +1,167 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState } from 'react';
+import { Alert, AlertActionCloseButton, AlertActionLink } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Toolbar, ToolbarContent, ToolbarGroup, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar/index.js";
+import { PageSection } from "@patternfly/react-core/dist/esm/components/Page/index.js";
+
+import cockpit from 'cockpit';
+import './cockpit-components-firewalld-request.scss';
+
+const _ = cockpit.gettext;
+const firewalld = cockpit.dbus('org.fedoraproject.FirewallD1', { superuser: "try" });
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("firewall"))
+ console.debug.apply(console, arguments);
+}
+
+/* React component for an info alert to enable some new service in firewalld.
+ * Use this when enabling some network-facing service. The alert will only be shown
+ * if firewalld is running, has at least one active zone, and the service is not enabled
+ * in any zone yet. It will allow the user to enable the service in any active zone,
+ * or go to the firewall page for more fine-grained configuration.
+ *
+ * Properties:
+ * - service (string, required): firewalld service name
+ * - title (string, required): Human readable/translated alert title
+ * - pageSection (bool, optional, default false): Render the alert inside a <PageSection>
+ */
+export const FirewalldRequest = ({ service, title, pageSection }) => {
+ const [zones, setZones] = useState(null);
+ const [selectedZone, setSelectedZone] = useState(null);
+ const [zoneSelectorOpened, setZoneSelectorOpened] = useState(false);
+ const [enabledAnywhere, setEnabledAnywhere] = useState(null);
+ const [enableError, setEnableError] = useState(null);
+ debug("FirewalldRequest", service, "zones", JSON.stringify(zones), "selected zone", selectedZone, "enabledAnywhere", enabledAnywhere);
+
+ if (!service)
+ return null;
+
+ // query zones on component initialization
+ if (zones === null) {
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getActiveZones")
+ .then(([info]) => {
+ const names = Object.keys(info);
+ Promise.all(names.map(name => firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "getZoneSettings2", [name])))
+ .then(zoneInfos => {
+ setEnabledAnywhere(!!zoneInfos.find(zoneInfo => ((zoneInfo[0].services || {}).v || []).indexOf(service) >= 0));
+ setZones(names);
+ })
+ .catch(ex => {
+ console.warn("FirewalldRequest: getZoneSettings failed:", JSON.stringify(ex));
+ setZones([]);
+ });
+
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1", "getDefaultZone")
+ .then(([zone]) => setSelectedZone(zone))
+ .catch(ex => console.warn("FirewalldRequest: getDefaultZone failed:", JSON.stringify(ex)));
+ })
+ .catch(ex => {
+ // firewalld not running
+ debug("FirewalldRequest: getActiveZones failed, considering firewall inactive:", JSON.stringify(ex));
+ setZones([]);
+ });
+ }
+
+ const onAddService = () => {
+ firewalld.call("/org/fedoraproject/FirewallD1", "org.fedoraproject.FirewallD1.zone", "addService",
+ [selectedZone, service, 0])
+ // permanent config
+ .then(() => firewalld.call("/org/fedoraproject/FirewallD1/config",
+ "org.fedoraproject.FirewallD1.config",
+ "getZoneByName", [selectedZone]))
+ .then(([path]) => firewalld.call(path, "org.fedoraproject.FirewallD1.config.zone", "addService", [service]))
+ // all successful, hide alert
+ .then(() => setEnabledAnywhere(true))
+ .catch(ex => {
+ // may already be enabled in permanent config, that's ok
+ if (ex.message && ex.message.indexOf("ALREADY_ENABLED") >= 0) {
+ setEnabledAnywhere(true);
+ return;
+ }
+
+ setEnableError(ex.toString());
+ setEnabledAnywhere(true);
+ console.error("Failed to enable", service, "in firewalld:", JSON.stringify(ex));
+ });
+ };
+
+ let alert;
+
+ if (enableError) {
+ alert = (
+ <Alert isInline variant="warning"
+ title={ cockpit.format(_("Failed to enable $0 in firewalld"), service) }
+ actionClose={ <AlertActionCloseButton onClose={ () => setEnableError(null) } /> }
+ actionLinks={
+ <AlertActionLink onClick={() => cockpit.jump("/network/firewall")}>
+ { _("Visit firewall") }
+ </AlertActionLink>
+ }>
+ {enableError}
+ </Alert>
+ );
+ // don't show anything if firewalld is not active, or service is already enabled somewhere
+ } else if (!zones || zones.length === 0 || !selectedZone || enabledAnywhere) {
+ return null;
+ } else {
+ alert = (
+ <Alert isInline variant="info" title={title} className="pf-v5-u-box-shadow-sm">
+ <Toolbar className="ct-alert-toolbar">
+ <ToolbarContent>
+ <ToolbarGroup spaceItems={{ default: "spaceItemsMd" }}>
+ <ToolbarItem variant="label">{ _("Zone") }</ToolbarItem>
+ <ToolbarItem>
+ <Select
+ aria-label={_("Zone")}
+ onToggle={(_event, isOpen) => setZoneSelectorOpened(isOpen)}
+ isOpen={zoneSelectorOpened}
+ onSelect={ (e, sel) => { setSelectedZone(sel); setZoneSelectorOpened(false) } }
+ selections={selectedZone}
+ toggleId={"firewalld-request-" + service}>
+ { zones.map(zone => <SelectOption key={zone} value={zone}>{zone}</SelectOption>) }
+ </Select>
+ </ToolbarItem>
+
+ <ToolbarItem>
+ <Button variant="primary" onClick={onAddService}>{ cockpit.format(_("Add $0"), service) }</Button>
+ </ToolbarItem>
+ </ToolbarGroup>
+
+ <ToolbarItem variant="separator" />
+
+ <ToolbarItem>
+ <Button variant="link" onClick={() => cockpit.jump("/network/firewall")}>
+ { _("Visit firewall") }
+ </Button>
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ </Alert>
+ );
+ }
+
+ if (pageSection)
+ return <PageSection className="ct-no-bottom-padding">{alert}</PageSection>;
+ else
+ return alert;
+};
diff --git a/pkg/lib/cockpit-components-firewalld-request.scss b/pkg/lib/cockpit-components-firewalld-request.scss
new file mode 100644
index 0000000..3c27011
--- /dev/null
+++ b/pkg/lib/cockpit-components-firewalld-request.scss
@@ -0,0 +1,12 @@
+// Required for `pf-v5-u-box-shadow-sm` as inline alert don't have a box shadow
+@use "../../node_modules/@patternfly/patternfly/utilities/BoxShadow/box-shadow.css";
+
+.pf-v5-c-page__main-section.ct-no-bottom-padding {
+ --pf-v5-c-page__main-section--PaddingBottom: 0;
+}
+
+// <Toolbar> embedded into an <Alert>
+.pf-v5-c-toolbar.ct-alert-toolbar {
+ --pf-v5-c-toolbar--BackgroundColor: transparent;
+ --pf-v5-c-toolbar--PaddingBottom: 0;
+}
diff --git a/pkg/lib/cockpit-components-form-helper.jsx b/pkg/lib/cockpit-components-form-helper.jsx
new file mode 100644
index 0000000..86e7f11
--- /dev/null
+++ b/pkg/lib/cockpit-components-form-helper.jsx
@@ -0,0 +1,43 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2023 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+import { FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText";
+
+export const FormHelper = ({ helperText, helperTextInvalid, variant, icon, fieldId }) => {
+ const formHelperVariant = variant || (helperTextInvalid ? "error" : "default");
+
+ if (!(helperText || helperTextInvalid))
+ return null;
+
+ return (
+ <FormHelperText>
+ <HelperText>
+ <HelperTextItem
+ id={fieldId ? (fieldId + "-helper") : undefined}
+ variant={formHelperVariant}
+ icon={icon}>
+ {formHelperVariant === "error" ? helperTextInvalid : helperText}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>
+ );
+};
diff --git a/pkg/lib/cockpit-components-inline-notification.css b/pkg/lib/cockpit-components-inline-notification.css
new file mode 100644
index 0000000..3c428bc
--- /dev/null
+++ b/pkg/lib/cockpit-components-inline-notification.css
@@ -0,0 +1,7 @@
+.alert-link.more-button {
+ margin-inline-start: var(--pf-v5-global--spacer--sm);
+}
+
+.notification-message {
+ white-space: pre-wrap;
+}
diff --git a/pkg/lib/cockpit-components-inline-notification.jsx b/pkg/lib/cockpit-components-inline-notification.jsx
new file mode 100644
index 0000000..94985ce
--- /dev/null
+++ b/pkg/lib/cockpit-components-inline-notification.jsx
@@ -0,0 +1,96 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import PropTypes from 'prop-types';
+import cockpit from 'cockpit';
+
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import './cockpit-components-inline-notification.css';
+
+const _ = cockpit.gettext;
+
+function mouseClick(fun) {
+ return function (event) {
+ if (!event || event.button !== 0)
+ return;
+ event.preventDefault();
+ return fun(event);
+ };
+}
+
+export class InlineNotification extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ isDetail: false,
+ };
+
+ this.toggleDetail = this.toggleDetail.bind(this);
+ }
+
+ toggleDetail () {
+ this.setState({
+ isDetail: !this.state.isDetail,
+ });
+ }
+
+ render () {
+ const { text, detail, type, onDismiss } = this.props;
+
+ let detailButton = null;
+ if (detail) {
+ let detailButtonText = _("show more");
+ if (this.state.isDetail) {
+ detailButtonText = _("show less");
+ }
+
+ detailButton = (<Button variant="link" isInline className='alert-link more-button'
+ onClick={mouseClick(this.toggleDetail)}>{detailButtonText}</Button>);
+ }
+ const extraProps = {};
+ if (onDismiss)
+ extraProps.actionClose = <AlertActionCloseButton onClose={onDismiss} />;
+
+ return (
+ <Alert variant={type || 'danger'}
+ isLiveRegion={this.props.isLiveRegion}
+ isInline={this.props.isInline != undefined ? this.props.isInline : true}
+ title={<> {text} {detailButton} </>} {...extraProps}>
+ {this.state.isDetail && (<p>{detail}</p>)}
+ </Alert>
+ );
+ }
+}
+
+InlineNotification.propTypes = {
+ onDismiss: PropTypes.func,
+ isInline: PropTypes.bool,
+ text: PropTypes.string.isRequired, // main information to render
+ detail: PropTypes.string, // optional, more detailed information. If empty, the more/less button is not rendered.
+ type: PropTypes.string,
+};
+
+export const ModalError = ({ dialogError, dialogErrorDetail, id, isExpandable }) => {
+ return (
+ <Alert id={id} variant='danger' isInline title={dialogError} isExpandable={!!isExpandable}>
+ { typeof dialogErrorDetail === 'string' ? <p>{dialogErrorDetail}</p> : dialogErrorDetail }
+ </Alert>
+ );
+};
diff --git a/pkg/lib/cockpit-components-install-dialog.css b/pkg/lib/cockpit-components-install-dialog.css
new file mode 100644
index 0000000..5fdfb3e
--- /dev/null
+++ b/pkg/lib/cockpit-components-install-dialog.css
@@ -0,0 +1,43 @@
+.package-list-ct {
+ margin-block: 1em;
+ margin-inline: 0;
+ padding: 0;
+ max-inline-size: 110rem;
+ text-align: start;
+ box-sizing: border-box;
+}
+
+.package-list-ct li {
+ text-align: start;
+ box-sizing: border-box;
+ inline-size: 22rem;
+ padding-block: 0;
+ padding-inline: 1ex;
+ display: inline-block;
+ overflow: hidden;
+ white-space: nowrap;
+ text-overflow: ellipsis;
+}
+
+.scale-up-ct {
+ animation: dialogScaleUpCt 1s;
+}
+
+@keyframes dialogScaleUpCt {
+ 0% {
+ opacity: 0;
+ max-block-size: 0;
+ }
+
+ 25% {
+ opacity: 0;
+ }
+
+ 50% {
+ max-block-size: 100vh;
+ }
+
+ 100% {
+ opacity: 1;
+ }
+}
diff --git a/pkg/lib/cockpit-components-install-dialog.jsx b/pkg/lib/cockpit-components-install-dialog.jsx
new file mode 100644
index 0000000..0d933ae
--- /dev/null
+++ b/pkg/lib/cockpit-components-install-dialog.jsx
@@ -0,0 +1,211 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { WarningTriangleIcon } from "@patternfly/react-icons";
+
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import * as PK from "packagekit.js";
+
+import "cockpit-components-install-dialog.css";
+
+const _ = cockpit.gettext;
+
+// TODO - generalize this to arbitrary number of arguments (when needed)
+function format_to_fragments(fmt, arg) {
+ const index = fmt.indexOf("$0");
+ if (index >= 0)
+ return <>{fmt.slice(0, index)}{arg}{fmt.slice(index + 2)}</>;
+ else
+ return fmt;
+}
+
+/* Calling install_dialog will open a dialog that lets the user
+ * install the given package.
+ *
+ * The install_dialog function returns a promise that is fulfilled when the dialog closes after
+ * a successful installation. The promise is rejected when the user cancels the dialog.
+ *
+ * If the package is already installed before the dialog opens, we still go
+ * through all the motions and the dialog closes successfully without doing
+ * anything when the use hits "Install".
+ *
+ * You shouldn't call install_dialog unless you know that PackageKit is available.
+ * (If you do anyway, the resulting D-Bus errors will be shown to the user.)
+ */
+
+export function install_dialog(pkg, options) {
+ let data = null;
+ let error_message = null;
+ let progress_message = null;
+ let cancel = null;
+ let done = null;
+
+ if (!Array.isArray(pkg))
+ pkg = [pkg];
+
+ options = options || { };
+
+ const prom = new Promise((resolve, reject) => { done = f => { if (f) resolve(); else reject(); } });
+
+ let dialog = null;
+ function update() {
+ let extra_details = null;
+ let remove_details = null;
+ let footer_message = null;
+
+ const missing_name = <strong>{pkg.join(", ")}</strong>;
+
+ if (data && data.extra_names.length > 0)
+ extra_details = (
+ <div className="scale-up-ct">
+ {_("Additional packages:")}
+ <ul className="package-list-ct">{data.extra_names.map(id => <li key={id}>{id}</li>)}</ul>
+ </div>
+ );
+
+ if (data && data.remove_names.length > 0)
+ remove_details = (
+ <div className="scale-up-ct">
+ <WarningTriangleIcon /> {_("Removals:")}
+ <ul className="package-list">{data.remove_names.map(id => <li key={id}>{id}</li>)}</ul>
+ </div>
+ );
+
+ if (progress_message)
+ footer_message = (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ <span>{ progress_message }</span>
+ <Spinner size="sm" />
+ </Flex>
+ );
+ else if (data?.download_size) {
+ footer_message = (
+ <div>
+ { format_to_fragments(_("Total size: $0"), <strong>{cockpit.format_bytes(data.download_size)}</strong>) }
+ </div>
+ );
+ }
+
+ const body = {
+ id: "dialog",
+ title: options.title || _("Install software"),
+ body: (
+ <div className="scroll">
+ <p>{ format_to_fragments(options.text || _("$0 will be installed."), missing_name) }</p>
+ { remove_details }
+ { extra_details }
+ </div>
+ ),
+ static_error: error_message,
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Install"),
+ style: "primary",
+ clicked: install_missing,
+ disabled: data == null
+ }
+ ],
+ idle_message: footer_message,
+ dialog_done: f => { if (!f && cancel) cancel(); done(f) }
+ };
+
+ if (dialog) {
+ dialog.setProps(body);
+ dialog.setFooterProps(footer);
+ } else {
+ dialog = show_modal_dialog(body, footer);
+ }
+ }
+
+ function check_missing() {
+ PK.check_missing_packages(pkg,
+ p => {
+ cancel = p.cancel;
+ let pm = null;
+ if (p.waiting)
+ pm = _("Waiting for other software management operations to finish");
+ else
+ pm = _("Checking installed software");
+ if (pm != progress_message) {
+ progress_message = pm;
+ update();
+ }
+ })
+ .then(d => {
+ if (d.unavailable_names.length > 0)
+ error_message = cockpit.format(_("$0 is not available from any repository."),
+ d.unavailable_names[0]);
+ else
+ data = d;
+ progress_message = null;
+ cancel = null;
+ update();
+ })
+ .catch(e => {
+ progress_message = null;
+ cancel = null;
+ error_message = e.toString();
+ update();
+ });
+ }
+
+ function install_missing() {
+ // We need to return a Cockpit flavoured promise since we want
+ // to use progress notifications.
+ const dfd = cockpit.defer();
+
+ PK.install_missing_packages(data,
+ p => {
+ let text = null;
+ if (p.waiting) {
+ text = _("Waiting for other software management operations to finish");
+ } else if (p.package) {
+ let fmt;
+ if (p.info == PK.Enum.INFO_DOWNLOADING)
+ fmt = _("Downloading $0");
+ else if (p.info == PK.Enum.INFO_REMOVING)
+ fmt = _("Removing $0");
+ else
+ fmt = _("Installing $0");
+ text = format_to_fragments(fmt, <strong>{p.package}</strong>);
+ }
+ dfd.notify(text, p.cancel);
+ })
+ .then(() => {
+ dfd.resolve();
+ })
+ .catch(error => {
+ dfd.reject(error);
+ });
+
+ return dfd.promise;
+ }
+
+ update();
+ check_missing();
+ return prom;
+}
diff --git a/pkg/lib/cockpit-components-listing-panel.jsx b/pkg/lib/cockpit-components-listing-panel.jsx
new file mode 100644
index 0000000..3f2b784
--- /dev/null
+++ b/pkg/lib/cockpit-components-listing-panel.jsx
@@ -0,0 +1,87 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Tab, TabTitleText, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
+import './cockpit-components-listing-panel.scss';
+
+/* tabRenderers optional: list of tab renderers for inline expansion, array of objects with
+ * - name tab name (has to be unique in the entry, used as react key)
+ * - renderer react component
+ * - data render data passed to the tab renderer
+ */
+export class ListingPanel extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ activeTab: props.initiallyActiveTab ? props.initiallyActiveTab : 0, // currently active tab in expanded mode, defaults to first tab
+ };
+ this.handleTabClick = this.handleTabClick.bind(this);
+ }
+
+ handleTabClick(event, tabIndex) {
+ event.preventDefault();
+ if (this.state.activeTab !== tabIndex) {
+ this.setState({ activeTab: tabIndex });
+ }
+ }
+
+ render() {
+ let listingDetail;
+ if ('listingDetail' in this.props) {
+ listingDetail = (
+ <span className="ct-listing-panel-caption">
+ {this.props.listingDetail}
+ </span>
+ );
+ }
+
+ return (
+ <div className="ct-listing-panel">
+ {listingDetail && <div className="ct-listing-panel-actions pf-v5-c-tabs">
+ {listingDetail}
+ </div>}
+ {this.props.tabRenderers.length && <Tabs activeKey={this.state.activeTab} className="ct-listing-panel-tabs" mountOnEnter onSelect={this.handleTabClick}>
+ {this.props.tabRenderers.map((itm, tabIdx) => {
+ const Renderer = itm.renderer;
+ const rendererData = itm.data;
+
+ return (
+ <Tab key={tabIdx} eventKey={tabIdx} title={<TabTitleText>{itm.name}</TabTitleText>}>
+ <div className="ct-listing-panel-body" key={tabIdx} data-key={tabIdx}>
+ <Renderer {...rendererData} />
+ </div>
+ </Tab>
+ );
+ })}
+ </Tabs>}
+ </div>
+ );
+ }
+}
+ListingPanel.defaultProps = {
+ tabRenderers: [],
+};
+
+ListingPanel.propTypes = {
+ tabRenderers: PropTypes.array,
+ listingDetail: PropTypes.node,
+ initiallyActiveTab: PropTypes.number,
+};
diff --git a/pkg/lib/cockpit-components-listing-panel.scss b/pkg/lib/cockpit-components-listing-panel.scss
new file mode 100644
index 0000000..c258ae6
--- /dev/null
+++ b/pkg/lib/cockpit-components-listing-panel.scss
@@ -0,0 +1,93 @@
+.ct-listing-panel {
+ display: flex;
+ flex-wrap: wrap;
+
+ &-actions {
+ order: 2;
+ flex-grow: 1;
+ padding-block: var(--pf-v5-global--spacer--sm);
+ padding-inline: var(--pf-v5-global--spacer--md) var(--pf-v5-global--spacer--lg);
+ }
+
+ &-caption {
+ margin-inline-start: auto;
+ }
+
+ &-tabs {
+ flex-grow: 1;
+ order: 1;
+ }
+
+ .pf-v5-c-tab-content {
+ order: 3;
+ flex-basis: 100%;
+ }
+
+ &-body {
+ // Don't let PF4 automatically add a border in tables inside the body
+ --pf-v5-c-table__expandable-row--after--BorderLeftWidth: 0;
+ --pf-v5-c-table--border-width--base: 0;
+
+ // Add some sizing to the body
+ padding-block: var(--pf-v5-global--spacer--md);
+ padding-inline: var(--pf-v5-global--spacer--lg);
+ inline-size: 100%;
+
+ // Containing hack part 1
+ float: inline-start;
+
+ &::after {
+ // Containing hack part 2: Clearfix CSS hack,
+ // to allow children content to float fine without setting overflow
+ content: "";
+ clear: both;
+ display: table;
+ }
+ }
+}
+
+.ct-table {
+ > tbody > .pf-v5-c-table__expandable-row {
+ // Don't scroll table's expanded contents vertically.
+ // Instead, rely on page scrolling.
+ // Important for mobile; also useful for desktop.
+ overflow-block: visible !important;
+ max-block-size: unset !important;
+ }
+}
+
+// PF4 upstream issue to adopt expand animation:
+// https://github.com/patternfly/patternfly-design/issues/899
+
+@media not all and (prefers-reduced-motion: reduce) {
+ // Add expansion animations when prefers-reduced isn't enabled
+ .ct-table .pf-v5-c-table__expandable-row-content {
+ // Animation ends at or before 2/3 in most cases; so we extend by 1.5 to compensate
+ animation: ctListingPanelShow calc(var(--pf-v5-global--TransitionDuration) * 1.5) var(--pf-v5-global--TimingFunction);
+ }
+}
+
+@keyframes ctListingPanelShow {
+ 0% {
+ // The animation needs to flow downward to feel natural
+ transform-origin: top;
+ // Overflow will revert when done (but should be hidden during animation)
+ overflow: hidden;
+ max-block-size: 0;
+ // Padding should 'tween between 0 and the actual padding (unstated)
+ padding-block: 0;
+ }
+
+ 67% {
+ // Max height is tricky in animations, as auto doesn't work
+ // 100vh makes sense, but would cause different speeds on different devices
+ // Screens are almost all =< 12000px; data is almost always smaller
+ // we'll relax it to to 100vh at 100%, just in case.
+ max-block-size: 1200px;
+ }
+
+ 100% {
+ // Allow content to extend to the height of the screen (just in case)
+ max-block-size: 100vh;
+ }
+}
diff --git a/pkg/lib/cockpit-components-logs-panel.jsx b/pkg/lib/cockpit-components-logs-panel.jsx
new file mode 100644
index 0000000..9887252
--- /dev/null
+++ b/pkg/lib/cockpit-components-logs-panel.jsx
@@ -0,0 +1,185 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from "react";
+
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from '@patternfly/react-core/dist/esm/components/Card/index.js';
+import { ExclamationTriangleIcon, TimesCircleIcon } from '@patternfly/react-icons';
+
+import { journal } from "journal";
+import "journal.css";
+import "cockpit-components-logs-panel.scss";
+
+const _ = cockpit.gettext;
+
+/* JournalOutput implements the interface expected by
+ journal.renderer, and also collects the output.
+ */
+
+export class JournalOutput {
+ constructor(search_options) {
+ this.logs = [];
+ this.reboot_key = 0;
+ this.search_options = search_options || {};
+ }
+
+ onEvent(ev, cursor, full_content) {
+ // only consider primary mouse button for clicks
+ if (ev.type === 'click') {
+ if (ev.button !== 0)
+ return;
+
+ // Ignore if text is being selected - less than 3 characters most likely means misclick
+ const selection = window.getSelection().toString();
+ if (selection && selection.length > 2 && full_content.indexOf(selection) >= 0)
+ return;
+ }
+
+ // only consider enter button for keyboard events
+ if (ev.type === 'KeyDown' && ev.key !== "Enter")
+ return;
+
+ cockpit.jump("system/logs#/" + cursor + "?parent_options=" + JSON.stringify(this.search_options));
+ }
+
+ render_line(ident, prio, message, count, time, entry) {
+ let problem = false;
+ let warning = false;
+
+ if (ident === 'abrt-notification') {
+ problem = true;
+ ident = entry.PROBLEM_BINARY;
+ } else if (prio < 4) {
+ warning = true;
+ }
+
+ const full_content = [time, message, ident].join("\n");
+
+ return (
+ <div className="cockpit-logline" role="row" tabIndex="0" key={entry.__CURSOR}
+ data-cursor={entry.__CURSOR}
+ onClick={ev => this.onEvent(ev, entry.__CURSOR, full_content)}
+ onKeyDown={ev => this.onEvent(ev, entry.__CURSOR, full_content)}>
+ <div className="cockpit-log-warning" role="cell">
+ { warning
+ ? <ExclamationTriangleIcon className="ct-icon-exclamation-triangle" />
+ : null
+ }
+ { problem
+ ? <TimesCircleIcon className="ct-icon-times-circle" />
+ : null
+ }
+ </div>
+ <div className="cockpit-log-time" role="cell">{time}</div>
+ <span className="cockpit-log-message" role="cell">{message}</span>
+ {
+ count > 1
+ ? <div className="cockpit-log-service-container" role="cell">
+ <div className="cockpit-log-service-reduced">{ident}</div>
+ <Badge screenReaderText={_("Occurrences")} isRead key={count}>{count}</Badge>
+ </div>
+ : <div className="cockpit-log-service" role="cell">{ident}</div>
+ }
+ </div>
+ );
+ }
+
+ render_day_header(day) {
+ return <div className="panel-heading" key={day}>{day}</div>;
+ }
+
+ render_reboot_separator() {
+ return (
+ <div className="cockpit-logline" role="row" key={"reboot-" + this.reboot_key++}>
+ <div className="cockpit-log-warning" role="cell" />
+ <span className="cockpit-log-message cockpit-logmsg-reboot" role="cell">{_("Reboot")}</span>
+ </div>
+ );
+ }
+
+ prepend(item) {
+ this.logs.unshift(item);
+ }
+
+ append(item) {
+ this.logs.push(item);
+ }
+
+ remove_first() {
+ this.logs.shift();
+ }
+
+ remove_last() {
+ this.logs.pop();
+ }
+
+ limit(max) {
+ if (this.logs.length > max)
+ this.logs = this.logs.slice(0, max);
+ }
+}
+
+export class LogsPanel extends React.Component {
+ constructor() {
+ super();
+ this.state = { logs: [] };
+ }
+
+ componentDidMount() {
+ this.journalctl = journal.journalctl(this.props.match, { count: this.props.max });
+
+ const out = new JournalOutput(this.props.search_options);
+ const render = journal.renderer(out);
+
+ this.journalctl.stream((entries) => {
+ for (let i = 0; i < entries.length; i++)
+ render.prepend(entries[i]);
+ render.prepend_flush();
+ // "max + 1" since there is always a date header and we
+ // want to show "max" entries below it.
+ out.limit(this.props.max + 1);
+ this.setState({ logs: out.logs });
+ });
+ }
+
+ componentWillUnmount() {
+ this.journalctl.stop();
+ }
+
+ render() {
+ const actions = (this.state.logs.length > 0 && this.props.goto_url) && <Button variant="secondary" onClick={e => cockpit.jump(this.props.goto_url)}>{_("View all logs")}</Button>;
+
+ return (
+ <Card className="cockpit-log-panel">
+ <CardHeader actions={{ actions }}>
+ <CardTitle>{this.props.title}</CardTitle>
+ </CardHeader>
+ <CardBody className={(!this.state.logs.length && this.props.emptyMessage.length) ? "empty-message" : "contains-list"}>
+ { this.state.logs.length ? this.state.logs : this.props.emptyMessage }
+ </CardBody>
+ </Card>
+ );
+ }
+}
+LogsPanel.defaultProps = {
+ emptyMessage: [],
+};
diff --git a/pkg/lib/cockpit-components-logs-panel.scss b/pkg/lib/cockpit-components-logs-panel.scss
new file mode 100644
index 0000000..31cb0fc
--- /dev/null
+++ b/pkg/lib/cockpit-components-logs-panel.scss
@@ -0,0 +1,15 @@
+.panel-body.empty-message {
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ text-align: center;
+}
+
+.cockpit-log-panel .panel-body {
+ padding-inline: 0;
+}
+
+.cockpit-log-panel .pf-v5-c-card__header-main > .pf-v5-c-card__title > .pf-v5-c-card__title-text {
+ padding: 0;
+ font-weight: normal;
+ font-size: var(--pf-v5-global--FontSize--2xl);
+}
diff --git a/pkg/lib/cockpit-components-modifications.css b/pkg/lib/cockpit-components-modifications.css
new file mode 100644
index 0000000..dfdaadb
--- /dev/null
+++ b/pkg/lib/cockpit-components-modifications.css
@@ -0,0 +1,28 @@
+.automation-script-modal pre {
+ max-block-size: 20em;
+ margin-block-end: 5px;
+}
+
+.automation-script-modal span.fa {
+ margin-inline-end: 5px;
+}
+
+.automation-script-modal i.fa {
+ margin-inline: 5px 2px;
+}
+
+.automation-script-modal textarea {
+ min-block-size: 15rem;
+}
+
+.automation-script-modal .ansible-docs-link > svg {
+ padding-inline-end: var(--pf-v5-global--spacer--xs);
+}
+
+.green-icon {
+ color: var(--pf-v5-global--success-color--100);
+}
+
+.pf-v5-c-card.modifications-table .pf-v5-c-card__header {
+ justify-content: space-between;
+}
diff --git a/pkg/lib/cockpit-components-modifications.jsx b/pkg/lib/cockpit-components-modifications.jsx
new file mode 100644
index 0000000..85b67c3
--- /dev/null
+++ b/pkg/lib/cockpit-components-modifications.jsx
@@ -0,0 +1,182 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import PropTypes from 'prop-types';
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card/index.js";
+import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Tab, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { CheckIcon, CopyIcon, ExternalLinkAltIcon, OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+
+import cockpit from "cockpit";
+import 'cockpit-components-modifications.css';
+
+const _ = cockpit.gettext;
+
+/* Dialog for showing scripts to modify system
+ *
+ * Enables showing shell and ansible script. Shell one is mandatory and ansible one can be omitted.
+ *
+ */
+export const ModificationsExportDialog = ({ show, onClose, shell, ansible }) => {
+ const [active_tab, setActiveTab] = React.useState("ansible");
+ const [copied, setCopied] = React.useState(false);
+ const [timeoutId, setTimeoutId] = React.useState(null);
+
+ const handleSelect = (_event, active_tab) => {
+ setCopied(false);
+ setActiveTab(active_tab);
+ if (timeoutId !== null) {
+ clearTimeout(timeoutId);
+ setTimeoutId(null);
+ }
+ };
+
+ const copyToClipboard = () => {
+ try {
+ navigator.clipboard.writeText((active_tab === "ansible" ? ansible : shell).trim())
+ .then(() => {
+ setCopied(true);
+ setTimeoutId(setTimeout(() => {
+ setCopied(false);
+ setTimeoutId(null);
+ }, 3000));
+ })
+ .catch(e => console.error('Text could not be copied: ', e ? e.toString() : ""));
+ } catch (error) {
+ console.error('Text could not be copied: ', error.toString());
+ }
+ };
+
+ const footer = (
+ <>
+ <Button variant='secondary' className="btn-clipboard" onClick={copyToClipboard} icon={copied ? <CheckIcon className="green-icon" /> : <CopyIcon />}>
+ { _("Copy to clipboard") }
+ </Button>
+ <Button variant='secondary' className='btn-cancel' onClick={onClose}>
+ { _("Close") }
+ </Button>
+ </>
+ );
+
+ return (
+ <Modal isOpen={show} className="automation-script-modal"
+ position="top" variant="medium"
+ onClose={onClose}
+ footer={footer}
+ title={_("Automation script") }>
+ <Tabs activeKey={active_tab} onSelect={handleSelect}>
+ <Tab eventKey="ansible" title={_("Ansible")}>
+ <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={ansible.trim()} />
+ <div className="ansible-docs-link">
+ <OutlinedQuestionCircleIcon />
+ { _("Create new task file with this content.") }
+ <Button variant="link" component="a" href="https://docs.ansible.com/ansible/latest/playbook_guide/playbooks_reuse_roles.html"
+ target="_blank" rel="noopener noreferrer"
+ icon={<ExternalLinkAltIcon />}>
+ { _("Ansible roles documentation") }
+ </Button>
+ </div>
+ </Tab>
+ <Tab eventKey="shell" title={_("Shell script")}>
+ <TextArea resizeOrientation='vertical' readOnlyVariant="default" defaultValue={shell.trim()} />
+ </Tab>
+ </Tabs>
+ </Modal>
+ );
+};
+
+ModificationsExportDialog.propTypes = {
+ shell: PropTypes.string.isRequired,
+ ansible: PropTypes.string.isRequired,
+ show: PropTypes.bool.isRequired,
+ onClose: PropTypes.func.isRequired,
+};
+
+/* Display list of modifications in human readable format
+ *
+ * Also show `View automation script` button which opens dialog in which different
+ * scripts are available. With these scripts it is possible to apply the same
+ * configurations to other machines.
+ *
+ * Pass array `entries` to show human readable messages.
+ * Pass string `shell` and `ansible` with scripts.
+ *
+ */
+export const Modifications = ({ entries, failed, permitted, title, shell, ansible }) => {
+ const [showDialog, setShowDialog] = React.useState(false);
+
+ let emptyRow = null;
+ let fail_message = permitted ? _("No system modifications") : _("The logged in user is not permitted to view system modifications");
+ fail_message = failed || fail_message;
+ if (entries === null) {
+ emptyRow = <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key="loading">{_("Loading system modifications...")}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>;
+ }
+ if (entries?.length === 0) {
+ emptyRow = <DataListItem>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key={fail_message}>{fail_message}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>;
+ }
+
+ return (
+ <>
+ <ModificationsExportDialog show={showDialog} shell={shell} ansible={ansible} onClose={() => setShowDialog(false)} />
+ <Card className="modifications-table">
+ <CardHeader>
+ <CardTitle component="h2">{title}</CardTitle>
+ { !emptyRow &&
+ <Button variant="secondary" onClick={() => setShowDialog(true)}>
+ {_("View automation script")}
+ </Button>
+ }
+ </CardHeader>
+ <CardBody className="contains-list">
+ <DataList aria-label={title} isCompact>
+ { emptyRow ||
+ entries.map(entry => <DataListItem key={entry}>
+ <DataListItemRow>
+ <DataListItemCells dataListCells={[<DataListCell key={entry}>{entry}</DataListCell>]} />
+ </DataListItemRow>
+ </DataListItem>
+ )
+ }
+ </DataList>
+ </CardBody>
+ </Card>
+ </>
+ );
+};
+
+Modifications.propTypes = {
+ failed: PropTypes.string,
+ title: PropTypes.string.isRequired,
+ permitted: PropTypes.bool.isRequired,
+ entries: PropTypes.arrayOf(PropTypes.string),
+ shell: PropTypes.string.isRequired,
+ ansible: PropTypes.string.isRequired,
+};
diff --git a/pkg/lib/cockpit-components-password.jsx b/pkg/lib/cockpit-components-password.jsx
new file mode 100644
index 0000000..d0c8cc7
--- /dev/null
+++ b/pkg/lib/cockpit-components-password.jsx
@@ -0,0 +1,188 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from 'cockpit';
+import React, { useState } from 'react';
+import { debounce } from 'throttle-debounce';
+import { Button } from '@patternfly/react-core/dist/esm/components/Button/index.js';
+import { FormGroup, FormHelperText } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { InputGroup, InputGroupItem } from '@patternfly/react-core/dist/esm/components/InputGroup/index.js';
+import { HelperText, HelperTextItem } from "@patternfly/react-core/dist/esm/components/HelperText/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Progress, ProgressMeasureLocation, ProgressSize } from "@patternfly/react-core/dist/esm/components/Progress/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { EyeIcon, EyeSlashIcon, HelpIcon } from '@patternfly/react-icons';
+
+import { FormHelper } from "cockpit-components-form-helper";
+
+import './cockpit-components-password.scss';
+import { Flex, FlexItem } from '@patternfly/react-core';
+
+const _ = cockpit.gettext;
+
+export function password_quality(password, force) {
+ return new Promise((resolve, reject) => {
+ cockpit.spawn('/usr/bin/pwscore', { err: "message" })
+ .input(password)
+ .done(function(content) {
+ const quality = parseInt(content, 10);
+ if (quality === 0)
+ reject(new Error(_("Password is too weak")));
+ else
+ resolve({ value: quality, message: quality === 100 ? _("Excellent password") : undefined });
+ })
+ .fail(function(ex) {
+ if (!force)
+ reject(new Error(ex.message || _("Password is not acceptable")));
+ else
+ resolve({ value: 0 });
+ });
+ });
+}
+
+const debounced_password_quality = debounce(300, (value, callback) => {
+ password_quality(value).catch(() => ({ value: 0 })).then(callback);
+});
+
+export const PasswordFormFields = ({
+ password_label, password_confirm_label,
+ password_label_info,
+ initial_password,
+ error_password, error_password_confirm,
+ idPrefix, change
+}) => {
+ const [password, setPassword] = useState(initial_password || "");
+ const [passwordConfirm, setConfirmPassword] = useState("");
+ const [passwordStrength, setPasswordStrength] = useState();
+ const [passwordMessage, setPasswordMessage] = useState("");
+ const [passwordHidden, setPasswordHidden] = useState(true);
+ const [passwordConfirmHidden, setPasswordConfirmHidden] = useState(true);
+
+ function onPasswordChanged(value) {
+ setPassword(value);
+ change("password", value);
+
+ if (value) {
+ debounced_password_quality(value, strength => {
+ setPasswordStrength(strength.value);
+ setPasswordMessage(strength.message);
+ });
+ } else {
+ setPasswordStrength();
+ setPasswordMessage("");
+ }
+ }
+
+ let variant;
+ let message;
+ let messageColor;
+ if (passwordStrength > 66) {
+ variant = "success";
+ messageColor = "pf-v5-u-success-color-200";
+ message = _("Strong password");
+ } else if (passwordStrength > 33) {
+ variant = "warning";
+ messageColor = "pf-v5-u-warning-color-200";
+ message = _("Acceptable password");
+ } else {
+ variant = "danger";
+ messageColor = "pf-v5-u-danger-color-200";
+ message = _("Weak password");
+ }
+
+ if (!passwordMessage && message)
+ setPasswordMessage(message);
+
+ let passwordStrengthValue = Number.isInteger(passwordStrength) ? Number.parseInt(passwordStrength) : -1;
+ if (password !== "" && (passwordStrengthValue >= 0 && passwordStrengthValue < 25))
+ passwordStrengthValue = 25;
+
+ return (
+ <>
+ <FormGroup label={password_label}
+ labelIcon={password_label_info &&
+ <Popover bodyContent={password_label_info}>
+ <button onClick={e => e.preventDefault()}
+ className="pf-v5-c-form__group-label-help">
+ <HelpIcon />
+ </button>
+ </Popover>
+ }
+ validated={error_password ? "warning" : "default"}
+ id={idPrefix + "-pw1-group"}
+ fieldId={idPrefix + "-pw1"}>
+ <InputGroup>
+ <InputGroupItem isFill>
+ <TextInput className="check-passwords" type={passwordHidden ? "password" : "text"} id={idPrefix + "-pw1"}
+ autoComplete="new-password" value={password} onChange={(_event, value) => onPasswordChanged(value)}
+ validated={error_password ? "warning" : "default"} />
+ </InputGroupItem>
+ <InputGroupItem>
+ <Button
+ variant="control"
+ onClick={() => setPasswordHidden(!passwordHidden)}
+ aria-label={passwordHidden ? _("Show password") : _("Hide password")}>
+ {passwordHidden ? <EyeIcon /> : <EyeSlashIcon />}
+ </Button>
+ </InputGroupItem>
+ </InputGroup>
+ {passwordStrengthValue >= 0 && <Flex spaceItems={{ default: 'spaceItemsSm' }}>
+ <FlexItem>
+ <Progress id={idPrefix + "-meter"}
+ className={"pf-v5-u-pt-xs ct-password-strength-meter " + variant}
+ title={_("password quality")}
+ size={ProgressSize.sm}
+ measureLocation={ProgressMeasureLocation.none}
+ variant={variant}
+ value={passwordStrengthValue} />
+ </FlexItem>
+ <FlexItem>
+ <div id={idPrefix + "-password-meter-message"} className={"pf-v5-c-form__helper-text " + messageColor} aria-live="polite">{passwordMessage}</div>
+ </FlexItem>
+ </Flex>}
+ {error_password && <FormHelperText>
+ <HelperText component="ul" aria-live="polite" id="password-error-message">
+ <HelperTextItem isDynamic variant="warning" component="li">
+ {error_password}
+ </HelperTextItem>
+ </HelperText>
+ </FormHelperText>}
+ </FormGroup>
+
+ {password_confirm_label && <FormGroup label={password_confirm_label}
+ id={idPrefix + "-pw2-group"}
+ fieldId={idPrefix + "-pw2"}>
+ <InputGroup>
+ <InputGroupItem isFill>
+ <TextInput type={passwordConfirmHidden ? "password" : "text"} id={idPrefix + "-pw2"} autoComplete="new-password"
+ value={passwordConfirm} onChange={(_event, value) => { setConfirmPassword(value); change("password_confirm", value) }} />
+ </InputGroupItem>
+ <InputGroupItem>
+ <Button
+ variant="control"
+ onClick={() => setPasswordConfirmHidden(!passwordConfirmHidden)}
+ aria-label={passwordConfirmHidden ? _("Show confirmation password") : _("Hide confirmation password")}>
+ {passwordConfirmHidden ? <EyeIcon /> : <EyeSlashIcon />}
+ </Button>
+ </InputGroupItem>
+ </InputGroup>
+ <FormHelper fieldId={idPrefix + "-pw2"} helperTextInvalid={error_password_confirm} />
+ </FormGroup>}
+ </>
+ );
+};
diff --git a/pkg/lib/cockpit-components-password.scss b/pkg/lib/cockpit-components-password.scss
new file mode 100644
index 0000000..e1d2d44
--- /dev/null
+++ b/pkg/lib/cockpit-components-password.scss
@@ -0,0 +1,11 @@
+@import "global-variables";
+@import "@patternfly/patternfly/utilities/Text/text.scss";
+
+.ct-password-strength-meter {
+ grid-gap: var(--pf-v5-global--spacer--xs);
+ inline-size: var(--pf-v5-global--spacer--2xl);
+
+ .pf-v5-c-progress__description, .pf-v5-c-progress__status {
+ display: none;
+ }
+}
diff --git a/pkg/lib/cockpit-components-plot.jsx b/pkg/lib/cockpit-components-plot.jsx
new file mode 100644
index 0000000..527f513
--- /dev/null
+++ b/pkg/lib/cockpit-components-plot.jsx
@@ -0,0 +1,513 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import React, { useState, useRef, useLayoutEffect } from 'react';
+import { useEvent } from "hooks.js";
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Dropdown, DropdownItem, DropdownSeparator, DropdownToggle } from '@patternfly/react-core/dist/esm/deprecated/components/Dropdown/index.js';
+
+import { AngleLeftIcon, AngleRightIcon, SearchMinusIcon } from '@patternfly/react-icons';
+
+import * as timeformat from "timeformat";
+import '@patternfly/patternfly/patternfly-charts.scss';
+import "cockpit-components-plot.scss";
+
+const _ = cockpit.gettext;
+
+function time_ticks(data) {
+ const first_plot = data[0].data;
+ const start_ms = first_plot[0][0];
+ const end_ms = first_plot[first_plot.length - 1][0];
+
+ // Determine size between ticks
+
+ const sizes_in_seconds = [
+ 60, // minute
+ 5 * 60, // 5 minutes
+ 10 * 60, // 10 minutes
+ 30 * 60, // half hour
+ 60 * 60, // hour
+ 6 * 60 * 60, // quarter day
+ 12 * 60 * 60, // half day
+ 24 * 60 * 60, // day
+ 7 * 24 * 60 * 60, // week
+ 30 * 24 * 60 * 60, // month
+ 183 * 24 * 60 * 60, // half a year
+ 365 * 24 * 60 * 60, // year
+ 10 * 365 * 24 * 60 * 60 // 10 years
+ ];
+
+ let size;
+ for (let i = 0; i < sizes_in_seconds.length; i++) {
+ if (((end_ms - start_ms) / 1000) / sizes_in_seconds[i] < 10 || i == sizes_in_seconds.length - 1) {
+ size = sizes_in_seconds[i] * 1000;
+ break;
+ }
+ }
+
+ // Determine what to omit from the tick label. If it's all in the
+ // current year, we don't need to include the year; and if it's
+ // all happening today, we don't include the month and date.
+
+ const now_date = new Date();
+ const start_date = new Date(start_ms);
+
+ let include_year = true;
+ let include_month_and_day = true;
+
+ if (start_date.getFullYear() == now_date.getFullYear()) {
+ include_year = false;
+ if (start_date.getMonth() == now_date.getMonth() && start_date.getDate() == now_date.getDate())
+ include_month_and_day = false;
+ }
+
+ // Compute the actual ticks
+
+ const ticks = [];
+ let t = Math.ceil(start_ms / size) * size;
+ while (t < end_ms) {
+ ticks.push(t);
+ t += size;
+ }
+
+ // Render the label
+
+ function pad(n) {
+ let str = n.toFixed();
+ if (str.length == 1)
+ str = '0' + str;
+ return str;
+ }
+
+ function format_tick(val, index, ticks) {
+ const d = new Date(val);
+ let label = ' ';
+
+ if (include_month_and_day) {
+ if (include_year)
+ label += timeformat.date(d) + '\n';
+ else
+ label += timeformat.formatter({ month: "long" }).format(d) + ' ' + d.getDate().toFixed() + '\n';
+ }
+ label += pad(d.getHours()) + ':' + pad(d.getMinutes());
+
+ return label;
+ }
+
+ return {
+ ticks,
+ formatter: format_tick,
+ start: start_ms,
+ end: end_ms
+ };
+}
+
+function value_ticks(data, config) {
+ let max = config.min_max;
+ const last_plot = data[data.length - 1].data;
+ for (let i = 0; i < last_plot.length; i++) {
+ const s = last_plot[i][1] || last_plot[i][2];
+ if (s > max)
+ max = s;
+ }
+
+ // Find the highest power of the base unit that is still below
+ // MAX.
+ //
+ // For example, if the base unit is 1000 and MAX is 402,345,765
+ // this will set UNIT to 1,000,000, aka "Mega".
+ //
+ let unit = 1;
+ while (config.base_unit && max > unit * config.base_unit)
+ unit *= config.base_unit;
+
+ // Find the highest power of 10 that is below the maximum number
+ // on a tick label. If we use that as the distance between ticks,
+ // we get at most 10 ticks.
+ //
+ // To continue the example, MAX is 402,345,765 and UNIT is thus
+ // 1,000,000. The highest number on a tick label would be MAX /
+ // UNIT = 402ish. The highest power of 10 below that is 100. Thus
+ // the size between ticks is 100*UNIT = 100,000,000. Ticks would
+ // thus be "100 Mega" apart.
+ //
+ // If the highest number of would be only, say, 81, then we would get
+ // a highest power of 10, and ticks would be 10 units apart.
+ //
+ let size = Math.pow(10, Math.floor(Math.log10(max / unit))) * unit;
+
+ // Get the number of ticks to be around 4, but don't produce
+ // fractional numbers. This is done by doubling or halving the
+ // size between ticks until we get MAX / SIZE to be less than 8 or
+ // greater than 2.
+ //
+ // In the example, MAX / SIZE is already in range, so nothing
+ // changes here.
+ //
+ // If MAX / UNIT is close to the next power of ten, such as 999, we
+ // would end up with a doubled SIZE of 200,000,000.
+ //
+ // If on the other hand MAX / UNIT would be closer to the next
+ // lower power of 10, like say 110, then we would half the SIZE to
+ // get moreticks. With 110, it will happen twice and SIZE ends up
+ // being 25,000,000.
+ //
+ // However, if we only have single digit tick labels, we don't
+ // want to halve them any further, since we don't want tick labels
+ // like "0.75".
+ //
+ while (max / size > 7)
+ size *= 2;
+ while (max / size < 3 && size / unit >= 10)
+ size /= 2;
+
+ // Make a list of tick values, each SIZE apart until we are just
+ // above MAX.
+ //
+ // In the example, we get
+ //
+ // [ 0, 100000000, 200000000, 300000000, 400000000, 500000000 ]
+ //
+ const ticks = [];
+ for (let t = 0; t < max + size; t += size)
+ ticks.push(t);
+
+ if (config.pull_out_unit) {
+ const unit_str = config.formatter(unit, config.base_unit, true)[1];
+
+ return {
+ ticks,
+ formatter: (val) => config.formatter(val, unit_str, true)[0],
+ unit: unit_str,
+ max: ticks[ticks.length - 1]
+ };
+ } else {
+ return {
+ ticks,
+ formatter: config.formatter,
+ max: ticks[ticks.length - 1]
+ };
+ }
+}
+
+export const ZoomControls = ({ plot_state }) => {
+ function format_range(seconds) {
+ let n;
+ if (seconds >= 365 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (365 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 year", "$0 years", n), n);
+ } else if (seconds >= 30 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (30 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 month", "$0 months", n), n);
+ } else if (seconds >= 7 * 24 * 60 * 60) {
+ n = Math.ceil(seconds / (7 * 24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 week", "$0 weeks", n), n);
+ } else if (seconds >= 24 * 60 * 60) {
+ n = Math.ceil(seconds / (24 * 60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 day", "$0 days", n), n);
+ } else if (seconds >= 60 * 60) {
+ n = Math.ceil(seconds / (60 * 60));
+ return cockpit.format(cockpit.ngettext("$0 hour", "$0 hours", n), n);
+ } else {
+ n = Math.ceil(seconds / 60);
+ return cockpit.format(cockpit.ngettext("$0 minute", "$0 minutes", n), n);
+ }
+ }
+
+ const zoom_state = plot_state.zoom_state;
+
+ const [isOpen, setIsOpen] = useState(false);
+ useEvent(plot_state, "changed");
+ useEvent(zoom_state, "changed");
+
+ function range_item(seconds, title) {
+ return (
+ <DropdownItem key={title}
+ onClick={() => {
+ setIsOpen(false);
+ zoom_state.set_range(seconds);
+ }}>
+ {title}
+ </DropdownItem>
+ );
+ }
+
+ if (!zoom_state)
+ return null;
+
+ return (
+ <div>
+ <Dropdown
+ isOpen={isOpen}
+ toggle={<DropdownToggle onToggle={(_, isOpen) => setIsOpen(isOpen)}>{format_range(zoom_state.x_range)}</DropdownToggle>}
+ dropdownItems={[
+ <DropdownItem key="now" onClick={() => { zoom_state.goto_now(); setIsOpen(false) }}>
+ {_("Go to now")}
+ </DropdownItem>,
+ <DropdownSeparator key="sep" />,
+ range_item(5 * 60, _("5 minutes")),
+ range_item(60 * 60, _("1 hour")),
+ range_item(6 * 60 * 60, _("6 hours")),
+ range_item(24 * 60 * 60, _("1 day")),
+ range_item(7 * 24 * 60 * 60, _("1 week"))
+ ]} />
+ { "\n" }
+ <Button variant="secondary" onClick={() => zoom_state.zoom_out()}
+ isDisabled={!zoom_state.enable_zoom_out}>
+ <SearchMinusIcon />
+ </Button>
+ { "\n" }
+ <Button variant="secondary" onClick={() => zoom_state.scroll_left()}
+ isDisabled={!zoom_state.enable_scroll_left}>
+ <AngleLeftIcon />
+ </Button>
+ <Button variant="secondary" onClick={() => zoom_state.scroll_right()}
+ isDisabled={!zoom_state.enable_scroll_right}>
+ <AngleRightIcon />
+ </Button>
+ </div>
+ );
+};
+
+const useLayoutSize = (init_width, init_height) => {
+ const ref = useRef(null);
+ const [size, setSize] = useState({ width: init_width, height: init_height });
+ /* eslint-disable react-hooks/exhaustive-deps */
+ useLayoutEffect(() => {
+ if (ref.current) {
+ const rect = ref.current.getBoundingClientRect();
+ if (rect.width != size.width || rect.height != size.height)
+ setSize({ width: rect.width, height: rect.height });
+ }
+ });
+ /* eslint-enable */
+ return [ref, size];
+};
+
+export const SvgPlot = ({ title, config, plot_state, plot_id, className }) => {
+ const [container_ref, container_size] = useLayoutSize(0, 0);
+ const [measure_ref, measure_size] = useLayoutSize(36, 20);
+
+ useEvent(plot_state, "plot:" + plot_id);
+ useEvent(plot_state, "changed");
+ useEvent(window, "resize");
+
+ const [selection, setSelection] = useState(null);
+
+ const chart_data = plot_state.data(plot_id);
+ if (!chart_data || chart_data.length == 0)
+ return null;
+
+ const t_ticks = time_ticks(chart_data);
+ const y_ticks = value_ticks(chart_data, config);
+
+ function make_chart() {
+ const w = container_size.width;
+ const h = container_size.height;
+
+ if (w == 0 || h == 0)
+ return null;
+
+ const x_off = t_ticks.start;
+ const x_range = (t_ticks.end - t_ticks.start);
+ const y_range = y_ticks.max;
+
+ const tick_length = 5;
+ const tick_gap = 3;
+
+ // widest string plus gap plus tick
+ const m_left = Math.ceil(measure_size.width) + tick_gap + tick_length;
+
+ // half of the time label so that it pops in fully formed at the far right edge
+ const m_right = 30;
+
+ // half a line for the top-half of the top-most y-axis label
+ // plus one extra line if there is a unit or a title.
+ const m_top = (y_ticks.unit || title ? 1.5 : 0.5) * Math.ceil(measure_size.height);
+
+ // x-axis labels can be up to two lines
+ const m_bottom = tick_length + tick_gap + 2 * Math.ceil(measure_size.height);
+
+ function x_coord(x) {
+ return (x - x_off) / x_range * (w - m_left - m_right) + m_left;
+ }
+
+ function x_value(c) {
+ return (c - m_left) / (w - m_left - m_right) * x_range + x_off;
+ }
+
+ function y_coord(y) {
+ return h - Math.max(y, 0) / y_range * (h - m_top - m_bottom) - m_bottom;
+ }
+
+ function cmd(op, x, y) {
+ return op + x.toFixed() + "," + y.toFixed() + " ";
+ }
+
+ function path(data, hover_arg) {
+ let d = cmd("M", m_left, h - m_bottom);
+ for (let i = 0; i < data.length; i++) {
+ d += cmd("L", x_coord(data[i][0]), y_coord(data[i][1]));
+ }
+ d += cmd("L", w - m_right, h - m_bottom);
+ d += "z";
+
+ return (
+ <path key={hover_arg} d={d}
+ role="presentation">
+ <title>{hover_arg}</title>
+ </path>
+ );
+ }
+
+ const paths = [];
+ for (let i = chart_data.length - 1; i >= 0; i--)
+ paths.push(path(chart_data[i].data, chart_data[i].name || true));
+
+ function start_dragging(event) {
+ if (event.button !== 0)
+ return;
+
+ const bounds = container_ref.current.getBoundingClientRect();
+ const x = event.clientX - bounds.x;
+ if (x >= m_left && x < w - m_right)
+ setSelection({ start: x, stop: x, left: x, right: x });
+ }
+
+ function drag(event) {
+ const bounds = container_ref.current.getBoundingClientRect();
+ let x = event.clientX - bounds.x;
+ if (x < m_left) x = m_left;
+ if (x > w - m_right) x = w - m_right;
+ setSelection({
+ start: selection.start,
+ stop: x,
+ left: Math.min(selection.start, x),
+ right: Math.max(selection.start, x)
+ });
+ }
+
+ function stop_dragging() {
+ const left = x_value(selection.left) / 1000;
+ const right = x_value(selection.right) / 1000;
+ plot_state.zoom_state.zoom_in(right - left, right);
+ setSelection(null);
+ }
+
+ function cancel_dragging() {
+ setSelection(null);
+ }
+
+ // This is a thin transparent rectangle placed at the x-axis,
+ // on top of all the graphs. It prevents bogus hover events
+ // for parts of the graph that are zero or very very close to
+ // it.
+ const hover_guard =
+ <rect x={0} y={h - m_bottom - 1} width={w} height={5} fill="transparent" />;
+
+ return (
+ <svg width={w} height={h}
+ className="ct-plot"
+ aria-label={title}
+ // TODO: Figure out a way to handle a11y without entirely hiding the live-updating graphs
+ aria-hidden="true"
+ role="img"
+ onMouseDown={plot_state.zoom_state?.enable_zoom_in ? start_dragging : null}
+ onMouseUp={selection ? stop_dragging : null}
+ onMouseMove={selection ? drag : null}
+ onMouseLeave={cancel_dragging}>
+ <title>{title}</title>
+ <text x={0} y={-20} className="ct-plot-widest" ref={measure_ref} aria-hidden="true">{config.widest_string}</text>
+ <rect x={m_left} y={m_top} width={w - m_left - m_right} height={h - m_top - m_bottom}
+ className="ct-plot-border" />
+ { y_ticks.unit && <text x={m_left - tick_length - tick_gap} y={0.5 * m_top}
+ className="ct-plot-unit"
+ textAnchor="end">
+ {y_ticks.unit}
+ </text>
+ }
+ { title && <text x={m_left} y={0.5 * m_top} className="ct-plot-title">
+ {title}
+ </text>
+ }
+ <g className="ct-plot-lines" role="presentation">
+ { y_ticks.ticks.map((t, i) => <line key={i}
+ x1={m_left - tick_length} x2={w - m_right}
+ y1={y_coord(t)} y2={y_coord(t)} />) }
+ </g>
+ <g className="ct-plot-ticks" role="presentation">
+ { t_ticks.ticks.map((t, i) => <line key={i}
+ x1={x_coord(t)} x2={x_coord(t)}
+ y1={h - m_bottom} y2={h - m_bottom + tick_length} />) }
+ </g>
+ <g className="ct-plot-paths">
+ { paths }
+ </g>
+ { hover_guard }
+ <g className="ct-plot-axis ct-plot-axis-y" textAnchor="end">
+ { y_ticks.ticks.map((t, i) => <text key={i} x={m_left - tick_length - tick_gap} y={y_coord(t) + 5}>
+ {y_ticks.formatter(t)}
+ </text>) }
+ </g>
+ <g className="ct-plot-axis ct-plot-axis-x" textAnchor="middle">
+ { t_ticks.ticks.map((t, i) => <text key={i} y={h - m_bottom + tick_length + tick_gap}>
+ { t_ticks.formatter(t).split("\n")
+ .map((s, j) =>
+ <tspan key={i + "." + j} x={x_coord(t)} dy="1.2em">{s}</tspan>) }
+ </text>) }
+ </g>
+ { selection &&
+ <rect x={selection.left} y={m_top} width={selection.right - selection.left} height={h - m_top - m_bottom}
+ className="ct-plot-selection" /> }
+ </svg>
+ );
+ }
+
+ return (
+ <div className={className} ref={container_ref}>
+ {make_chart()}
+ </div>);
+};
+
+export const bytes_config = {
+ base_unit: 1024,
+ min_max: 10240,
+ pull_out_unit: true,
+ widest_string: "MiB",
+ formatter: cockpit.format_bytes
+};
+
+export const bytes_per_sec_config = {
+ base_unit: 1024,
+ min_max: 10240,
+ pull_out_unit: true,
+ widest_string: "MiB/s",
+ formatter: cockpit.format_bytes_per_sec
+};
+
+export const bits_per_sec_config = {
+ base_unit: 1000,
+ min_max: 10000,
+ pull_out_unit: true,
+ widest_string: "Mbps",
+ formatter: cockpit.format_bits_per_sec
+};
diff --git a/pkg/lib/cockpit-components-plot.scss b/pkg/lib/cockpit-components-plot.scss
new file mode 100644
index 0000000..d4faefa
--- /dev/null
+++ b/pkg/lib/cockpit-components-plot.scss
@@ -0,0 +1,119 @@
+// Selected set of PF chart colors to optimize for full-spectrum and colorblindness
+// using unique part of PF name and a hex color fallback
+$plotColors: (
+ blue-300 #06c,
+ green-100 #bde2b9,
+ cyan-200 #73c5c5,
+ purple-100 #b2b0ea,
+ gold-300 #f4c145,
+ orange-300 #ec7a08,
+ red-200 #a30000,
+ cyan-300 #009596,
+ black-500 #4d5258
+);
+
+.ct-plot {
+ font-family: var(--pf-v5-chart-global--FontFamily);
+
+ &-border {
+ stroke: var(--pf-v5-chart-global--Fill--Color--300);
+ fill: transparent;
+ shape-rendering: crispedges;
+ }
+
+ &-title {
+ font-size: calc(var(--pf-v5-chart-global--FontSize--md) * 1px);
+ }
+
+ // Placeholder string to stretch the column, set offscreen
+ &-widest {
+ fill: transparent;
+ }
+
+ &-axis,
+ &-unit {
+ font-size: calc(var(--pf-v5-chart-global--FontSize--xs) * 1px);
+ fill: var(--pf-v5-chart-global--Fill--Color--700);
+ letter-spacing: var(--pf-v5-chart-global--letter-spacing);
+ }
+
+ .pf-v5-theme-dark &-axis,
+ .pf-v5-theme-dark &-unit {
+ fill: var(--pf-v5-chart-global--Fill--Color--400);
+ }
+
+ &-lines,
+ &-ticks {
+ stroke: var(--pf-v5-chart-global--Fill--Color--300);
+ shape-rendering: crispedges;
+ }
+
+ &-selection {
+ fill: tan;
+ stroke: black;
+ opacity: 0.5;
+ shape-rendering: crispedges;
+ }
+
+ &-paths {
+ stroke-width: var(--pf-v5-chart-global--stroke--Width--sm);
+ shape-rendering: geometricprecision;
+
+ > path {
+ fill: var(--ct-plot-path-color);
+ stroke: var(--ct-plot-path-color);
+ }
+ }
+}
+
+.ct-plot-title {
+ fill: var(--pf-v5-global--Color--100);
+}
+
+$plotColorCurrent: 0;
+$plotColorTotal: 0;
+
+// Count up total number of plot colors
+@each $plotColor in $plotColors { $plotColorTotal: $plotColorTotal + 1; }
+
+// Iterate through colors and set each graph area to a color
+@each $plotColor, $plotColorBackup in $plotColors {
+ $plotColorCurrent: $plotColorCurrent + 1;
+ .ct-plot-paths > path:nth-last-child(#{$plotColorTotal}n + #{$plotColorCurrent}) {
+ --ct-plot-path-color: var(--pf-v5-chart-color-#{$plotColor}, #{$plotColorBackup});
+ }
+}
+
+// Make plot colors available to the entire page
+:root {
+ --ct-plot-color-total: #{$plotColorTotal};
+
+ @for $i from 1 through $plotColorTotal {
+ --ct-plot-color-#{i}: #{$plotColors[$i]};
+ }
+}
+
+[dir="rtl"] {
+ // Mirror the entire graph (placement & animation)
+ .ct-plot {
+ transform: scaleX(-1);
+
+ // Flip the text back (so it's not a mirror image)
+ text {
+ transform: scaleX(-1);
+ transform-origin: 50%;
+ transform-box: fill-box;
+ }
+
+ // Set the anchor point for y-axis and units
+ .ct-plot-axis-y text,
+ .ct-plot-unit {
+ transform-origin: 0%;
+ }
+
+ // Set the anchor point for the title
+ .ct-plot-title {
+ transform-origin: 100%;
+ }
+ }
+}
diff --git a/pkg/lib/cockpit-components-privileged.jsx b/pkg/lib/cockpit-components-privileged.jsx
new file mode 100644
index 0000000..89ce2ec
--- /dev/null
+++ b/pkg/lib/cockpit-components-privileged.jsx
@@ -0,0 +1,77 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React, { useState, useEffect } from 'react';
+import PropTypes from 'prop-types';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Tooltip, TooltipPosition } from "@patternfly/react-core/dist/esm/components/Tooltip/index.js";
+
+import cockpit from "cockpit";
+import { superuser } from 'superuser';
+import { useEvent } from "hooks";
+
+/**
+ * UI element wrapper for something that requires privilege. When access is not
+ * allowed, then wrap the element into a Tooltip.
+ *
+ * Note that the wrapped element itself needs to be disabled explicitly, this
+ * wrapper cannot do this (unfortunately wrapping it into a disabled span does
+ * not inherit).
+ */
+export function Privileged({ excuse, allowed, placement, tooltipId, children }) {
+ // wrap into extra <span> so that a disabled child keeps the tooltip working
+ let contents = <span id={allowed ? null : tooltipId}>{ children }</span>;
+ if (!allowed) {
+ contents = (
+ <Tooltip position={ placement || TooltipPosition.top} id={ tooltipId + "_tooltip" }
+ content={ excuse }>
+ { contents }
+ </Tooltip>);
+ }
+ return contents;
+}
+
+/**
+ * Convenience element for a Privilege wrapped Button
+ */
+export const PrivilegedButton = ({ tooltipId, placement, excuse, buttonId, onClick, ariaLabel, variant, isDanger, children }) => {
+ const [user, setUser] = useState(null);
+ useEvent(superuser, "changed");
+ useEffect(() => cockpit.user().then(user => setUser(user)));
+
+ return (
+ <Privileged allowed={ superuser.allowed } tooltipId={ tooltipId } placement={ placement }
+ excuse={ cockpit.format(excuse, user?.name ?? '') }>
+ <Button id={ buttonId } variant={ variant } isDanger={ isDanger } onClick={ onClick }
+ isInline isDisabled={ !superuser.allowed } aria-label={ ariaLabel }>
+ { children }
+ </Button>
+ </Privileged>
+ );
+};
+
+PrivilegedButton.propTypes = {
+ excuse: PropTypes.string.isRequired, // must contain a $0, replaced with user name
+ onClick: PropTypes.func,
+ variant: PropTypes.string,
+ placement: PropTypes.string, // default: top
+ buttonId: PropTypes.string,
+ tooltipId: PropTypes.string,
+ ariaLabel: PropTypes.string,
+};
diff --git a/pkg/lib/cockpit-components-shutdown.jsx b/pkg/lib/cockpit-components-shutdown.jsx
new file mode 100644
index 0000000..ee2ec7d
--- /dev/null
+++ b/pkg/lib/cockpit-components-shutdown.jsx
@@ -0,0 +1,248 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2021 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert/index.js";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider/index.js";
+import { TextArea } from "@patternfly/react-core/dist/esm/components/TextArea/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+
+import { ServerTime } from 'serverTime.js';
+import * as timeformat from "timeformat.js";
+import { DialogsContext } from "dialogs.jsx";
+import { FormHelper } from "cockpit-components-form-helper";
+
+import "cockpit-components-shutdown.scss";
+
+const _ = cockpit.gettext;
+
+export class ShutdownModal extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+ this.date_spawn = null;
+ this.state = {
+ error: "",
+ dateError: "",
+ message: "",
+ isOpen: false,
+ selected: "1",
+ dateObject: undefined,
+ startDate: undefined,
+ date: "",
+ time: "",
+ when: "+1",
+ formFilled: false,
+ };
+ this.onSubmit = this.onSubmit.bind(this);
+ this.updateDate = this.updateDate.bind(this);
+ this.updateTime = this.updateTime.bind(this);
+ this.calculate = this.calculate.bind(this);
+ this.dateRangeValidator = this.dateRangeValidator.bind(this);
+
+ this.server_time = new ServerTime();
+ }
+
+ componentDidMount() {
+ this.server_time.wait()
+ .then(() => {
+ const dateObject = this.server_time.utc_fake_now;
+ const date = timeformat.dateShort(dateObject);
+ const hour = this.server_time.utc_fake_now.getUTCHours();
+ const minute = this.server_time.utc_fake_now.getUTCMinutes();
+ this.setState({
+ dateObject,
+ date,
+ startDate: new Date(dateObject.toDateString()),
+ time: hour.toString().padStart(2, "0") + ":" + minute.toString().padStart(2, "0"),
+ });
+ })
+ .always(() => this.setState({ formFilled: true }));
+ }
+
+ updateDate(value, dateObject) {
+ this.setState({ date: value, dateObject }, this.calculate);
+ }
+
+ updateTime(value, hour, minute) {
+ this.setState({ time: value, hour, minute }, this.calculate);
+ }
+
+ calculate() {
+ if (this.date_spawn)
+ this.date_spawn.close("cancelled");
+
+ if (this.state.selected != "x") {
+ this.setState(prevState => ({
+ when: "+" + prevState.selected,
+ error: "",
+ dateError: "",
+ }));
+ return;
+ }
+
+ const time_error = this.state.hour === null || this.state.minute === null;
+ const date_error = !this.state.dateObject;
+
+ if (time_error && date_error) {
+ this.setState({ dateError: _("Invalid date format and invalid time format") });
+ return;
+ } else if (time_error) {
+ this.setState({ dateError: _("Invalid time format") });
+ return;
+ } else if (date_error) {
+ this.setState({ dateError: _("Invalid date format") });
+ return;
+ }
+
+ const cmd = ["date", "--date=" + (new Intl.DateTimeFormat('en-us').format(this.state.dateObject)) + " " + this.state.time, "+%s"];
+ this.date_spawn = cockpit.spawn(cmd, { err: "message" });
+ this.date_spawn.then(data => {
+ const input_timestamp = parseInt(data, 10);
+ const server_timestamp = parseInt(this.server_time.now.getTime() / 1000, 10);
+ let offset = Math.ceil((input_timestamp - server_timestamp) / 60);
+
+ /* If the time in minutes just changed, make it happen now */
+ if (offset === -1) {
+ offset = 0;
+ } else if (offset < 0) { // Otherwise it is a failure
+ this.setState({ dateError: _("Cannot schedule event in the past") });
+ return;
+ }
+
+ this.setState({
+ when: "+" + offset,
+ error: "",
+ dateError: "",
+ });
+ });
+ this.date_spawn.catch(e => {
+ if (e.problem == "cancelled")
+ return;
+ this.setState({ error: e.message });
+ });
+ this.date_spawn.finally(() => { this.date_spawn = null });
+ }
+
+ onSubmit(event) {
+ const Dialogs = this.context;
+ const arg = this.props.shutdown ? "--poweroff" : "--reboot";
+ if (!this.props.shutdown)
+ cockpit.hint("restart");
+
+ cockpit.spawn(["shutdown", arg, this.state.when, this.state.message], { superuser: true, err: "message" })
+ .then(this.props.onClose || Dialogs.close)
+ .catch(e => this.setState({ error: e }));
+
+ event.preventDefault();
+ return false;
+ }
+
+ dateRangeValidator(date) {
+ if (this.state.startDate && date < this.state.startDate) {
+ return _("Cannot schedule event in the past");
+ }
+ return '';
+ }
+
+ render() {
+ const Dialogs = this.context;
+ const options = [
+ <SelectOption value="0" key="0">{_("No delay")}</SelectOption>,
+ <Divider key="divider" component="li" />,
+ <SelectOption value="1" key="1">{_("1 minute")}</SelectOption>,
+ <SelectOption value="5" key="5">{_("5 minutes")}</SelectOption>,
+ <SelectOption value="20" key="20">{_("20 minutes")}</SelectOption>,
+ <SelectOption value="40" key="40">{_("40 minutes")}</SelectOption>,
+ <SelectOption value="60" key="60">{_("60 minutes")}</SelectOption>,
+ <Divider key="divider-2" component="li" />,
+ <SelectOption value="x" key="x">{_("Specific time")}</SelectOption>
+ ];
+
+ return (
+ <Modal isOpen position="top" variant="medium"
+ onClose={this.props.onClose || Dialogs.close}
+ id="shutdown-dialog"
+ title={this.props.shutdown ? _("Shut down") : _("Reboot")}
+ footer={<>
+ <Button variant='danger' isDisabled={this.state.error || this.state.dateError} onClick={this.onSubmit}>{this.props.shutdown ? _("Shut down") : _("Reboot")}</Button>
+ <Button variant='link' onClick={this.props.onClose || Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <>
+ <Form isHorizontal onSubmit={this.onSubmit}>
+ <FormGroup fieldId="message" label={_("Message to logged in users")}>
+ <TextArea id="message" resizeOrientation="vertical" value={this.state.message} onChange={(_, v) => this.setState({ message: v })} />
+ </FormGroup>
+ <FormGroup fieldId="delay" label={_("Delay")}>
+ <Flex className="shutdown-delay-group" alignItems={{ default: 'alignItemsCenter' }}>
+ <Select toggleId="delay" isOpen={this.state.isOpen} selections={this.state.selected}
+ isDisabled={!this.state.formFilled}
+ className='shutdown-select-delay'
+ onToggle={(_event, o) => this.setState({ isOpen: o })} menuAppendTo="parent"
+ onSelect={(e, s) => this.setState({ selected: s, isOpen: false }, this.calculate)}>
+ {options}
+ </Select>
+ {this.state.selected === "x" && <>
+ <DatePicker aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ className='shutdown-date-picker'
+ dateFormat={timeformat.dateShort}
+ // https://github.com/patternfly/patternfly-react/issues/9721
+ dateParse={date => {
+ const newDate = timeformat.parseShortDate(date);
+ return Number.isNaN(newDate.valueOf()) ? false : newDate;
+ }}
+ invalidFormatText=""
+ isDisabled={!this.state.formFilled}
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ onBlur={this.calculate}
+ onChange={(_, d, ds) => this.updateDate(d, ds)}
+ placeholder={timeformat.dateShortFormat()}
+ validators={[this.dateRangeValidator]}
+ value={this.state.date}
+ appendTo={() => document.body} />
+ <TimePicker time={this.state.time} is24Hour
+ className='shutdown-time-picker'
+ id="shutdown-time"
+ isDisabled={!this.state.formFilled}
+ invalidFormatErrorMessage=""
+ menuAppendTo={() => document.body}
+ onBlur={this.calculate}
+ onChange={(_, time, h, m) => this.updateTime(time, h, m) } />
+ </>}
+ </Flex>
+ <FormHelper fieldId="delay" helperTextInvalid={this.state.dateError} />
+ </FormGroup>
+ </Form>
+ {this.state.error && <Alert isInline variant='danger' title={this.state.error} />}
+ </>
+ </Modal>
+ );
+ }
+}
diff --git a/pkg/lib/cockpit-components-shutdown.scss b/pkg/lib/cockpit-components-shutdown.scss
new file mode 100644
index 0000000..80a255f
--- /dev/null
+++ b/pkg/lib/cockpit-components-shutdown.scss
@@ -0,0 +1,17 @@
+#shutdown-group {
+ overflow: visible;
+}
+
+.shutdown-delay-group {
+ // Add spacing between rows for when the flex items wrap
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ .shutdown-time-picker {
+ max-inline-size: 7rem;
+ }
+
+ .shutdown-select-delay,
+ .shutdown-date-picker {
+ max-inline-size: 10rem;
+ }
+}
diff --git a/pkg/lib/cockpit-components-table.jsx b/pkg/lib/cockpit-components-table.jsx
new file mode 100644
index 0000000..fd472ca
--- /dev/null
+++ b/pkg/lib/cockpit-components-table.jsx
@@ -0,0 +1,297 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React, { useState, useEffect } from 'react';
+import {
+ ExpandableRowContent,
+ Table, Thead, Tbody, Tr, Th, Td,
+ SortByDirection,
+} from '@patternfly/react-table';
+import { EmptyState, EmptyStateBody, EmptyStateFooter, EmptyStateActions } from "@patternfly/react-core/dist/esm/components/EmptyState/index.js";
+import { Text, TextContent, TextVariants } from "@patternfly/react-core/dist/esm/components/Text/index.js";
+
+import './cockpit-components-table.scss';
+
+/* This is a wrapper around PF Table component
+ * See https://www.patternfly.org/components/table/
+ * Properties (all optional unless specified otherwise):
+ * - caption
+ * - id: optional identifier
+ * - className: additional classes added to the Table
+ * - actions: additional listing-wide actions (displayed next to the list's title)
+ * - columns: { title: string, header: boolean, sortable: boolean }[] or string[]
+ * - rows: {
+ * columns: (React.Node or string or { title: string, key: string, ...extraProps: object}}[]
+ Through extraProps the consumers can pass arbitrary properties to the <td>
+ * props: { key: string, ...extraProps: object }
+ This property is mandatory and should contain a unique `key`, all additional properties are optional.
+ Through extraProps the consumers can pass arbitrary properties to the <tr>
+ * expandedContent: (React.Node)[])
+ * selected: boolean option if the row is selected
+ * initiallyExpanded : the entry will be initially rendered as expanded, but then behaves normally
+ * }[]
+ * - emptyCaption: header caption to show if list is empty
+ * - emptyCaptionDetail: extra details to show after emptyCaption if list is empty
+ * - emptyComponent: Whole empty state component to show if the list is empty
+ * - isEmptyStateInTable: if empty state is result of a filter function this should be set, otherwise false
+ * - loading: Set to string when the content is still loading. This string is shown.
+ * - variant: For compact tables pass 'compact'
+ * - gridBreakPoint: Specifies the grid breakpoints ('', 'grid' | 'grid-md' | 'grid-lg' | 'grid-xl' | 'grid-2xl')
+ * - sortBy: { index: Number, direction: SortByDirection }
+ * - sortMethod: callback function used for sorting rows. Called with 3 parameters: sortMethod(rows, activeSortDirection, activeSortIndex)
+ * - style: object of additional css rules
+ * - afterToggle: function to be called when content is toggled
+ * - onSelect: function to be called when a checkbox is clicked. Called with 5 parameters:
+ * event, isSelected, rowIndex, rowData, extraData. rowData contains props with an id property of the clicked row.
+ * - onHeaderSelect: event, isSelected.
+ */
+export const ListingTable = ({
+ actions = [],
+ afterToggle,
+ caption = '',
+ className,
+ columns: cells = [],
+ emptyCaption = '',
+ emptyCaptionDetail,
+ emptyComponent,
+ isEmptyStateInTable = false,
+ loading = '',
+ onRowClick,
+ onSelect,
+ onHeaderSelect,
+ rows: tableRows = [],
+ showHeader = true,
+ sortBy,
+ sortMethod,
+ ...extraProps
+}) => {
+ let rows = [...tableRows];
+ const [expanded, setExpanded] = useState({});
+ const [newItems, setNewItems] = useState([]);
+ const [currentRowsKeys, setCurrentRowsKeys] = useState([]);
+ const [activeSortIndex, setActiveSortIndex] = useState(sortBy?.index ?? 0);
+ const [activeSortDirection, setActiveSortDirection] = useState(sortBy?.direction ?? SortByDirection.asc);
+ const rowKeys = rows.map(row => row.props?.key)
+ .filter(key => key !== undefined);
+ const rowKeysStr = JSON.stringify(rowKeys);
+ const currentRowsKeysStr = JSON.stringify(currentRowsKeys);
+
+ useEffect(() => {
+ // Don't highlight all when the list gets loaded
+ const _currentRowsKeys = JSON.parse(currentRowsKeysStr);
+ const _rowKeys = JSON.parse(rowKeysStr);
+
+ if (_currentRowsKeys.length !== 0) {
+ const new_keys = _rowKeys.filter(key => _currentRowsKeys.indexOf(key) === -1);
+ if (new_keys.length) {
+ setTimeout(() => setNewItems(items => items.filter(item => new_keys.indexOf(item) < 0)), 4000);
+ setNewItems(ni => [...ni, ...new_keys]);
+ }
+ }
+
+ setCurrentRowsKeys(crk => [...new Set([...crk, ..._rowKeys])]);
+ }, [currentRowsKeysStr, rowKeysStr]);
+
+ const isSortable = cells.some(col => col.sortable);
+ const isExpandable = rows.some(row => row.expandedContent);
+
+ const tableProps = {};
+
+ /* Basic table properties */
+ tableProps.className = "ct-table";
+ if (className)
+ tableProps.className = tableProps.className + " " + className;
+ if (rows.length == 0)
+ tableProps.className += ' ct-table-empty';
+
+ const header = (
+ (caption || actions.length != 0)
+ ? <header className='ct-table-header'>
+ <h3 className='ct-table-heading'> {caption} </h3>
+ {actions && <div className='ct-table-actions'> {actions} </div>}
+ </header>
+ : null
+ );
+
+ if (loading)
+ return <EmptyState>
+ <EmptyStateBody>
+ {loading}
+ </EmptyStateBody>
+ </EmptyState>;
+
+ if (rows == 0) {
+ let emptyState = null;
+ if (emptyComponent)
+ emptyState = emptyComponent;
+ else
+ emptyState = (
+ <EmptyState>
+ <EmptyStateBody>
+ <div>{emptyCaption}</div>
+ <TextContent>
+ <Text component={TextVariants.small}>
+ {emptyCaptionDetail}
+ </Text>
+ </TextContent>
+ </EmptyStateBody>
+ {actions.length > 0 &&
+ <EmptyStateFooter>
+ <EmptyStateActions>{actions}</EmptyStateActions>
+ </EmptyStateFooter>}
+ </EmptyState>
+ );
+ if (!isEmptyStateInTable)
+ return emptyState;
+
+ const emptyStateCell = (
+ [{
+ props: { colSpan: cells.length },
+ title: emptyState
+ }]
+ );
+
+ rows = [{ columns: emptyStateCell }];
+ }
+
+ const sortRows = () => {
+ const sortedRows = rows.sort((a, b) => {
+ const aitem = a.columns[activeSortIndex];
+ const bitem = b.columns[activeSortIndex];
+
+ return ((typeof aitem == 'string' ? aitem : (aitem.sortKey || aitem.title)).localeCompare(typeof bitem == 'string' ? bitem : (bitem.sortKey || bitem.title)));
+ });
+ return activeSortDirection === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const onSort = (event, index, direction) => {
+ setActiveSortIndex(index);
+ setActiveSortDirection(direction);
+ };
+
+ const rowsComponents = (isSortable ? (sortMethod ? sortMethod(rows, activeSortDirection, activeSortIndex) : sortRows()) : rows).map((row, rowIndex) => {
+ const rowProps = row.props || {};
+ if (onRowClick) {
+ rowProps.isClickable = true;
+ rowProps.onRowClick = (event) => onRowClick(event, row);
+ }
+
+ if (rowProps.key && newItems.indexOf(rowProps.key) >= 0)
+ rowProps.className = (rowProps.className || "") + " ct-new-item";
+
+ const rowKey = rowProps.key || rowIndex;
+ const isExpanded = expanded[rowKey] === undefined ? !!row.initiallyExpanded : expanded[rowKey];
+ const rowPair = (
+ <React.Fragment key={rowKey + "-inner-row"}>
+ <Tr {...rowProps}>
+ {isExpandable
+ ? (row.expandedContent
+ ? <Td expand={{
+ rowIndex: rowKey,
+ isExpanded,
+ onToggle: () => {
+ if (afterToggle)
+ afterToggle(!expanded[rowKey]);
+ setExpanded({ ...expanded, [rowKey]: !expanded[rowKey] });
+ }
+ }} />
+ : <Td className="pf-v5-c-table__toggle" />)
+ : null
+ }
+ {onSelect &&
+ <Td select={{
+ rowIndex,
+ onSelect,
+ isSelected: !!row.selected,
+ props: {
+ id: rowKey
+ }
+ }} />
+ }
+ {row.columns.map((cell, cellIndex) => {
+ const { key, ...cellProps } = cell.props || {};
+ const dataLabel = typeof cells[cellIndex] == 'object' ? cells[cellIndex].title : cells[cellIndex];
+ const colKey = dataLabel || cellIndex;
+ if (cells[cellIndex]?.header)
+ return (
+ <Th key={key || `row_${rowKey}_cell_${colKey}`} dataLabel={dataLabel} {...cellProps}>
+ {typeof cell == 'object' ? cell.title : cell}
+ </Th>
+ );
+
+ return (
+ <Td key={key || `row_${rowKey}_cell_${colKey}`} dataLabel={dataLabel} {...cellProps}>
+ {typeof cell == 'object' ? cell.title : cell}
+ </Td>
+ );
+ })}
+ </Tr>
+ {row.expandedContent && <Tr id={"expanded-content" + rowIndex} isExpanded={isExpanded}>
+ <Td noPadding={row.hasPadding !== true} colSpan={row.columns.length + 1 + (onSelect ? 1 : 0)}>
+ <ExpandableRowContent>{row.expandedContent}</ExpandableRowContent>
+ </Td>
+ </Tr>}
+ </React.Fragment>
+ );
+
+ return <Tbody key={rowKey} isExpanded={row.expandedContent && isExpanded}>{rowPair}</Tbody>;
+ });
+
+ return (
+ <>
+ {header}
+ <Table {...extraProps} {...tableProps}>
+ {showHeader && <Thead>
+ <Tr>
+ {isExpandable && <Th />}
+ {!onHeaderSelect && onSelect && <Th />}
+ {onHeaderSelect && onSelect && <Th select={{
+ onSelect: onHeaderSelect,
+ isSelected: rows.every(r => r.selected)
+ }} />}
+ {cells.map((column, columnIndex) => {
+ const columnProps = column.props;
+ const sortParams = (
+ column.sortable
+ ? {
+ sort: {
+ sortBy: {
+ index: activeSortIndex,
+ direction: activeSortDirection
+ },
+ onSort,
+ columnIndex
+ }
+ }
+ : {}
+ );
+
+ return (
+ <Th key={columnIndex} {...columnProps} {...sortParams}>
+ {typeof column == 'object' ? column.title : column}
+ </Th>
+ );
+ })}
+ </Tr>
+ </Thead>}
+ {rowsComponents}
+ </Table>
+ </>
+ );
+};
diff --git a/pkg/lib/cockpit-components-table.scss b/pkg/lib/cockpit-components-table.scss
new file mode 100644
index 0000000..6854ac1
--- /dev/null
+++ b/pkg/lib/cockpit-components-table.scss
@@ -0,0 +1,106 @@
+@import "global-variables";
+
+.ct-table {
+ &.pf-m-compact {
+ > thead, > tbody {
+ > tr:not(.pf-v5-c-table__expandable-row) {
+ // We actually want the normal font size for our lists
+ --pf-v5-c-table-cell--FontSize: var(--pf-v5-global--FontSize--md);
+ }
+ }
+ }
+
+ &-header {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+
+ > :only-child {
+ flex: auto;
+ }
+ }
+
+ &-heading {
+ // Push buttons to the right by stretching the heading
+ flex: auto;
+ // Add a bit of minimum margin to the right of the heading
+ margin-inline-end: var(--pf-v5-global--spacer--md);
+ // Set a minimum height of 3rem, so when buttons wrap, there's spacing
+ min-block-size: var(--pf-v5-global--spacer--2xl);
+ // Make sure textual content is aligned to the center
+ display: flex;
+ align-items: center;
+ }
+
+ &-actions {
+ > * {
+ margin-block: var(--pf-v5-global--spacer--xs);
+ margin-inline: var(--pf-v5-global--spacer--sm) 0;
+ }
+
+ > :first-child {
+ margin-inline-start: 0;
+ }
+ }
+
+ // https://github.com/patternfly/patternfly-react/issues/5379
+ &-empty {
+ [data-label] {
+ display: revert;
+ }
+
+ [data-label]::before {
+ display: none;
+ }
+ }
+
+ // Don't wrap labels
+ [data-label]::before {
+ white-space: nowrap;
+ }
+
+ // Fix toggle button alignment
+ .pf-v5-c-table__toggle {
+ // Workaround: Chrome sometimes oddly expands the table,
+ // unless a width is set. (This affects panels the most, but not only.)
+ // As the width is smaller than the contents, and this is a table,
+ // the cell will stay at the correct width.
+ inline-size: 1px;
+ }
+
+ // Properly align actions on the end
+ > tbody > tr > td:last-child > .btn-group {
+ display: flex;
+ justify-content: flex-end;
+ align-items: center;
+ }
+
+ // Use PF4 style headings
+ > thead th {
+ font-size: var(--pf-v5-global--FontSize--sm);
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ }
+
+ // Adjust the padding for nested ct-tables in ct-tables
+ // FIXME: https://github.com/patternfly/patternfly/issues/4280
+ .ct-table {
+ td, th {
+ &:first-child {
+ --pf-v5-c-table--nested--first-last-child--PaddingLeft: var(--pf-v5-global--spacer--lg);
+ }
+
+ &:last-child {
+ --pf-v5-c-table--nested--first-last-child--PaddingRight: var(--pf-v5-global--spacer--lg);
+ }
+ }
+ }
+}
+
+// Special handling for rows with errors
+.pf-v5-c-table tbody tr:first-child.error {
+ &, tbody.pf-m-expanded > & {
+ background-color: var(--ct-color-list-critical-bg) !important; /* keep red background when expanded */
+ border-block-start: 1px solid var(--ct-color-list-critical-border);
+ border-block-end: 1px solid var(--ct-color-list-critical-border);
+ }
+}
diff --git a/pkg/lib/cockpit-components-terminal.jsx b/pkg/lib/cockpit-components-terminal.jsx
new file mode 100644
index 0000000..ede83ec
--- /dev/null
+++ b/pkg/lib/cockpit-components-terminal.jsx
@@ -0,0 +1,362 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+import PropTypes from "prop-types";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal/index.js";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { MenuList, MenuItem } from "@patternfly/react-core/dist/esm/components/Menu";
+import { Terminal as Term } from "xterm";
+import { CanvasAddon } from 'xterm-addon-canvas';
+
+import { ContextMenu } from "cockpit-components-context-menu.jsx";
+import cockpit from "cockpit";
+
+import "console.css";
+
+const _ = cockpit.gettext;
+
+const theme_core = {
+ yellow: "#b58900",
+ brightRed: "#cb4b16",
+ red: "#dc322f",
+ magenta: "#d33682",
+ brightMagenta: "#6c71c4",
+ blue: "#268bd2",
+ cyan: "#2aa198",
+ green: "#859900"
+};
+
+const themes = {
+ "black-theme": {
+ background: "#000000",
+ foreground: "#ffffff"
+ },
+ "dark-theme": Object.assign({}, theme_core, {
+ background: "#002b36",
+ foreground: "#fdf6e3",
+ cursor: "#eee8d5",
+ selection: "#ffffff77",
+ brightBlack: "#002b36",
+ black: "#073642",
+ brightGreen: "#586e75",
+ brightYellow: "#657b83",
+ brightBlue: "#839496",
+ brightCyan: "#93a1a1",
+ white: "#eee8d5",
+ brightWhite: "#fdf6e3"
+ }),
+ "light-theme": Object.assign({}, theme_core, {
+ background: "#fdf6e3",
+ foreground: "#002b36",
+ cursor: "#073642",
+ selection: "#00000044",
+ brightWhite: "#002b36",
+ white: "#073642",
+ brightCyan: "#586e75",
+ brightBlue: "#657b83",
+ brightYellow: "#839496",
+ brightGreen: "#93a1a1",
+ black: "#eee8d5",
+ brightBlack: "#fdf6e3"
+ }),
+ "white-theme": {
+ background: "#ffffff",
+ foreground: "#000000",
+ selection: "#00000044",
+ cursor: "#000000",
+ },
+};
+
+/*
+ * A terminal component that communicates over a cockpit channel.
+ *
+ * The only required property is 'channel', which must point to a cockpit
+ * stream channel.
+ *
+ * The size of the terminal can be set with the 'rows' and 'cols'
+ * properties. If those properties are not given, the terminal will fill
+ * its container.
+ *
+ * If the 'onTitleChanged' callback property is set, it will be called whenever
+ * the title of the terminal changes.
+ *
+ * Call focus() to set the input focus on the terminal.
+ *
+ * Also it is possible to set up theme by property 'theme'.
+ */
+export class Terminal extends React.Component {
+ constructor(props) {
+ super(props);
+ this.onChannelMessage = this.onChannelMessage.bind(this);
+ this.onChannelClose = this.onChannelClose.bind(this);
+ this.connectChannel = this.connectChannel.bind(this);
+ this.disconnectChannel = this.disconnectChannel.bind(this);
+ this.reset = this.reset.bind(this);
+ this.focus = this.focus.bind(this);
+ this.onWindowResize = this.onWindowResize.bind(this);
+ this.resizeTerminal = this.resizeTerminal.bind(this);
+ this.onFocusIn = this.onFocusIn.bind(this);
+ this.onFocusOut = this.onFocusOut.bind(this);
+ this.setText = this.setText.bind(this);
+ this.getText = this.getText.bind(this);
+ this.setTerminalTheme = this.setTerminalTheme.bind(this);
+
+ const term = new Term({
+ cols: props.cols || 80,
+ rows: props.rows || 25,
+ screenKeys: true,
+ cursorBlink: true,
+ fontSize: props.fontSize || 16,
+ fontFamily: 'Menlo, Monaco, Consolas, monospace',
+ screenReaderMode: true,
+ showPastingModal: false,
+ });
+
+ this.terminalRef = React.createRef();
+
+ term.onData(function(data) {
+ if (this.props.channel.valid)
+ this.props.channel.send(data);
+ }.bind(this));
+
+ if (props.onTitleChanged)
+ term.onTitleChange(props.onTitleChanged);
+
+ this.terminal = term;
+ this.state = {
+ showPastingModal: false,
+ cols: props.cols || 80,
+ rows: props.rows || 25
+ };
+ }
+
+ componentDidMount() {
+ this.terminal.open(this.terminalRef.current);
+ this.terminal.loadAddon(new CanvasAddon());
+ this.connectChannel();
+
+ if (!this.props.rows) {
+ window.addEventListener('resize', this.onWindowResize);
+ this.onWindowResize();
+ }
+ this.setTerminalTheme(this.props.theme || 'black-theme');
+ this.terminal.focus();
+ }
+
+ resizeTerminal(cols, rows) {
+ this.terminal.resize(cols, rows);
+ this.props.channel.control({
+ window: {
+ rows,
+ cols
+ }
+ });
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ if (prevProps.fontSize !== this.props.fontSize) {
+ this.terminal.options.fontSize = this.props.fontSize;
+
+ // After font size is changed, resize needs to be triggered
+ const dimensions = this.calculateDimensions();
+ if (dimensions.cols !== this.state.cols || dimensions.rows !== this.state.rows) {
+ this.onWindowResize();
+ } else {
+ // When font size changes but dimensions are the same, we need to force `resize`
+ this.resizeTerminal(dimensions.cols - 1, dimensions.rows);
+ }
+ }
+
+ if (prevState.cols !== this.state.cols || prevState.rows !== this.state.rows)
+ this.resizeTerminal(this.state.cols, this.state.rows);
+
+ if (prevProps.theme !== this.props.theme)
+ this.setTerminalTheme(this.props.theme);
+
+ if (prevProps.channel !== this.props.channel) {
+ this.terminal.reset();
+ this.disconnectChannel(prevProps.channel);
+ this.connectChannel();
+ this.props.channel.control({
+ window: {
+ rows: this.state.rows,
+ cols: this.state.cols
+ }
+ });
+ }
+ this.terminal.focus();
+ }
+
+ render() {
+ const contextMenuList = (
+ <MenuList>
+ <MenuItem className="contextMenuOption" onClick={this.getText}>
+ <div className="contextMenuName"> { _("Copy") } </div>
+ <div className="contextMenuShortcut">{ _("Ctrl+Insert") }</div>
+ </MenuItem>
+ <MenuItem className="contextMenuOption" onClick={this.setText}>
+ <div className="contextMenuName"> { _("Paste") } </div>
+ <div className="contextMenuShortcut">{ _("Shift+Insert") }</div>
+ </MenuItem>
+ </MenuList>
+ );
+
+ return (
+ <>
+ <Modal title={_("Paste error")}
+ position="top"
+ variant="small"
+ isOpen={this.state.showPastingModal}
+ onClose={() => this.setState({ showPastingModal: false })}
+ actions={[
+ <Button key="cancel" variant="secondary" onClick={() => this.setState({ showPastingModal: false })}>
+ {_("Close")}
+ </Button>
+ ]}>
+ {_("Your browser does not allow paste from the context menu. You can use Shift+Insert.")}
+ </Modal>
+ <div ref={this.terminalRef}
+ key={this.terminal}
+ className="console-ct"
+ onFocus={this.onFocusIn}
+ onContextMenu={this.contextMenu}
+ onBlur={this.onFocusOut} />
+ <ContextMenu parentId={this.props.parentId}>
+ {contextMenuList}
+ </ContextMenu>
+ </>
+ );
+ }
+
+ componentWillUnmount() {
+ this.disconnectChannel();
+ this.terminal.dispose();
+ window.removeEventListener('resize', this.onWindowResize);
+ this.onFocusOut();
+ }
+
+ setText() {
+ try {
+ navigator.clipboard.readText()
+ .then(text => this.props.channel.send(text))
+ .catch(e => this.setState({ showPastingModal: true }))
+ .finally(() => this.terminal.focus());
+ } catch (error) {
+ this.setState({ showPastingModal: true });
+ }
+ }
+
+ getText() {
+ try {
+ navigator.clipboard.writeText(this.terminal.getSelection())
+ .catch(e => console.error('Text could not be copied, use Ctrl+Insert ', e ? e.toString() : ""))
+ .finally(() => this.terminal.focus());
+ } catch (error) {
+ console.error('Text could not be copied, use Ctrl+Insert:', error.toString());
+ }
+ }
+
+ onChannelMessage(event, data) {
+ this.terminal.write(data);
+ }
+
+ onChannelClose(event, options) {
+ const term = this.terminal;
+ term.write('\x1b[31m' + (options.problem || 'disconnected') + '\x1b[m\r\n');
+ term.cursorHidden = true;
+ term.refresh(term.rows, term.rows);
+ }
+
+ connectChannel() {
+ const channel = this.props.channel;
+ if (channel?.valid) {
+ channel.addEventListener('message', this.onChannelMessage.bind(this));
+ channel.addEventListener('close', this.onChannelClose.bind(this));
+ }
+ }
+
+ disconnectChannel(channel) {
+ if (channel === undefined)
+ channel = this.props.channel;
+ if (channel) {
+ channel.removeEventListener('message', this.onChannelMessage);
+ channel.removeEventListener('close', this.onChannelClose);
+ }
+ channel.close();
+ }
+
+ reset() {
+ this.terminal.reset();
+ this.props.channel.send(String.fromCharCode(12)); // Send SIGWINCH to show prompt on attaching
+ }
+
+ focus() {
+ if (this.terminal)
+ this.terminal.focus();
+ }
+
+ calculateDimensions() {
+ const padding = 10; // Leave a bit of space around terminal
+ const realHeight = this.terminal._core._renderService.dimensions.css.cell.height;
+ const realWidth = this.terminal._core._renderService.dimensions.css.cell.width;
+ if (realHeight && realWidth && realWidth !== 0 && realHeight !== 0)
+ return {
+ rows: Math.floor((this.terminalRef.current.parentElement.clientHeight - padding) / realHeight),
+ cols: Math.floor((this.terminalRef.current.parentElement.clientWidth - padding - 12) / realWidth) // Remove 12px for scrollbar
+ };
+
+ return { rows: this.state.rows, cols: this.state.cols };
+ }
+
+ onWindowResize() {
+ this.setState(this.calculateDimensions());
+ }
+
+ setTerminalTheme(theme) {
+ this.terminal.options.theme = themes[theme];
+ }
+
+ onBeforeUnload(event) {
+ // Firefox requires this when the page is in an iframe
+ event.preventDefault();
+
+ // see "an almost cross-browser solution" at
+ // https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
+ event.returnValue = '';
+ return '';
+ }
+
+ onFocusIn() {
+ window.addEventListener('beforeunload', this.onBeforeUnload);
+ }
+
+ onFocusOut() {
+ window.removeEventListener('beforeunload', this.onBeforeUnload);
+ }
+}
+
+Terminal.propTypes = {
+ cols: PropTypes.number,
+ rows: PropTypes.number,
+ channel: PropTypes.object.isRequired,
+ onTitleChanged: PropTypes.func,
+ theme: PropTypes.string,
+ parentId: PropTypes.string.isRequired
+};
diff --git a/pkg/lib/cockpit-components-truncate.jsx b/pkg/lib/cockpit-components-truncate.jsx
new file mode 100644
index 0000000..5fb5322
--- /dev/null
+++ b/pkg/lib/cockpit-components-truncate.jsx
@@ -0,0 +1,42 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2024 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* This is our version of the PatternFly Truncate component. We have
+ it since we don't want Patternfly's unconditional tooltip.
+
+ Truncation in the middle doesn't work with Patternsfly's approach
+ in mixed RTL/LTR environments, so we only offer truncation at the
+ end.
+ */
+
+import * as React from "react";
+import './cockpit-components-truncate.scss';
+
+export const Truncate = ({
+ content,
+ ...props
+}) => {
+ return (
+ <span className="pf-v5-c-truncate ct-no-truncate-min-width" {...props}>
+ <span className="pf-v5-c-truncate__start">
+ {content}
+ </span>
+ </span>
+ );
+};
diff --git a/pkg/lib/cockpit-components-truncate.scss b/pkg/lib/cockpit-components-truncate.scss
new file mode 100644
index 0000000..e557ee1
--- /dev/null
+++ b/pkg/lib/cockpit-components-truncate.scss
@@ -0,0 +1,4 @@
+.pf-v5-c-truncate.ct-no-truncate-min-width {
+ --pf-v5-c-truncate--MinWidth: 0;
+ --pf-v5-c-truncate__start--MinWidth: 0;
+}
diff --git a/pkg/lib/cockpit-dark-theme.js b/pkg/lib/cockpit-dark-theme.js
new file mode 100644
index 0000000..00249fa
--- /dev/null
+++ b/pkg/lib/cockpit-dark-theme.js
@@ -0,0 +1,70 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function debug() {
+ if (window.debugging == "all" || window.debugging?.includes("style")) {
+ console.debug([`cockpit-dark-theme: ${document.documentElement.id}:`, ...arguments].join(" "));
+ }
+}
+
+function changeDarkThemeClass(documentElement, dark_mode) {
+ debug(`Setting cockpit theme to ${dark_mode ? "dark" : "light"}`);
+
+ if (dark_mode) {
+ documentElement.classList.add('pf-v5-theme-dark');
+ } else {
+ documentElement.classList.remove('pf-v5-theme-dark');
+ }
+}
+
+function _setDarkMode(_style) {
+ const style = _style || localStorage.getItem('shell:style') || 'auto';
+ let dark_mode;
+ // If a user set's an explicit theme, ignore system changes.
+ if ((window.matchMedia?.('(prefers-color-scheme: dark)').matches && style === "auto") || style === "dark") {
+ dark_mode = true;
+ } else {
+ dark_mode = false;
+ }
+ changeDarkThemeClass(document.documentElement, dark_mode);
+}
+
+window.addEventListener("storage", event => {
+ if (event.key === "shell:style") {
+ debug(`Storage element 'shell:style' changed from ${event.oldValue} to ${event.newValue}`);
+
+ _setDarkMode();
+ }
+});
+
+// When changing the theme from the shell switcher the localstorage change will not fire for the same page (aka shell)
+// so we need to listen for the event on the window object.
+window.addEventListener("cockpit-style", event => {
+ const style = event.detail.style;
+ debug(`Event received from shell with 'cockpit-style' ${style}`);
+
+ _setDarkMode(style);
+});
+
+window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
+ debug(`Operating system theme preference changed to ${window.matchMedia?.('(prefers-color-scheme: dark)').matches ? "dark" : "light"}`);
+ _setDarkMode();
+});
+
+_setDarkMode();
diff --git a/pkg/lib/cockpit-po-plugin.js b/pkg/lib/cockpit-po-plugin.js
new file mode 100644
index 0000000..6cba648
--- /dev/null
+++ b/pkg/lib/cockpit-po-plugin.js
@@ -0,0 +1,133 @@
+import fs from "fs";
+import glob from "glob";
+import path from "path";
+
+import Jed from "jed";
+import gettext_parser from "gettext-parser";
+
+const config = {};
+
+const DEFAULT_WRAPPER = 'cockpit.locale(PO_DATA);';
+
+function get_po_files() {
+ try {
+ const linguas_file = path.resolve(config.srcdir, "po/LINGUAS");
+ const linguas = fs.readFileSync(linguas_file, 'utf8').match(/\S+/g);
+ return linguas.map(lang => path.resolve(config.srcdir, 'po', lang + '.po'));
+ } catch (error) {
+ if (error.code !== 'ENOENT') {
+ throw error;
+ }
+
+ /* No LINGUAS file? Fall back to globbing.
+ * Note: we won't detect .po files being added in this case.
+ */
+ return glob.sync(path.resolve(config.srcdir, 'po/*.po'));
+ }
+}
+
+function get_plural_expr(statement) {
+ try {
+ /* Check that the plural forms isn't being sneaky since we build a function here */
+ Jed.PF.parse(statement);
+ } catch (ex) {
+ console.error("bad plural forms: " + ex.message);
+ process.exit(1);
+ }
+
+ const expr = statement.replace(/nplurals=[1-9]; plural=([^;]*);?$/, '(n) => $1');
+ if (expr === statement) {
+ console.error("bad plural forms: " + statement);
+ process.exit(1);
+ }
+
+ return expr;
+}
+
+function buildFile(po_file, subdir, filename, filter) {
+ return new Promise((resolve, reject) => {
+ // Read the PO file, remove fuzzy/disabled lines to avoid tripping up the validator
+ const po_data = fs.readFileSync(po_file, 'utf8')
+ .split('\n')
+ .filter(line => !line.startsWith('#~'))
+ .join('\n');
+ const parsed = gettext_parser.po.parse(po_data, { defaultCharset: 'utf8', validation: true });
+ delete parsed.translations[""][""]; // second header copy
+
+ const rtl_langs = ["ar", "fa", "he", "ur"];
+ const dir = rtl_langs.includes(parsed.headers.Language) ? "rtl" : "ltr";
+
+ // cockpit.js only looks at "plural-forms" and "language"
+ const chunks = [
+ '{\n',
+ ' "": {\n',
+ ` "plural-forms": ${get_plural_expr(parsed.headers['Plural-Forms'])},\n`,
+ ` "language": "${parsed.headers.Language}",\n`,
+ ` "language-direction": "${dir}"\n`,
+ ' }'
+ ];
+ for (const [msgctxt, context] of Object.entries(parsed.translations)) {
+ const context_prefix = msgctxt ? msgctxt + '\u0004' : ''; /* for cockpit.ngettext */
+
+ for (const [msgid, translation] of Object.entries(context)) {
+ /* Only include msgids which appear in this source directory */
+ const references = translation.comments.reference.split(/\s/);
+ if (!references.some(str => str.startsWith(`pkg/${subdir}`) || str.startsWith(config.src_directory) || str.startsWith(`pkg/lib`)))
+ continue;
+
+ if (translation.comments.flag?.match(/\bfuzzy\b/))
+ continue;
+
+ if (!references.some(filter))
+ continue;
+
+ const key = JSON.stringify(context_prefix + msgid);
+ // cockpit.js always ignores the first item
+ chunks.push(`,\n ${key}: [\n null`);
+ for (const str of translation.msgstr) {
+ chunks.push(',\n ' + JSON.stringify(str));
+ }
+ chunks.push('\n ]');
+ }
+ }
+ chunks.push('\n}');
+
+ const wrapper = config.wrapper?.(subdir) || DEFAULT_WRAPPER;
+ const output = wrapper.replace('PO_DATA', chunks.join('')) + '\n';
+
+ const out_path = path.join(subdir ? (subdir + '/') : '', filename);
+ fs.writeFileSync(path.resolve(config.outdir, out_path), output);
+ return resolve();
+ });
+}
+
+function init(options) {
+ config.srcdir = process.env.SRCDIR || './';
+ config.subdirs = options.subdirs || [''];
+ config.src_directory = options.src_directory || 'src';
+ config.wrapper = options.wrapper;
+ config.outdir = options.outdir || './dist';
+}
+
+function run() {
+ const promises = [];
+ for (const subdir of config.subdirs) {
+ for (const po_file of get_po_files()) {
+ const lang = path.basename(po_file).slice(0, -3);
+ promises.push(Promise.all([
+ // Separate translations for the manifest.json file and normal pages
+ buildFile(po_file, subdir, `po.${lang}.js`, str => !str.includes('manifest.json')),
+ buildFile(po_file, subdir, `po.manifest.${lang}.js`, str => str.includes('manifest.json'))
+ ]));
+ }
+ }
+ return Promise.all(promises);
+}
+
+export const cockpitPoEsbuildPlugin = options => ({
+ name: 'cockpitPoEsbuildPlugin',
+ setup(build) {
+ init({ ...options, outdir: build.initialOptions.outdir });
+ build.onEnd(async result => { result.errors.length === 0 && await run() });
+ },
+});
diff --git a/pkg/lib/cockpit-rsync-plugin.js b/pkg/lib/cockpit-rsync-plugin.js
new file mode 100644
index 0000000..bb26be3
--- /dev/null
+++ b/pkg/lib/cockpit-rsync-plugin.js
@@ -0,0 +1,49 @@
+import child_process from "child_process";
+
+const config = {};
+
+function init(options) {
+ config.dest = options.dest || "";
+ config.source = options.source || "dist/";
+ config.ssh_host = process.env.RSYNC || process.env.RSYNC_DEVEL;
+
+ // ensure the target directory exists
+ if (config.ssh_host) {
+ config.rsync_dir = process.env.RSYNC ? "/usr/local/share/cockpit/" : "~/.local/share/cockpit/";
+ child_process.spawnSync("ssh", [config.ssh_host, "mkdir", "-p", config.rsync_dir], { stdio: "inherit" });
+ }
+}
+
+function run(callback) {
+ if (config.ssh_host) {
+ const proc = child_process.spawn("rsync", ["--recursive", "--info=PROGRESS2", "--delete",
+ config.source, config.ssh_host + ":" + config.rsync_dir + config.dest], { stdio: "inherit" });
+ proc.on('close', (code) => {
+ if (code !== 0) {
+ process.exit(1);
+ } else {
+ callback();
+ }
+ });
+ } else {
+ callback();
+ }
+}
+
+export const cockpitRsyncEsbuildPlugin = options => ({
+ name: 'cockpitRsyncPlugin',
+ setup(build) {
+ init(options || {});
+ build.onEnd(result => result.errors.length === 0 ? run(() => {}) : {});
+ },
+});
+
+export class CockpitRsyncWebpackPlugin {
+ constructor(options) {
+ init(options || {});
+ }
+
+ apply(compiler) {
+ compiler.hooks.afterEmit.tapAsync('WebpackHookPlugin', (_compilation, callback) => run(callback));
+ }
+}
diff --git a/pkg/lib/cockpit.js b/pkg/lib/cockpit.js
new file mode 100644
index 0000000..bef8e61
--- /dev/null
+++ b/pkg/lib/cockpit.js
@@ -0,0 +1,4444 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* eslint-disable indent,no-empty */
+
+let url_root;
+
+const meta_url_root = document.head.querySelector("meta[name='url-root']");
+if (meta_url_root) {
+ url_root = meta_url_root.content.replace(/^\/+|\/+$/g, '');
+} else {
+ // fallback for cockpit-ws < 272
+ try {
+ // Sometimes this throws a SecurityError such as during testing
+ url_root = window.localStorage.getItem('url-root');
+ } catch (e) { }
+}
+
+/* injected by tests */
+var mock = mock || { }; // eslint-disable-line no-use-before-define, no-var
+
+const cockpit = { };
+event_mixin(cockpit, { });
+
+/*
+ * The debugging property is a global that is used
+ * by various parts of the code to show/hide debug
+ * messages in the javascript console.
+ *
+ * We support using storage to get/set that property
+ * so that it carries across the various frames or
+ * alternatively persists across refreshes.
+ */
+if (typeof window.debugging === "undefined") {
+ try {
+ // Sometimes this throws a SecurityError such as during testing
+ Object.defineProperty(window, "debugging", {
+ get: function() { return window.sessionStorage.debugging || window.localStorage.debugging },
+ set: function(x) { window.sessionStorage.debugging = x }
+ });
+ } catch (e) { }
+}
+
+function in_array(array, val) {
+ const length = array.length;
+ for (let i = 0; i < length; i++) {
+ if (val === array[i])
+ return true;
+ }
+ return false;
+}
+
+function is_function(x) {
+ return typeof x === 'function';
+}
+
+function is_object(x) {
+ return x !== null && typeof x === 'object';
+}
+
+function is_plain_object(x) {
+ return is_object(x) && Object.prototype.toString.call(x) === '[object Object]';
+}
+
+/* Also works for negative zero */
+function is_negative(n) {
+ return ((n = +n) || 1 / n) < 0;
+}
+
+function invoke_functions(functions, self, args) {
+ const length = functions?.length ?? 0;
+ for (let i = 0; i < length; i++) {
+ if (functions[i])
+ functions[i].apply(self, args);
+ }
+}
+
+function iterate_data(data, callback, batch) {
+ let binary = false;
+ let len = 0;
+
+ if (!batch)
+ batch = 64 * 1024;
+
+ if (data) {
+ if (data.byteLength) {
+ len = data.byteLength;
+ binary = true;
+ } else if (data.length) {
+ len = data.length;
+ }
+ }
+
+ for (let i = 0; i < len; i += batch) {
+ const n = Math.min(len - i, batch);
+ if (binary)
+ callback(new window.Uint8Array(data.buffer, i, n));
+ else
+ callback(data.substr(i, n));
+ }
+}
+
+/* -------------------------------------------------------------------------
+ * Channels
+ *
+ * Public: https://cockpit-project.org/guide/latest/api-base1.html
+ */
+
+let default_transport = null;
+let public_transport = null;
+let reload_after_disconnect = false;
+let expect_disconnect = false;
+let init_callback = null;
+let default_host = null;
+let process_hints = null;
+let incoming_filters = null;
+let outgoing_filters = null;
+
+let transport_origin = window.location.origin;
+
+if (!transport_origin) {
+ transport_origin = window.location.protocol + "//" + window.location.hostname +
+ (window.location.port ? ':' + window.location.port : '');
+}
+
+function array_from_raw_string(str, constructor) {
+ const length = str.length;
+ const data = new (constructor || Array)(length);
+ for (let i = 0; i < length; i++)
+ data[i] = str.charCodeAt(i) & 0xFF;
+ return data;
+}
+
+function array_to_raw_string(data) {
+ const length = data.length;
+ let str = "";
+ for (let i = 0; i < length; i++)
+ str += String.fromCharCode(data[i]);
+ return str;
+}
+
+/*
+ * These are the polyfills from Mozilla. It's pretty nasty that
+ * these weren't in the typed array standardization.
+ *
+ * https://developer.mozilla.org/en-US/docs/Glossary/Base64
+ */
+
+function uint6_to_b64 (x) {
+ return x < 26 ? x + 65 : x < 52 ? x + 71 : x < 62 ? x - 4 : x === 62 ? 43 : x === 63 ? 47 : 65;
+}
+
+function base64_encode(data) {
+ if (typeof data === "string")
+ return window.btoa(data);
+ /* For when the caller has chosen to use ArrayBuffer */
+ if (data instanceof window.ArrayBuffer)
+ data = new window.Uint8Array(data);
+ const length = data.length;
+ let mod3 = 2;
+ let str = "";
+ for (let uint24 = 0, i = 0; i < length; i++) {
+ mod3 = i % 3;
+ uint24 |= data[i] << (16 >>> mod3 & 24);
+ if (mod3 === 2 || length - i === 1) {
+ str += String.fromCharCode(uint6_to_b64(uint24 >>> 18 & 63),
+ uint6_to_b64(uint24 >>> 12 & 63),
+ uint6_to_b64(uint24 >>> 6 & 63),
+ uint6_to_b64(uint24 & 63));
+ uint24 = 0;
+ }
+ }
+
+ return str.substr(0, str.length - 2 + mod3) + (mod3 === 2 ? '' : mod3 === 1 ? '=' : '==');
+}
+
+function b64_to_uint6 (x) {
+ return x > 64 && x < 91
+ ? x - 65
+ : x > 96 && x < 123
+ ? x - 71
+ : x > 47 && x < 58 ? x + 4 : x === 43 ? 62 : x === 47 ? 63 : 0;
+}
+
+function base64_decode(str, constructor) {
+ if (constructor === String)
+ return window.atob(str);
+ const ilen = str.length;
+ let eq;
+ for (eq = 0; eq < 3; eq++) {
+ if (str[ilen - (eq + 1)] != '=')
+ break;
+ }
+ const olen = (ilen * 3 + 1 >> 2) - eq;
+ const data = new (constructor || Array)(olen);
+ for (let mod3, mod4, uint24 = 0, oi = 0, ii = 0; ii < ilen; ii++) {
+ mod4 = ii & 3;
+ uint24 |= b64_to_uint6(str.charCodeAt(ii)) << 18 - 6 * mod4;
+ if (mod4 === 3 || ilen - ii === 1) {
+ for (mod3 = 0; mod3 < 3 && oi < olen; mod3++, oi++)
+ data[oi] = uint24 >>> (16 >>> mod3 & 24) & 255;
+ uint24 = 0;
+ }
+ }
+ return data;
+}
+
+window.addEventListener('beforeunload', function() {
+ expect_disconnect = true;
+}, false);
+
+function transport_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("channel"))
+ console.debug.apply(console, arguments);
+}
+
+/*
+ * Extends an object to have the standard DOM style addEventListener
+ * removeEventListener and dispatchEvent methods. The dispatchEvent
+ * method has the additional capability to create a new event from a type
+ * string and arguments.
+ */
+function event_mixin(obj, handlers) {
+ Object.defineProperties(obj, {
+ addEventListener: {
+ enumerable: false,
+ value: function addEventListener(type, handler) {
+ if (handlers[type] === undefined)
+ handlers[type] = [];
+ handlers[type].push(handler);
+ }
+ },
+ removeEventListener: {
+ enumerable: false,
+ value: function removeEventListener(type, handler) {
+ const length = handlers[type] ? handlers[type].length : 0;
+ for (let i = 0; i < length; i++) {
+ if (handlers[type][i] === handler) {
+ handlers[type][i] = null;
+ break;
+ }
+ }
+ }
+ },
+ dispatchEvent: {
+ enumerable: false,
+ value: function dispatchEvent(event) {
+ let type, args;
+ if (typeof event === "string") {
+ type = event;
+ args = Array.prototype.slice.call(arguments, 1);
+
+ let detail = null;
+ if (arguments.length == 2)
+ detail = arguments[1];
+ else if (arguments.length > 2)
+ detail = args;
+
+ event = new CustomEvent(type, {
+ bubbles: false,
+ cancelable: false,
+ detail
+ });
+
+ args.unshift(event);
+ } else {
+ type = event.type;
+ args = arguments;
+ }
+ if (is_function(obj['on' + type]))
+ obj['on' + type].apply(obj, args);
+ invoke_functions(handlers[type], obj, args);
+ }
+ }
+ });
+}
+
+function calculate_application() {
+ let path = window.location.pathname || "/";
+ let _url_root = url_root;
+ if (window.mock?.pathname)
+ path = window.mock.pathname;
+ if (window.mock?.url_root)
+ _url_root = window.mock.url_root;
+
+ if (_url_root && path.indexOf('/' + _url_root) === 0)
+ path = path.replace('/' + _url_root, '') || '/';
+
+ if (path.indexOf("/cockpit/") !== 0 && path.indexOf("/cockpit+") !== 0) {
+ if (path.indexOf("/=") === 0)
+ path = "/cockpit+" + path.split("/")[1];
+ else
+ path = "/cockpit";
+ }
+
+ return path.split("/")[1];
+}
+
+function calculate_url(suffix) {
+ if (!suffix)
+ suffix = "socket";
+ const window_loc = window.location.toString();
+ let _url_root = url_root;
+
+ if (window.mock?.url)
+ return window.mock.url;
+ if (window.mock?.url_root)
+ _url_root = window.mock.url_root;
+
+ let prefix = calculate_application();
+ if (_url_root)
+ prefix = _url_root + "/" + prefix;
+
+ if (window_loc.indexOf('http:') === 0) {
+ return "ws://" + window.location.host + "/" + prefix + "/" + suffix;
+ } else if (window_loc.indexOf('https:') === 0) {
+ return "wss://" + window.location.host + "/" + prefix + "/" + suffix;
+ } else {
+ transport_debug("Cockpit must be used over http or https");
+ return null;
+ }
+}
+
+function join_data(buffers, binary) {
+ if (!binary)
+ return buffers.join("");
+
+ let total = 0;
+ const length = buffers.length;
+ for (let i = 0; i < length; i++)
+ total += buffers[i].length;
+
+ const data = window.Uint8Array ? new window.Uint8Array(total) : new Array(total);
+
+ if (data.set) {
+ for (let j = 0, i = 0; i < length; i++) {
+ data.set(buffers[i], j);
+ j += buffers[i].length;
+ }
+ } else {
+ for (let j = 0, i = 0; i < length; i++) {
+ for (let k = 0; k < buffers[i].length; k++)
+ data[i + j] = buffers[i][k];
+ j += buffers[i].length;
+ }
+ }
+
+ return data;
+}
+
+/*
+ * A WebSocket that connects to parent frame. The mechanism
+ * for doing this will eventually be documented publicly,
+ * but for now:
+ *
+ * * Forward raw cockpit1 string protocol messages via window.postMessage
+ * * Listen for cockpit1 string protocol messages via window.onmessage
+ * * Never accept or send messages to another origin
+ * * An empty string message means "close" (not completely used yet)
+ */
+function ParentWebSocket(parent) {
+ const self = this;
+ self.readyState = 0;
+
+ window.addEventListener("message", function receive(event) {
+ if (event.origin !== transport_origin || event.source !== parent)
+ return;
+ const data = event.data;
+ if (data === undefined || (data.length === undefined && data.byteLength === undefined))
+ return;
+ if (data.length === 0) {
+ self.readyState = 3;
+ self.onclose();
+ } else {
+ self.onmessage(event);
+ }
+ }, false);
+
+ self.send = function send(message) {
+ parent.postMessage(message, transport_origin);
+ };
+
+ self.close = function close() {
+ self.readyState = 3;
+ parent.postMessage("", transport_origin);
+ self.onclose();
+ };
+
+ window.setTimeout(function() {
+ self.readyState = 1;
+ self.onopen();
+ }, 0);
+}
+
+function parse_channel(data) {
+ let channel;
+
+ /* A binary message, split out the channel */
+ if (data instanceof window.ArrayBuffer) {
+ const binary = new window.Uint8Array(data);
+ const length = binary.length;
+ let pos;
+ for (pos = 0; pos < length; pos++) {
+ if (binary[pos] == 10) /* new line */
+ break;
+ }
+ if (pos === length) {
+ console.warn("binary message without channel");
+ return null;
+ } else if (pos === 0) {
+ console.warn("binary control message");
+ return null;
+ } else {
+ channel = String.fromCharCode.apply(null, binary.subarray(0, pos));
+ }
+
+ /* A textual message */
+ } else {
+ const pos = data.indexOf('\n');
+ if (pos === -1) {
+ console.warn("text message without channel");
+ return null;
+ }
+ channel = data.substring(0, pos);
+ }
+
+ return channel;
+}
+
+/* Private Transport class */
+function Transport() {
+ const self = this;
+ self.application = calculate_application();
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ let last_channel = 0;
+ let channel_seed = "";
+
+ if (window.mock)
+ window.mock.last_transport = self;
+
+ let ws;
+ let ignore_health_check = false;
+ let got_message = false;
+
+ /* See if we should communicate via parent */
+ if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
+ ws = new ParentWebSocket(window.parent);
+
+ let check_health_timer;
+
+ if (!ws) {
+ const ws_loc = calculate_url();
+ transport_debug("connecting to " + ws_loc);
+
+ if (ws_loc) {
+ if ("WebSocket" in window) {
+ ws = new window.WebSocket(ws_loc, "cockpit1");
+ } else {
+ console.error("WebSocket not supported, application will not work!");
+ }
+ }
+
+ check_health_timer = window.setInterval(function () {
+ if (self.ready)
+ ws.send("\n{ \"command\": \"ping\" }");
+ if (!got_message) {
+ if (ignore_health_check) {
+ console.log("health check failure ignored");
+ } else {
+ console.log("health check failed");
+ self.close({ problem: "timeout" });
+ }
+ }
+ got_message = false;
+ }, 30000);
+ }
+
+ if (!ws) {
+ ws = { close: function() { } };
+ window.setTimeout(function() {
+ self.close({ problem: "no-cockpit" });
+ }, 50);
+ }
+
+ const control_cbs = { };
+ const message_cbs = { };
+ let waiting_for_init = true;
+ self.ready = false;
+
+ /* Called when ready for channels to interact */
+ function ready_for_channels() {
+ if (!self.ready) {
+ self.ready = true;
+ self.dispatchEvent("ready");
+ }
+ }
+
+ ws.onopen = function() {
+ if (ws) {
+ if (typeof ws.binaryType !== "undefined")
+ ws.binaryType = "arraybuffer";
+ ws.send("\n{ \"command\": \"init\", \"version\": 1 }");
+ }
+ };
+
+ ws.onclose = function() {
+ transport_debug("WebSocket onclose");
+ ws = null;
+ if (reload_after_disconnect) {
+ expect_disconnect = true;
+ window.location.reload(true);
+ }
+ self.close();
+ };
+
+ ws.onmessage = self.dispatch_data = function(arg) {
+ got_message = true;
+
+ /* The first line of a message is the channel */
+ const message = arg.data;
+
+ const channel = parse_channel(message);
+ if (channel === null)
+ return false;
+
+ const payload = message instanceof window.ArrayBuffer
+ ? new window.Uint8Array(message, channel.length + 1)
+ : message.substring(channel.length + 1);
+ let control;
+
+ /* A control message, always string */
+ if (!channel) {
+ transport_debug("recv control:", payload);
+ control = JSON.parse(payload);
+ } else {
+ transport_debug("recv " + channel + ":", payload);
+ }
+
+ const length = incoming_filters ? incoming_filters.length : 0;
+ for (let i = 0; i < length; i++) {
+ if (incoming_filters[i](message, channel, control) === false)
+ return false;
+ }
+
+ if (!channel)
+ process_control(control);
+ else
+ process_message(channel, payload);
+
+ return true;
+ };
+
+ self.close = function close(options) {
+ if (!options)
+ options = { problem: "disconnected" };
+ options.command = "close";
+ window.clearInterval(check_health_timer);
+ const ows = ws;
+ ws = null;
+ if (ows)
+ ows.close();
+ if (expect_disconnect)
+ return;
+ ready_for_channels(); /* ready to fail */
+
+ /* Broadcast to everyone */
+ for (const chan in control_cbs)
+ control_cbs[chan].apply(null, [options]);
+ };
+
+ self.next_channel = function next_channel() {
+ last_channel++;
+ return channel_seed + String(last_channel);
+ };
+
+ function process_init(options) {
+ if (options.problem) {
+ self.close({ problem: options.problem });
+ return;
+ }
+
+ if (options.version !== 1) {
+ console.error("received unsupported version in init message: " + options.version);
+ self.close({ problem: "not-supported" });
+ return;
+ }
+
+ if (options["channel-seed"])
+ channel_seed = String(options["channel-seed"]);
+ if (options.host)
+ default_host = options.host;
+
+ if (public_transport) {
+ public_transport.options = options;
+ public_transport.csrf_token = options["csrf-token"];
+ public_transport.host = default_host;
+ }
+
+ if (init_callback)
+ init_callback(options);
+
+ if (waiting_for_init) {
+ waiting_for_init = false;
+ ready_for_channels();
+ }
+ }
+
+ function process_control(data) {
+ const channel = data.channel;
+
+ /* Init message received */
+ if (data.command == "init") {
+ process_init(data);
+ } else if (waiting_for_init) {
+ waiting_for_init = false;
+ if (data.command != "close" || channel) {
+ console.error("received message before init: ", data.command);
+ data = { problem: "protocol-error" };
+ }
+ self.close(data);
+
+ /* Any pings get sent back as pongs */
+ } else if (data.command == "ping") {
+ data.command = "pong";
+ self.send_control(data);
+ } else if (data.command == "pong") {
+ /* Any pong commands are ignored */
+
+ } else if (data.command == "hint") {
+ if (process_hints)
+ process_hints(data);
+ } else if (channel !== undefined) {
+ const func = control_cbs[channel];
+ if (func)
+ func(data);
+ }
+ }
+
+ function process_message(channel, payload) {
+ const func = message_cbs[channel];
+ if (func)
+ func(payload);
+ }
+
+ /* The channel/control arguments is used by filters, and auto-populated if necessary */
+ self.send_data = function send_data(data, channel, control) {
+ if (!ws) {
+ return false;
+ }
+
+ const length = outgoing_filters ? outgoing_filters.length : 0;
+ for (let i = 0; i < length; i++) {
+ if (channel === undefined)
+ channel = parse_channel(data);
+ if (!channel && control === undefined)
+ control = JSON.parse(data);
+ if (outgoing_filters[i](data, channel, control) === false)
+ return false;
+ }
+
+ ws.send(data);
+ return true;
+ };
+
+ /* The control arguments is used by filters, and auto populated if necessary */
+ self.send_message = function send_message(payload, channel, control) {
+ if (channel)
+ transport_debug("send " + channel, payload);
+ else
+ transport_debug("send control:", payload);
+
+ /* A binary message */
+ if (payload.byteLength || Array.isArray(payload)) {
+ if (payload instanceof window.ArrayBuffer)
+ payload = new window.Uint8Array(payload);
+ const output = join_data([array_from_raw_string(channel), [10], payload], true);
+ return self.send_data(output.buffer, channel, control);
+
+ /* A string message */
+ } else {
+ return self.send_data(channel.toString() + "\n" + payload, channel, control);
+ }
+ };
+
+ self.send_control = function send_control(data) {
+ if (!ws && (data.command == "close" || data.command == "kill"))
+ return; /* don't complain if closed and closing */
+ if (check_health_timer &&
+ data.command == "hint" && data.hint == "ignore_transport_health_check") {
+ /* This is for us, process it directly. */
+ ignore_health_check = data.data;
+ return;
+ }
+ return self.send_message(JSON.stringify(data), "", data);
+ };
+
+ self.register = function register(channel, control_cb, message_cb) {
+ control_cbs[channel] = control_cb;
+ message_cbs[channel] = message_cb;
+ };
+
+ self.unregister = function unregister(channel) {
+ delete control_cbs[channel];
+ delete message_cbs[channel];
+ };
+}
+
+function ensure_transport(callback) {
+ if (!default_transport)
+ default_transport = new Transport();
+ const transport = default_transport;
+ if (transport.ready) {
+ callback(transport);
+ } else {
+ transport.addEventListener("ready", function() {
+ callback(transport);
+ });
+ }
+}
+
+/* Always close the transport explicitly: allows parent windows to track us */
+window.addEventListener("unload", function() {
+ if (default_transport)
+ default_transport.close();
+});
+
+function Channel(options) {
+ const self = this;
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ let transport;
+ let ready = null;
+ let closed = null;
+ let waiting = null;
+ let received_done = false;
+ let sent_done = false;
+ let id = null;
+ const binary = (options.binary === true);
+
+ /*
+ * Queue while waiting for transport, items are tuples:
+ * [is_control ? true : false, payload]
+ */
+ const queue = [];
+
+ /* Handy for callers, but not used by us */
+ self.valid = true;
+ self.options = options;
+ self.binary = binary;
+ self.id = id;
+
+ function on_message(payload) {
+ if (received_done) {
+ console.warn("received message after done");
+ self.close("protocol-error");
+ } else {
+ self.dispatchEvent("message", payload);
+ }
+ }
+
+ function on_close(data) {
+ closed = data;
+ self.valid = false;
+ if (transport && id)
+ transport.unregister(id);
+ if (closed.message && !options.err)
+ console.warn(closed.message);
+ self.dispatchEvent("close", closed);
+ if (waiting)
+ waiting.resolve(closed);
+ }
+
+ function on_ready(data) {
+ ready = data;
+ self.dispatchEvent("ready", ready);
+ }
+
+ function on_control(data) {
+ if (data.command == "close") {
+ on_close(data);
+ return;
+ } else if (data.command == "ready") {
+ on_ready(data);
+ }
+
+ const done = data.command === "done";
+ if (done && received_done) {
+ console.warn("received two done commands on channel");
+ self.close("protocol-error");
+ } else {
+ if (done)
+ received_done = true;
+ self.dispatchEvent("control", data);
+ }
+ }
+
+ function send_payload(payload) {
+ if (!binary) {
+ if (typeof payload !== "string")
+ payload = String(payload);
+ }
+ transport.send_message(payload, id);
+ }
+
+ ensure_transport(function(trans) {
+ transport = trans;
+ if (closed)
+ return;
+
+ id = transport.next_channel();
+ self.id = id;
+
+ /* Register channel handlers */
+ transport.register(id, on_control, on_message);
+
+ /* Now open the channel */
+ const command = { };
+ for (const i in options)
+ command[i] = options[i];
+ command.command = "open";
+ command.channel = id;
+
+ if (!command.host) {
+ if (default_host)
+ command.host = default_host;
+ }
+
+ if (binary)
+ command.binary = "raw";
+ else
+ delete command.binary;
+
+ command["flow-control"] = true;
+ transport.send_control(command);
+
+ /* Now drain the queue */
+ while (queue.length > 0) {
+ const item = queue.shift();
+ if (item[0]) {
+ item[1].channel = id;
+ transport.send_control(item[1]);
+ } else {
+ send_payload(item[1]);
+ }
+ }
+ });
+
+ self.send = function send(message) {
+ if (closed)
+ console.warn("sending message on closed channel");
+ else if (sent_done)
+ console.warn("sending message after done");
+ else if (!transport)
+ queue.push([false, message]);
+ else
+ send_payload(message);
+ };
+
+ self.control = function control(options) {
+ options = options || { };
+ if (!options.command)
+ options.command = "options";
+ if (options.command === "done")
+ sent_done = true;
+ options.channel = id;
+ if (!transport)
+ queue.push([true, options]);
+ else
+ transport.send_control(options);
+ };
+
+ self.wait = function wait(callback) {
+ if (!waiting) {
+ waiting = cockpit.defer();
+ if (closed) {
+ waiting.reject(closed);
+ } else if (ready) {
+ waiting.resolve(ready);
+ } else {
+ self.addEventListener("ready", function(event, data) {
+ waiting.resolve(data);
+ });
+ self.addEventListener("close", function(event, data) {
+ waiting.reject(data);
+ });
+ }
+ }
+ const promise = waiting.promise;
+ if (callback)
+ promise.then(callback, callback);
+ return promise;
+ };
+
+ self.close = function close(options) {
+ if (closed)
+ return;
+
+ if (!options)
+ options = { };
+ else if (typeof options == "string")
+ options = { problem: options };
+ options.command = "close";
+ options.channel = id;
+
+ if (!transport)
+ queue.push([true, options]);
+ else
+ transport.send_control(options);
+ on_close(options);
+ };
+
+ self.buffer = function buffer(callback) {
+ const buffers = [];
+ buffers.callback = callback;
+ buffers.squash = function squash() {
+ return join_data(buffers, binary);
+ };
+
+ function on_message(event, data) {
+ buffers.push(data);
+ if (buffers.callback) {
+ const block = join_data(buffers, binary);
+ if (block.length > 0) {
+ const consumed = buffers.callback.call(self, block);
+ if (typeof consumed !== "number" || consumed === block.length) {
+ buffers.length = 0;
+ } else if (consumed === 0) {
+ buffers.length = 1;
+ buffers[0] = block;
+ } else if (consumed !== 0) {
+ buffers.length = 1;
+ if (block.subarray)
+ buffers[0] = block.subarray(consumed);
+ else if (block.substring)
+ buffers[0] = block.substring(consumed);
+ else
+ buffers[0] = block.slice(consumed);
+ }
+ }
+ }
+ }
+
+ function on_close() {
+ self.removeEventListener("message", on_message);
+ self.removeEventListener("close", on_close);
+ }
+
+ self.addEventListener("message", on_message);
+ self.addEventListener("close", on_close);
+
+ return buffers;
+ };
+
+ self.toString = function toString() {
+ const host = options.host || "localhost";
+ return "[Channel " + (self.valid ? id : "<invalid>") + " -> " + host + "]";
+ };
+}
+
+/* Resolve dots and double dots */
+function resolve_path_dots(parts) {
+ const out = [];
+ const length = parts.length;
+ for (let i = 0; i < length; i++) {
+ const part = parts[i];
+ if (part === "" || part == ".") {
+ continue;
+ } else if (part == "..") {
+ if (out.length === 0)
+ return null;
+ out.pop();
+ } else {
+ out.push(part);
+ }
+ }
+ return out;
+}
+
+function factory() {
+ cockpit.channel = function channel(options) {
+ return new Channel(options);
+ };
+
+ cockpit.event_target = function event_target(obj) {
+ event_mixin(obj, { });
+ return obj;
+ };
+
+ /* obsolete backwards compatible shim */
+ cockpit.extend = Object.assign;
+
+ /* These can be filled in by loading ../manifests.js */
+ cockpit.manifests = { };
+
+ /* ------------------------------------------------------------
+ * Text Encoding
+ */
+
+ function Utf8TextEncoder(constructor) {
+ const self = this;
+ self.encoding = "utf-8";
+
+ self.encode = function encode(string, options) {
+ const data = window.unescape(encodeURIComponent(string));
+ if (constructor === String)
+ return data;
+ return array_from_raw_string(data, constructor);
+ };
+ }
+
+ function Utf8TextDecoder(fatal) {
+ const self = this;
+ let buffer = null;
+ self.encoding = "utf-8";
+
+ self.decode = function decode(data, options) {
+ const stream = options?.stream;
+
+ if (data === null || data === undefined)
+ data = "";
+ if (typeof data !== "string")
+ data = array_to_raw_string(data);
+ if (buffer) {
+ data = buffer + data;
+ buffer = null;
+ }
+
+ /* We have to scan to do non-fatal and streaming */
+ const len = data.length;
+ let beg = 0;
+ let i = 0;
+ let str = "";
+
+ while (i < len) {
+ const p = data.charCodeAt(i);
+ const x = p == 255
+ ? 0
+ : p > 251 && p < 254
+ ? 6
+ : p > 247 && p < 252
+ ? 5
+ : p > 239 && p < 248
+ ? 4
+ : p > 223 && p < 240
+ ? 3
+ : p > 191 && p < 224
+ ? 2
+ : p < 128 ? 1 : 0;
+
+ let ok = (i + x <= len);
+ if (!ok && stream) {
+ buffer = data.substring(i);
+ break;
+ }
+ if (x === 0)
+ ok = false;
+ for (let j = 1; ok && j < x; j++)
+ ok = (data.charCodeAt(i + j) & 0x80) !== 0;
+
+ if (!ok) {
+ if (fatal) {
+ i = len;
+ break;
+ }
+
+ str += decodeURIComponent(window.escape(data.substring(beg, i)));
+ str += "\ufffd";
+ i++;
+ beg = i;
+ } else {
+ i += x;
+ }
+ }
+
+ str += decodeURIComponent(window.escape(data.substring(beg, i)));
+ return str;
+ };
+ }
+
+ cockpit.utf8_encoder = function utf8_encoder(constructor) {
+ return new Utf8TextEncoder(constructor);
+ };
+
+ cockpit.utf8_decoder = function utf8_decoder(fatal) {
+ return new Utf8TextDecoder(!!fatal);
+ };
+
+ cockpit.base64_encode = base64_encode;
+ cockpit.base64_decode = base64_decode;
+
+ cockpit.kill = function kill(host, group) {
+ const options = { };
+ if (host)
+ options.host = host;
+ if (group)
+ options.group = group;
+ cockpit.transport.control("kill", options);
+ };
+
+ /* Not public API ... yet? */
+ cockpit.hint = function hint(name, options) {
+ if (!default_transport)
+ return;
+ if (!options)
+ options = default_host;
+ if (typeof options == "string")
+ options = { host: options };
+ options.hint = name;
+ cockpit.transport.control("hint", options);
+ };
+
+ cockpit.transport = public_transport = {
+ wait: ensure_transport,
+ inject: function inject(message, out) {
+ if (!default_transport)
+ return false;
+ if (out === undefined || out)
+ return default_transport.send_data(message);
+ else
+ return default_transport.dispatch_data({ data: message });
+ },
+ filter: function filter(callback, out) {
+ if (out) {
+ if (!outgoing_filters)
+ outgoing_filters = [];
+ outgoing_filters.push(callback);
+ } else {
+ if (!incoming_filters)
+ incoming_filters = [];
+ incoming_filters.push(callback);
+ }
+ },
+ close: function close(problem) {
+ if (default_transport)
+ default_transport.close(problem ? { problem } : undefined);
+ default_transport = null;
+ this.options = { };
+ },
+ origin: transport_origin,
+ options: { },
+ uri: calculate_url,
+ control: function(command, options) {
+ options = { ...options, command };
+ ensure_transport(function(transport) {
+ transport.send_control(options);
+ });
+ },
+ application: function () {
+ if (!default_transport || window.mock)
+ return calculate_application();
+ return default_transport.application;
+ },
+ };
+
+ /* ------------------------------------------------------------------------------------
+ * An ordered queue of functions that should be called later.
+ */
+
+ let later_queue = [];
+ let later_timeout = null;
+
+ function later_drain() {
+ const queue = later_queue;
+ later_timeout = null;
+ later_queue = [];
+ for (;;) {
+ const func = queue.shift();
+ if (!func)
+ break;
+ func();
+ }
+ }
+
+ function later_invoke(func) {
+ if (func)
+ later_queue.push(func);
+ if (later_timeout === null)
+ later_timeout = window.setTimeout(later_drain, 0);
+ }
+
+ /* ------------------------------------------------------------------------------------
+ * Promises.
+ * Based on Q and angular promises, with some jQuery compatibility. See the angular
+ * license in COPYING.node for license lineage. There are some key differences with
+ * both Q and jQuery.
+ *
+ * * Exceptions thrown in handlers are not treated as rejections or failures.
+ * Exceptions remain actual exceptions.
+ * * Unlike jQuery callbacks added to an already completed promise don't execute
+ * immediately. Wait until control is returned to the browser.
+ */
+
+ function promise_then(state, fulfilled, rejected, updated) {
+ if (fulfilled === undefined && rejected === undefined && updated === undefined)
+ return null;
+ const result = new Deferred();
+ state.pending = state.pending || [];
+ state.pending.push([result, fulfilled, rejected, updated]);
+ if (state.status > 0)
+ schedule_process_queue(state);
+ return result.promise;
+ }
+
+ function create_promise(state) {
+ /* Like jQuery the promise object is callable */
+ const self = function Promise(target) {
+ if (target) {
+ Object.assign(target, self);
+ return target;
+ }
+ return self;
+ };
+
+ state.status = 0;
+
+ self.then = function then(fulfilled, rejected, updated) {
+ return promise_then(state, fulfilled, rejected, updated) || self;
+ };
+
+ self.catch = function catch_(callback) {
+ return promise_then(state, null, callback) || self;
+ };
+
+ self.finally = function finally_(callback, updated) {
+ return promise_then(state, function() {
+ return handle_callback(arguments, true, callback);
+ }, function() {
+ return handle_callback(arguments, false, callback);
+ }, updated) || self;
+ };
+
+ /* Basic jQuery Promise compatibility */
+ self.done = function done(fulfilled) {
+ promise_then(state, fulfilled);
+ return self;
+ };
+
+ self.fail = function fail(rejected) {
+ promise_then(state, null, rejected);
+ return self;
+ };
+
+ self.always = function always(callback) {
+ promise_then(state, callback, callback);
+ return self;
+ };
+
+ self.progress = function progress(updated) {
+ promise_then(state, null, null, updated);
+ return self;
+ };
+
+ self.state = function state_() {
+ if (state.status == 1)
+ return "resolved";
+ if (state.status == 2)
+ return "rejected";
+ return "pending";
+ };
+
+ /* Promises are recursive like jQuery */
+ self.promise = self;
+
+ return self;
+ }
+
+ function process_queue(state) {
+ const pending = state.pending;
+ state.process_scheduled = false;
+ state.pending = undefined;
+ for (let i = 0, ii = pending.length; i < ii; ++i) {
+ state.pur = true;
+ const deferred = pending[i][0];
+ const fn = pending[i][state.status];
+ if (is_function(fn)) {
+ deferred.resolve(fn.apply(state.promise, state.values));
+ } else if (state.status === 1) {
+ deferred.resolve.apply(deferred.resolve, state.values);
+ } else {
+ deferred.reject.apply(deferred.reject, state.values);
+ }
+ }
+ }
+
+ function schedule_process_queue(state) {
+ if (state.process_scheduled || !state.pending)
+ return;
+ state.process_scheduled = true;
+ later_invoke(function() { process_queue(state) });
+ }
+
+ function deferred_resolve(state, values) {
+ let then;
+ let done = false;
+ if (is_object(values[0]) || is_function(values[0]))
+ then = values[0]?.then;
+ if (is_function(then)) {
+ state.status = -1;
+ then.call(values[0], function(/* ... */) {
+ if (done)
+ return;
+ done = true;
+ deferred_resolve(state, arguments);
+ }, function(/* ... */) {
+ if (done)
+ return;
+ done = true;
+ deferred_reject(state, arguments);
+ }, function(/* ... */) {
+ deferred_notify(state, arguments);
+ });
+ } else {
+ state.values = values;
+ state.status = 1;
+ schedule_process_queue(state);
+ }
+ }
+
+ function deferred_reject(state, values) {
+ state.values = values;
+ state.status = 2;
+ schedule_process_queue(state);
+ }
+
+ function deferred_notify(state, values) {
+ const callbacks = state.pending;
+ if ((state.status <= 0) && callbacks?.length) {
+ later_invoke(function() {
+ for (let i = 0, ii = callbacks.length; i < ii; i++) {
+ const result = callbacks[i][0];
+ const callback = callbacks[i][3];
+ if (is_function(callback))
+ result.notify(callback.apply(state.promise, values));
+ else
+ result.notify.apply(result, values);
+ }
+ });
+ }
+ }
+
+ function Deferred() {
+ const self = this;
+ const state = { };
+ self.promise = state.promise = create_promise(state);
+
+ self.resolve = function resolve(/* ... */) {
+ if (arguments[0] === state.promise)
+ throw new Error("Expected promise to be resolved with other value than itself");
+ if (!state.status)
+ deferred_resolve(state, arguments);
+ return self;
+ };
+
+ self.reject = function reject(/* ... */) {
+ if (state.status)
+ return;
+ deferred_reject(state, arguments);
+ return self;
+ };
+
+ self.notify = function notify(/* ... */) {
+ deferred_notify(state, arguments);
+ return self;
+ };
+ }
+
+ function prep_promise(values, resolved) {
+ const result = cockpit.defer();
+ if (resolved)
+ result.resolve.apply(result, values);
+ else
+ result.reject.apply(result, values);
+ return result.promise;
+ }
+
+ function handle_callback(values, is_resolved, callback) {
+ let callback_output = null;
+ if (is_function(callback))
+ callback_output = callback();
+ if (callback_output && is_function(callback_output.then)) {
+ return callback_output.then(function() {
+ return prep_promise(values, is_resolved);
+ }, function() {
+ return prep_promise(arguments, false);
+ });
+ } else {
+ return prep_promise(values, is_resolved);
+ }
+ }
+
+ cockpit.when = function when(value, fulfilled, rejected, updated) {
+ const result = cockpit.defer();
+ result.resolve(value);
+ return result.promise.then(fulfilled, rejected, updated);
+ };
+
+ cockpit.resolve = function resolve(result) {
+ return cockpit.defer().resolve(result).promise;
+ };
+
+ cockpit.reject = function reject(ex) {
+ return cockpit.defer().reject(ex).promise;
+ };
+
+ cockpit.defer = function() {
+ return new Deferred();
+ };
+
+ /* ---------------------------------------------------------------------
+ * Utilities
+ */
+
+ const fmt_re = /\$\{([^}]+)\}|\$([a-zA-Z0-9_]+)/g;
+ cockpit.format = function format(fmt, args) {
+ if (arguments.length != 2 || !is_object(args) || args === null)
+ args = Array.prototype.slice.call(arguments, 1);
+
+ function replace(m, x, y) {
+ const value = args[x || y];
+
+ /* Special-case 0 (also catches 0.0). All other falsy values return
+ * the empty string.
+ */
+ if (value === 0)
+ return '0';
+
+ return value || '';
+ }
+
+ return fmt.replace(fmt_re, replace);
+ };
+
+ cockpit.format_number = function format_number(number, precision) {
+ /* We show given number of digits of precision (default 3), but avoid scientific notation.
+ * We also show integers without digits after the comma.
+ *
+ * We want to localise the decimal separator, but we never want to
+ * show thousands separators (to avoid ambiguity). For this
+ * reason, for integers and large enough numbers, we use
+ * non-localised conversions (and in both cases, show no
+ * fractional part).
+ */
+ if (precision === undefined)
+ precision = 3;
+ const lang = cockpit.language === undefined ? undefined : cockpit.language.replace('_', '-');
+ const smallestValue = 10 ** (-precision);
+
+ if (!number && number !== 0)
+ return "";
+ else if (number % 1 === 0)
+ return number.toString();
+ else if (number > 0 && number <= smallestValue)
+ return smallestValue.toLocaleString(lang);
+ else if (number < 0 && number >= -smallestValue)
+ return (-smallestValue).toLocaleString(lang);
+ else if (number > 999 || number < -999)
+ return number.toFixed(0);
+ else
+ return number.toLocaleString(lang, {
+ maximumSignificantDigits: precision,
+ minimumSignificantDigits: precision,
+ });
+ };
+
+ function format_units(number, suffixes, factor, options) {
+ // backwards compat: "options" argument position used to be a boolean flag "separate"
+ if (!is_object(options))
+ options = { separate: options };
+
+ let suffix = null;
+
+ /* Find that factor string */
+ if (!number && number !== 0) {
+ suffix = null;
+ } else if (typeof (factor) === "string") {
+ /* Prefer larger factors */
+ const keys = [];
+ for (const key in suffixes)
+ keys.push(key);
+ keys.sort().reverse();
+ for (let y = 0; y < keys.length; y++) {
+ for (let x = 0; x < suffixes[keys[y]].length; x++) {
+ if (factor == suffixes[keys[y]][x]) {
+ number = number / Math.pow(keys[y], x);
+ suffix = factor;
+ break;
+ }
+ }
+ if (suffix)
+ break;
+ }
+
+ /* @factor is a number */
+ } else if (factor in suffixes) {
+ let divisor = 1;
+ for (let i = 0; i < suffixes[factor].length; i++) {
+ const quotient = number / divisor;
+ if (quotient < factor) {
+ number = quotient;
+ suffix = suffixes[factor][i];
+ break;
+ }
+ divisor *= factor;
+ }
+ }
+
+ const string_representation = cockpit.format_number(number, options.precision);
+ let ret;
+
+ if (string_representation && suffix)
+ ret = [string_representation, suffix];
+ else
+ ret = [string_representation];
+
+ if (!options.separate)
+ ret = ret.join(" ");
+
+ return ret;
+ }
+
+ const byte_suffixes = {
+ 1000: [null, "KB", "MB", "GB", "TB", "PB", "EB", "ZB"],
+ 1024: [null, "KiB", "MiB", "GiB", "TiB", "PiB", "EiB", "ZiB"]
+ };
+
+ cockpit.format_bytes = function format_bytes(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, byte_suffixes, factor, options);
+ };
+
+ cockpit.get_byte_units = function get_byte_units(guide_value, factor) {
+ if (factor === undefined || !(factor in byte_suffixes))
+ factor = 1000;
+
+ function unit(index) {
+ return {
+ name: byte_suffixes[factor][index],
+ factor: Math.pow(factor, index)
+ };
+ }
+
+ const units = [unit(2), unit(3), unit(4)];
+
+ // The default unit is the largest one that gives us at least
+ // two decimal digits in front of the comma.
+
+ for (let i = units.length - 1; i >= 0; i--) {
+ if (i === 0 || (guide_value / units[i].factor) >= 10) {
+ units[i].selected = true;
+ break;
+ }
+ }
+
+ return units;
+ };
+
+ const byte_sec_suffixes = {
+ 1000: ["B/s", "kB/s", "MB/s", "GB/s", "TB/s", "PB/s", "EB/s", "ZB/s"],
+ 1024: ["B/s", "KiB/s", "MiB/s", "GiB/s", "TiB/s", "PiB/s", "EiB/s", "ZiB/s"]
+ };
+
+ cockpit.format_bytes_per_sec = function format_bytes_per_sec(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, byte_sec_suffixes, factor, options);
+ };
+
+ const bit_suffixes = {
+ 1000: ["bps", "Kbps", "Mbps", "Gbps", "Tbps", "Pbps", "Ebps", "Zbps"]
+ };
+
+ cockpit.format_bits_per_sec = function format_bits_per_sec(number, factor, options) {
+ if (factor === undefined)
+ factor = 1000;
+ return format_units(number, bit_suffixes, factor, options);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Storage Helper.
+ *
+ * Use application to prefix data stored in browser storage
+ * with helpers for compatibility.
+ */
+ function StorageHelper(storageName) {
+ const self = this;
+ let storage;
+
+ try {
+ storage = window[storageName];
+ } catch (e) { }
+
+ self.prefixedKey = function (key) {
+ return cockpit.transport.application() + ":" + key;
+ };
+
+ self.getItem = function (key, both) {
+ let value = storage.getItem(self.prefixedKey(key));
+ if (!value && both)
+ value = storage.getItem(key);
+ return value;
+ };
+
+ self.setItem = function (key, value, both) {
+ storage.setItem(self.prefixedKey(key), value);
+ if (both)
+ storage.setItem(key, value);
+ };
+
+ self.removeItem = function(key, both) {
+ storage.removeItem(self.prefixedKey(key));
+ if (both)
+ storage.removeItem(key);
+ };
+
+ /* Instead of clearing, purge anything that isn't prefixed with an application
+ * and anything prefixed with our application.
+ */
+ self.clear = function(full) {
+ let i = 0;
+ while (i < storage.length) {
+ const k = storage.key(i);
+ if (full && k.indexOf("cockpit") !== 0)
+ storage.removeItem(k);
+ else if (k.indexOf(cockpit.transport.application()) === 0)
+ storage.removeItem(k);
+ else
+ i++;
+ }
+ };
+ }
+
+ cockpit.localStorage = new StorageHelper("localStorage");
+ cockpit.sessionStorage = new StorageHelper("sessionStorage");
+
+ /* ---------------------------------------------------------------------
+ * Shared data cache.
+ *
+ * We cannot use sessionStorage when keeping lots of data in memory and
+ * sharing it between frames. It has a rather paltry limit on the amount
+ * of data it can hold ... so we use window properties instead.
+ */
+
+ function lookup_storage(win) {
+ let storage;
+ if (win.parent && win.parent !== win)
+ storage = lookup_storage(win.parent);
+ if (!storage) {
+ try {
+ storage = win["cv1-storage"];
+ if (!storage)
+ win["cv1-storage"] = storage = { };
+ } catch (ex) { }
+ }
+ return storage;
+ }
+
+ function StorageCache(org_key, provider, consumer) {
+ const self = this;
+ const key = cockpit.transport.application() + ":" + org_key;
+
+ /* For triggering events and ownership */
+ const trigger = window.sessionStorage;
+ let last;
+
+ const storage = lookup_storage(window);
+
+ let claimed = false;
+ let source;
+
+ function callback() {
+ /* Only run the callback if we have a result */
+ if (storage[key] !== undefined) {
+ const value = storage[key];
+ window.setTimeout(function() {
+ if (consumer(value, org_key) === false)
+ self.close();
+ });
+ }
+ }
+
+ function result(value) {
+ if (source && !claimed)
+ claimed = true;
+ if (!claimed)
+ return;
+
+ // use a random number to avoid races by separate instances
+ const version = Math.floor(Math.random() * 10000000) + 1;
+
+ /* Event for the local window */
+ const ev = document.createEvent("StorageEvent");
+ ev.initStorageEvent("storage", false, false, key, null,
+ version, window.location, trigger);
+
+ storage[key] = value;
+ trigger.setItem(key, version);
+ ev.self = self;
+ window.dispatchEvent(ev);
+ }
+
+ self.claim = function claim() {
+ if (source)
+ return;
+
+ /* In case we're unclaimed during the callback */
+ const claiming = { close: function() { } };
+ source = claiming;
+
+ const changed = provider(result, org_key);
+ if (source === claiming)
+ source = changed;
+ else
+ changed.close();
+ };
+
+ function unclaim() {
+ if (source?.close)
+ source.close();
+ source = null;
+
+ if (!claimed)
+ return;
+
+ claimed = false;
+
+ let current_value = trigger.getItem(key);
+ if (current_value)
+ current_value = parseInt(current_value, 10);
+ else
+ current_value = null;
+
+ if (last && last === current_value) {
+ const ev = document.createEvent("StorageEvent");
+ const version = trigger[key];
+ ev.initStorageEvent("storage", false, false, key, version,
+ null, window.location, trigger);
+ delete storage[key];
+ trigger.removeItem(key);
+ ev.self = self;
+ window.dispatchEvent(ev);
+ }
+ }
+
+ function changed(event) {
+ if (event.key !== key)
+ return;
+
+ /* check where the event came from
+ - it came from someone else:
+ if it notifies their unclaim (new value null) and we haven't already claimed, do so
+ - it came from ourselves:
+ if the new value doesn't match the actual value in the cache, and
+ we tried to claim (from null to a number), cancel our claim
+ */
+ if (event.self !== self) {
+ if (!event.newValue && !claimed) {
+ self.claim();
+ return;
+ }
+ } else if (claimed && !event.oldValue && (event.newValue !== trigger.getItem(key))) {
+ unclaim();
+ }
+
+ let new_value = null;
+ if (event.newValue)
+ new_value = parseInt(event.newValue, 10);
+ if (last !== new_value) {
+ last = new_value;
+ callback();
+ }
+ }
+
+ self.close = function() {
+ window.removeEventListener("storage", changed, true);
+ unclaim();
+ };
+
+ window.addEventListener("storage", changed, true);
+
+ /* Always clear this data on unload */
+ window.addEventListener("beforeunload", function() {
+ self.close();
+ });
+ window.addEventListener("unload", function() {
+ self.close();
+ });
+
+ if (trigger.getItem(key))
+ callback();
+ else
+ self.claim();
+ }
+
+ cockpit.cache = function cache(key, provider, consumer) {
+ return new StorageCache(key, provider, consumer);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Metrics
+ *
+ * Implements the cockpit.series and cockpit.grid. Part of the metrics
+ * implementations that do not require jquery.
+ */
+
+ function SeriesSink(interval, identifier, fetch_callback) {
+ const self = this;
+
+ self.interval = interval;
+ self.limit = identifier ? 64 * 1024 : 1024;
+
+ /*
+ * The cache sits on a window, either our own or a parent
+ * window whichever we can access properly.
+ *
+ * Entries in the index are:
+ *
+ * { beg: N, items: [], mapping: { }, next: item }
+ */
+ const index = setup_index(identifier);
+
+ /*
+ * A linked list through the index, that we use for expiry
+ * of the cache.
+ */
+ let count = 0;
+ let head = null;
+ let tail = null;
+
+ function setup_index(id) {
+ if (!id)
+ return [];
+
+ /* Try and find a good place to cache data */
+ const storage = lookup_storage(window);
+
+ let index = storage[id];
+ if (!index)
+ storage[id] = index = [];
+ return index;
+ }
+
+ function search(idx, beg) {
+ let low = 0;
+ let high = idx.length - 1;
+
+ while (low <= high) {
+ const mid = (low + high) / 2 | 0;
+ const val = idx[mid].beg;
+ if (val < beg)
+ low = mid + 1;
+ else if (val > beg)
+ high = mid - 1;
+ else
+ return mid; /* key found */
+ }
+ return low;
+ }
+
+ function fetch(beg, end, for_walking) {
+ if (fetch_callback) {
+ if (!for_walking) {
+ /* Stash some fake data synchronously so that we don't ask
+ * again for the same range while they are still fetching
+ * it asynchronously.
+ */
+ stash(beg, new Array(end - beg), { });
+ }
+ fetch_callback(beg, end, for_walking);
+ }
+ }
+
+ self.load = function load(beg, end, for_walking) {
+ if (end <= beg)
+ return;
+
+ const at = search(index, beg);
+
+ const len = index.length;
+ let last = beg;
+
+ /* We do this in two phases: First, we walk the index to
+ * process what we already have and at the same time make
+ * notes about what we need to fetch. Then we go over the
+ * notes and actually fetch what we need. That way, the
+ * fetch callbacks in the second phase can modify the
+ * index data structure without disturbing the walk in the
+ * first phase.
+ */
+
+ const fetches = [];
+
+ /* Data relevant to this range can be at the found index, or earlier */
+ for (let i = at > 0 ? at - 1 : at; i < len; i++) {
+ const entry = index[i];
+ const en = entry.items.length;
+ if (!en)
+ continue;
+
+ const eb = entry.beg;
+ const b = Math.max(eb, beg);
+ const e = Math.min(eb + en, end);
+
+ if (b < e) {
+ if (b > last)
+ fetches.push([last, b]);
+ process(b, entry.items.slice(b - eb, e - eb), entry.mapping);
+ last = e;
+ } else if (i >= at) {
+ break; /* no further intersections */
+ }
+ }
+
+ for (let i = 0; i < fetches.length; i++)
+ fetch(fetches[i][0], fetches[i][1], for_walking);
+
+ if (last != end)
+ fetch(last, end, for_walking);
+ };
+
+ function stash(beg, items, mapping) {
+ if (!items.length)
+ return;
+
+ let at = search(index, beg);
+
+ const end = beg + items.length;
+
+ const len = index.length;
+ let i;
+ for (i = at > 0 ? at - 1 : at; i < len; i++) {
+ const entry = index[i];
+ const en = entry.items.length;
+ if (!en)
+ continue;
+
+ const eb = entry.beg;
+ const b = Math.max(eb, beg);
+ const e = Math.min(eb + en, end);
+
+ /*
+ * We truncate blocks that intersect with this one
+ *
+ * We could adjust them, but in general the loaders are
+ * intelligent enough to only load the required data, so
+ * not doing this optimization yet.
+ */
+
+ if (b < e) {
+ const num = e - b;
+ entry.items.splice(b - eb, num);
+ count -= num;
+ if (b - eb === 0)
+ entry.beg += (e - eb);
+ } else if (i >= at) {
+ break; /* no further intersections */
+ }
+ }
+
+ /* Insert our item into the array */
+ const entry = { beg, items, mapping };
+ if (!head)
+ head = entry;
+ if (tail)
+ tail.next = entry;
+ tail = entry;
+ count += items.length;
+ index.splice(at, 0, entry);
+
+ /* Remove any items with zero length around insertion point */
+ for (at--; at <= i; at++) {
+ const entry = index[at];
+ if (entry && !entry.items.length) {
+ index.splice(at, 1);
+ at--;
+ }
+ }
+
+ /* If our index has gotten too big, expire entries */
+ while (head && count > self.limit) {
+ count -= head.items.length;
+ head.items = [];
+ head.mapping = null;
+ head = head.next || null;
+ }
+
+ /* Remove any entries with zero length at beginning */
+ const newlen = index.length;
+ for (i = 0; i < newlen; i++) {
+ if (index[i].items.length > 0)
+ break;
+ }
+ index.splice(0, i);
+ }
+
+ /*
+ * Used to populate grids, the keys are grid ids and
+ * the values are objects: { grid, rows, notify }
+ *
+ * The rows field is an object indexed by paths
+ * container aliases, and the values are: [ row, path ]
+ */
+ const registered = { };
+
+ /* An undocumented function called by DataGrid */
+ self._register = function _register(grid, id) {
+ if (grid.interval != interval)
+ throw Error("mismatched metric interval between grid and sink");
+ let gdata = registered[id];
+ if (!gdata) {
+ gdata = registered[id] = { grid, links: [] };
+ gdata.links.remove = function remove() {
+ delete registered[id];
+ };
+ }
+ return gdata.links;
+ };
+
+ function process(beg, items, mapping) {
+ const end = beg + items.length;
+
+ for (const id in registered) {
+ const gdata = registered[id];
+ const grid = gdata.grid;
+
+ const b = Math.max(beg, grid.beg);
+ const e = Math.min(end, grid.end);
+
+ /* Does this grid overlap the bounds of item? */
+ if (b < e) {
+ /* Where in the items to take from */
+ const f = b - beg;
+
+ /* Where and how many to place */
+ const t = b - grid.beg;
+
+ /* How many to process */
+ const n = e - b;
+
+ for (let i = 0; i < n; i++) {
+ const klen = gdata.links.length;
+ for (let k = 0; k < klen; k++) {
+ const path = gdata.links[k][0];
+ const row = gdata.links[k][1];
+
+ /* Calculate the data field to fill in */
+ let data = items[f + i];
+ let map = mapping;
+ const jlen = path.length;
+ for (let j = 0; data !== undefined && j < jlen; j++) {
+ if (!data) {
+ data = undefined;
+ } else if (map !== undefined && map !== null) {
+ map = map[path[j]];
+ if (map)
+ data = data[map[""]];
+ else
+ data = data[path[j]];
+ } else {
+ data = data[path[j]];
+ }
+ }
+
+ row[t + i] = data;
+ }
+ }
+
+ /* Notify the grid, so it can call any functions */
+ grid.notify(t, n);
+ }
+ }
+ }
+
+ self.input = function input(beg, items, mapping) {
+ process(beg, items, mapping);
+ stash(beg, items, mapping);
+ };
+
+ self.close = function () {
+ for (const id in registered) {
+ const grid = registered[id];
+ if (grid?.grid)
+ grid.grid.remove_sink(self);
+ }
+ };
+ }
+
+ cockpit.series = function series(interval, cache, fetch) {
+ return new SeriesSink(interval, cache, fetch);
+ };
+
+ let unique = 1;
+
+ function SeriesGrid(interval, beg, end) {
+ const self = this;
+
+ /* We can trigger events */
+ event_mixin(self, { });
+
+ const rows = [];
+
+ self.interval = interval;
+ self.beg = 0;
+ self.end = 0;
+
+ /*
+ * Used to populate table data, the values are:
+ * [ callback, row ]
+ */
+ const callbacks = [];
+
+ const sinks = [];
+
+ let suppress = 0;
+
+ const id = "g1-" + unique;
+ unique += 1;
+
+ /* Used while walking */
+ let walking = null;
+ let offset = null;
+
+ self.notify = function notify(x, n) {
+ if (suppress)
+ return;
+ if (x + n > self.end - self.beg)
+ n = (self.end - self.beg) - x;
+ if (n <= 0)
+ return;
+ const jlen = callbacks.length;
+ for (let j = 0; j < jlen; j++) {
+ const callback = callbacks[j][0];
+ const row = callbacks[j][1];
+ callback.call(self, row, x, n);
+ }
+
+ self.dispatchEvent("notify", x, n);
+ };
+
+ self.add = function add(/* sink, path */) {
+ const row = [];
+ rows.push(row);
+
+ /* Called as add(sink, path) */
+ if (is_object(arguments[0])) {
+ const sink = arguments[0].series || arguments[0];
+
+ /* The path argument can be an array, or a dot separated string */
+ let path = arguments[1];
+ if (!path)
+ path = [];
+ else if (typeof (path) === "string")
+ path = path.split(".");
+
+ const links = sink._register(self, id);
+ if (!links.length)
+ sinks.push({ sink, links });
+ links.push([path, row]);
+
+ /* Called as add(callback) */
+ } else if (is_function(arguments[0])) {
+ const cb = [arguments[0], row];
+ if (arguments[1] === true)
+ callbacks.unshift(cb);
+ else
+ callbacks.push(cb);
+
+ /* Not called as add() */
+ } else if (arguments.length !== 0) {
+ throw Error("invalid args to grid.add()");
+ }
+
+ return row;
+ };
+
+ self.remove = function remove(row) {
+ /* Remove from the sinks */
+ let ilen = sinks.length;
+ for (let i = 0; i < ilen; i++) {
+ const jlen = sinks[i].links.length;
+ for (let j = 0; j < jlen; j++) {
+ if (sinks[i].links[j][1] === row) {
+ sinks[i].links.splice(j, 1);
+ break;
+ }
+ }
+ }
+
+ /* Remove from our list of rows */
+ ilen = rows.length;
+ for (let i = 0; i < ilen; i++) {
+ if (rows[i] === row) {
+ rows.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ self.remove_sink = function remove_sink(sink) {
+ const len = sinks.length;
+ for (let i = 0; i < len; i++) {
+ if (sinks[i].sink === sink) {
+ sinks[i].links.remove();
+ sinks.splice(i, 1);
+ break;
+ }
+ }
+ };
+
+ self.sync = function sync(for_walking) {
+ /* Suppress notifications */
+ suppress++;
+
+ /* Ask all sinks to load data */
+ const len = sinks.length;
+ for (let i = 0; i < len; i++) {
+ const sink = sinks[i].sink;
+ sink.load(self.beg, self.end, for_walking);
+ }
+
+ suppress--;
+
+ /* Notify for all rows */
+ self.notify(0, self.end - self.beg);
+ };
+
+ function move_internal(beg, end, for_walking) {
+ if (end === undefined)
+ end = beg + (self.end - self.beg);
+
+ if (end < beg)
+ beg = end;
+
+ self.beg = beg;
+ self.end = end;
+
+ if (!rows.length)
+ return;
+
+ rows.forEach(function(row) {
+ row.length = 0;
+ });
+
+ self.sync(for_walking);
+ }
+
+ function stop_walking() {
+ window.clearInterval(walking);
+ walking = null;
+ offset = null;
+ }
+
+ self.move = function move(beg, end) {
+ stop_walking();
+ /* Some code paths use now twice.
+ * They should use the same value.
+ */
+ let now = null;
+
+ /* Treat negative numbers relative to now */
+ if (beg === undefined) {
+ beg = 0;
+ } else if (is_negative(beg)) {
+ now = Date.now();
+ beg = Math.floor(now / self.interval) + beg;
+ }
+ if (end !== undefined && is_negative(end)) {
+ if (now === null)
+ now = Date.now();
+ end = Math.floor(now / self.interval) + end;
+ }
+
+ move_internal(beg, end, false);
+ };
+
+ self.walk = function walk() {
+ /* Don't overflow 32 signed bits with the interval since
+ * many browsers will mishandle it. This means that plots
+ * that would make about one step every month don't walk
+ * at all, but I guess that is ok.
+ *
+ * For example,
+ * https://developer.mozilla.org/en-US/docs/Web/API/setTimeout
+ * says:
+ *
+ * Browsers including Internet Explorer, Chrome,
+ * Safari, and Firefox store the delay as a 32-bit
+ * signed Integer internally. This causes an Integer
+ * overflow when using delays larger than 2147483647,
+ * resulting in the timeout being executed immediately.
+ */
+
+ const start = Date.now();
+ if (self.interval > 2000000000)
+ return;
+
+ stop_walking();
+ offset = start - self.beg * self.interval;
+ walking = window.setInterval(function() {
+ const now = Date.now();
+ move_internal(Math.floor((now - offset) / self.interval), undefined, true);
+ }, self.interval);
+ };
+
+ self.close = function close() {
+ stop_walking();
+ while (sinks.length)
+ (sinks.pop()).links.remove();
+ };
+
+ self.move(beg, end);
+ }
+
+ cockpit.grid = function grid(interval, beg, end) {
+ return new SeriesGrid(interval, beg, end);
+ };
+
+ /* --------------------------------------------------------------------
+ * Basic utilities.
+ */
+
+ function BasicError(problem, message) {
+ this.problem = problem;
+ this.message = message || cockpit.message(problem);
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ cockpit.logout = function logout(reload, reason) {
+ /* fully clear session storage */
+ cockpit.sessionStorage.clear(true);
+
+ /* Only clean application data from localStorage,
+ * except for login-data. Clear that completely */
+ cockpit.localStorage.removeItem('login-data', true);
+ cockpit.localStorage.clear(false);
+
+ if (reload !== false)
+ reload_after_disconnect = true;
+ ensure_transport(function(transport) {
+ if (!transport.send_control({ command: "logout", disconnect: true }))
+ window.location.reload(reload_after_disconnect);
+ });
+ window.sessionStorage.setItem("logout-intent", "explicit");
+ if (reason)
+ window.sessionStorage.setItem("logout-reason", reason);
+ };
+
+ /* Not public API ... yet? */
+ cockpit.drop_privileges = function drop_privileges() {
+ ensure_transport(function(transport) {
+ transport.send_control({ command: "logout", disconnect: false });
+ });
+ };
+
+ /* ---------------------------------------------------------------------
+ * User and system information
+ */
+
+ cockpit.info = { };
+ event_mixin(cockpit.info, { });
+
+ init_callback = function(options) {
+ if (options.system)
+ Object.assign(cockpit.info, options.system);
+ if (options.system)
+ cockpit.info.dispatchEvent("changed");
+ };
+
+ let the_user = null;
+ cockpit.user = function () {
+ const dfd = cockpit.defer();
+ if (!the_user) {
+ const dbus = cockpit.dbus(null, { bus: "internal" });
+ dbus.call("/user", "org.freedesktop.DBus.Properties", "GetAll",
+ ["cockpit.User"], { type: "s" })
+ .then(([user]) => {
+ the_user = {
+ id: user.Id.v,
+ name: user.Name.v,
+ full_name: user.Full.v,
+ groups: user.Groups.v,
+ home: user.Home.v,
+ shell: user.Shell.v
+ };
+ dfd.resolve(the_user);
+ })
+ .catch(ex => dfd.reject(ex))
+ .finally(() => dbus.close());
+ } else {
+ dfd.resolve(the_user);
+ }
+
+ return dfd.promise;
+ };
+
+ /* ------------------------------------------------------------------------
+ * Override for broken browser behavior
+ */
+
+ document.addEventListener("click", function(ev) {
+ if (ev.target.classList && in_array(ev.target.classList, 'disabled'))
+ ev.stopPropagation();
+ }, true);
+
+ /* ------------------------------------------------------------------------
+ * Cockpit location
+ */
+
+ /* HACK: Mozilla will unescape 'window.location.hash' before returning
+ * it, which is broken.
+ *
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=135309
+ */
+
+ let last_loc = null;
+
+ function get_window_location_hash() {
+ return (window.location.href.split('#')[1] || '');
+ }
+
+ function Location() {
+ const self = this;
+ const application = cockpit.transport.application();
+ self.url_root = url_root || "";
+
+ if (window.mock?.url_root)
+ self.url_root = window.mock.url_root;
+
+ if (application.indexOf("cockpit+=") === 0) {
+ if (self.url_root)
+ self.url_root += '/';
+ self.url_root = self.url_root + application.replace("cockpit+", '');
+ }
+
+ const href = get_window_location_hash();
+ const options = { };
+ self.path = decode(href, options);
+
+ function decode_path(input) {
+ const parts = input.split('/').map(decodeURIComponent);
+ let result, i;
+ let pre_parts = [];
+
+ if (self.url_root)
+ pre_parts = self.url_root.split('/').map(decodeURIComponent);
+
+ if (input && input[0] !== "/") {
+ result = [].concat(self.path);
+ result.pop();
+ result = result.concat(parts);
+ } else {
+ result = parts;
+ }
+
+ result = resolve_path_dots(result);
+ for (i = 0; i < pre_parts.length; i++) {
+ if (pre_parts[i] !== result[i])
+ break;
+ }
+ if (i == pre_parts.length)
+ result.splice(0, pre_parts.length);
+
+ return result;
+ }
+
+ function encode(path, options, with_root) {
+ if (typeof path == "string")
+ path = decode_path(path);
+
+ let href = "/" + path.map(encodeURIComponent).join("/");
+ if (with_root && self.url_root && href.indexOf("/" + self.url_root + "/") !== 0)
+ href = "/" + self.url_root + href;
+
+ /* Undo unnecessary encoding of these */
+ href = href.replaceAll("%40", "@");
+ href = href.replaceAll("%3D", "=");
+ href = href.replaceAll("%2B", "+");
+ href = href.replaceAll("%23", "#");
+
+ let opt;
+ const query = [];
+ function push_option(v) {
+ query.push(encodeURIComponent(opt) + "=" + encodeURIComponent(v));
+ }
+
+ if (options) {
+ for (opt in options) {
+ let value = options[opt];
+ if (!Array.isArray(value))
+ value = [value];
+ value.forEach(push_option);
+ }
+ if (query.length > 0)
+ href += "?" + query.join("&");
+ }
+ return href;
+ }
+
+ function decode(href, options) {
+ if (href[0] == '#')
+ href = href.substr(1);
+
+ const pos = href.indexOf('?');
+ const first = (pos === -1) ? href : href.substr(0, pos);
+ const path = decode_path(first);
+ if (pos !== -1 && options) {
+ href.substring(pos + 1).split("&")
+ .forEach(function(opt) {
+ const parts = opt.split('=');
+ const name = decodeURIComponent(parts[0]);
+ const value = decodeURIComponent(parts[1]);
+ if (options[name]) {
+ let last = options[name];
+ if (!Array.isArray(value))
+ last = options[name] = [last];
+ last.push(value);
+ } else {
+ options[name] = value;
+ }
+ });
+ }
+
+ return path;
+ }
+
+ function href_for_go_or_replace(/* ... */) {
+ let href;
+ if (arguments.length == 1 && arguments[0] instanceof Location) {
+ href = String(arguments[0]);
+ } else if (typeof arguments[0] == "string") {
+ const options = arguments[1] || { };
+ href = encode(decode(arguments[0], options), options);
+ } else {
+ href = encode.apply(self, arguments);
+ }
+ return href;
+ }
+
+ function replace(/* ... */) {
+ if (self !== last_loc)
+ return;
+ const href = href_for_go_or_replace.apply(self, arguments);
+ window.location.replace(window.location.pathname + '#' + href);
+ }
+
+ function go(/* ... */) {
+ if (self !== last_loc)
+ return;
+ const href = href_for_go_or_replace.apply(self, arguments);
+ window.location.hash = '#' + href;
+ }
+
+ Object.defineProperties(self, {
+ path: {
+ enumerable: true,
+ writable: false,
+ value: self.path
+ },
+ options: {
+ enumerable: true,
+ writable: false,
+ value: options
+ },
+ href: {
+ enumerable: true,
+ value: href
+ },
+ go: { value: go },
+ replace: { value: replace },
+ encode: { value: encode },
+ decode: { value: decode },
+ toString: { value: function() { return href } }
+ });
+ }
+
+ Object.defineProperty(cockpit, "location", {
+ enumerable: true,
+ get: function() {
+ if (!last_loc || last_loc.href !== get_window_location_hash())
+ last_loc = new Location();
+ return last_loc;
+ },
+ set: function(v) {
+ cockpit.location.go(v);
+ }
+ });
+
+ window.addEventListener("hashchange", function() {
+ last_loc = null;
+ let hash = window.location.hash;
+ if (hash.indexOf("#") === 0)
+ hash = hash.substring(1);
+ cockpit.hint("location", { hash });
+ cockpit.dispatchEvent("locationchanged");
+ });
+
+ /* ------------------------------------------------------------------------
+ * Cockpit jump
+ */
+
+ cockpit.jump = function jump(path, host) {
+ if (Array.isArray(path))
+ path = "/" + path.map(encodeURIComponent).join("/")
+.replaceAll("%40", "@")
+.replaceAll("%3D", "=")
+.replaceAll("%2B", "+");
+ else
+ path = "" + path;
+
+ /* When host is not given (undefined), use current transport's host. If
+ * it is null, use localhost.
+ */
+ if (host === undefined)
+ host = cockpit.transport.host;
+
+ const options = { command: "jump", location: path, host };
+ cockpit.transport.inject("\n" + JSON.stringify(options));
+ };
+
+ /* ---------------------------------------------------------------------
+ * Cockpit Page Visibility
+ */
+
+ (function() {
+ let hiddenProp;
+ let hiddenHint = false;
+
+ function visibility_change() {
+ let value = document[hiddenProp];
+ if (!hiddenProp || typeof value === "undefined")
+ value = false;
+ if (value === false)
+ value = hiddenHint;
+ if (cockpit.hidden !== value) {
+ cockpit.hidden = value;
+ cockpit.dispatchEvent("visibilitychange");
+ }
+ }
+
+ if (typeof document.hidden !== "undefined") {
+ hiddenProp = "hidden";
+ document.addEventListener("visibilitychange", visibility_change);
+ } else if (typeof document.mozHidden !== "undefined") {
+ hiddenProp = "mozHidden";
+ document.addEventListener("mozvisibilitychange", visibility_change);
+ } else if (typeof document.msHidden !== "undefined") {
+ hiddenProp = "msHidden";
+ document.addEventListener("msvisibilitychange", visibility_change);
+ } else if (typeof document.webkitHidden !== "undefined") {
+ hiddenProp = "webkitHidden";
+ document.addEventListener("webkitvisibilitychange", visibility_change);
+ }
+
+ /*
+ * Wait for changes in visibility of just our iframe. These are delivered
+ * via a hint message from the parent. For now we are the only handler of
+ * hint messages, so this is implemented rather simply on purpose.
+ */
+ process_hints = function(data) {
+ if ("hidden" in data) {
+ hiddenHint = data.hidden;
+ visibility_change();
+ }
+ };
+
+ /* The first time */
+ visibility_change();
+ }());
+
+ /* ---------------------------------------------------------------------
+ * Spawning
+ */
+
+ function ProcessError(options, name) {
+ this.problem = options.problem || null;
+ this.exit_status = options["exit-status"];
+ if (this.exit_status === undefined)
+ this.exit_status = null;
+ this.exit_signal = options["exit-signal"];
+ if (this.exit_signal === undefined)
+ this.exit_signal = null;
+ this.message = options.message;
+
+ if (this.message === undefined) {
+ if (this.problem)
+ this.message = cockpit.message(options.problem);
+ else if (this.exit_signal !== null)
+ this.message = cockpit.format(_("$0 killed with signal $1"), name, this.exit_signal);
+ else if (this.exit_status !== undefined)
+ this.message = cockpit.format(_("$0 exited with code $1"), name, this.exit_status);
+ else
+ this.message = cockpit.format(_("$0 failed"), name);
+ } else {
+ this.message = this.message.trim();
+ }
+
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ function spawn_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("spawn"))
+ console.debug.apply(console, arguments);
+ }
+
+ /* public */
+ cockpit.spawn = function(command, options) {
+ const dfd = cockpit.defer();
+
+ const args = { payload: "stream", spawn: [] };
+ if (command instanceof Array) {
+ for (let i = 0; i < command.length; i++)
+ args.spawn.push(String(command[i]));
+ } else {
+ args.spawn.push(String(command));
+ }
+ if (options !== undefined)
+ Object.assign(args, options);
+
+ spawn_debug("process spawn:", JSON.stringify(args.spawn));
+
+ const name = args.spawn[0] || "process";
+ const channel = cockpit.channel(args);
+
+ /* Callback that wants a stream response, see below */
+ const buffer = channel.buffer(null);
+
+ channel.addEventListener("close", function(event, options) {
+ const data = buffer.squash();
+ spawn_debug("process closed:", JSON.stringify(options));
+ if (data)
+ spawn_debug("process output:", data);
+ if (options.message !== undefined)
+ spawn_debug("process error:", options.message);
+
+ if (options.problem)
+ dfd.reject(new ProcessError(options, name));
+ else if (options["exit-status"] || options["exit-signal"])
+ dfd.reject(new ProcessError(options, name), data);
+ else if (options.message !== undefined)
+ dfd.resolve(data, options.message);
+ else
+ dfd.resolve(data);
+ });
+
+ const ret = dfd.promise;
+ ret.stream = function(callback) {
+ buffer.callback = callback.bind(ret);
+ return this;
+ };
+
+ ret.input = function(message, stream) {
+ if (message !== null && message !== undefined) {
+ spawn_debug("process input:", message);
+ iterate_data(message, function(data) {
+ channel.send(data);
+ });
+ }
+ if (!stream)
+ channel.control({ command: "done" });
+ return this;
+ };
+
+ ret.close = function(problem) {
+ spawn_debug("process closing:", problem);
+ if (channel.valid)
+ channel.close(problem);
+ return this;
+ };
+
+ return ret;
+ };
+
+ /* public */
+ cockpit.script = function(script, args, options) {
+ if (!options && is_plain_object(args)) {
+ options = args;
+ args = [];
+ }
+ const command = ["/bin/sh", "-c", script, "--"];
+ command.push.apply(command, args);
+ return cockpit.spawn(command, options);
+ };
+
+ function dbus_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("dbus"))
+ console.debug.apply(console, arguments);
+ }
+
+ function DBusError(arg, arg1) {
+ if (typeof (arg) == "string") {
+ this.problem = arg;
+ this.name = null;
+ this.message = arg1 || cockpit.message(arg);
+ } else {
+ this.problem = null;
+ this.name = arg[0];
+ this.message = arg[1][0] || arg[0];
+ }
+ this.toString = function() {
+ return this.message;
+ };
+ }
+
+ function DBusCache() {
+ const self = this;
+
+ let callbacks = [];
+ self.data = { };
+ self.meta = { };
+
+ self.connect = function connect(path, iface, callback, first) {
+ const cb = [path, iface, callback];
+ if (first)
+ callbacks.unshift(cb);
+ else
+ callbacks.push(cb);
+ return {
+ remove: function remove() {
+ const length = callbacks.length;
+ for (let i = 0; i < length; i++) {
+ const cb = callbacks[i];
+ if (cb[0] === path && cb[1] === iface && cb[2] === callback) {
+ delete cb[i];
+ break;
+ }
+ }
+ }
+ };
+ };
+
+ function emit(path, iface, props) {
+ const copy = callbacks.slice();
+ const length = copy.length;
+ for (let i = 0; i < length; i++) {
+ const cb = copy[i];
+ if ((!cb[0] || cb[0] === path) &&
+ (!cb[1] || cb[1] === iface)) {
+ cb[2](props, path);
+ }
+ }
+ }
+
+ self.update = function update(path, iface, props) {
+ if (!self.data[path])
+ self.data[path] = { };
+ if (!self.data[path][iface])
+ self.data[path][iface] = props;
+ else
+ props = Object.assign(self.data[path][iface], props);
+ emit(path, iface, props);
+ };
+
+ self.remove = function remove(path, iface) {
+ if (self.data[path]) {
+ delete self.data[path][iface];
+ emit(path, iface, null);
+ }
+ };
+
+ self.lookup = function lookup(path, iface) {
+ if (self.data[path])
+ return self.data[path][iface];
+ return undefined;
+ };
+
+ self.each = function each(iface, callback) {
+ for (const path in self.data) {
+ for (const ifa in self.data[path]) {
+ if (ifa == iface)
+ callback(self.data[path][iface], path);
+ }
+ }
+ };
+
+ self.close = function close() {
+ self.data = { };
+ const copy = callbacks;
+ callbacks = [];
+ const length = copy.length;
+ for (let i = 0; i < length; i++)
+ copy[i].callback();
+ };
+ }
+
+ function DBusProxy(client, cache, iface, path, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ let valid = false;
+ let defined = false;
+ const waits = cockpit.defer();
+
+ /* No enumeration on these properties */
+ Object.defineProperties(self, {
+ client: { value: client, enumerable: false, writable: false },
+ path: { value: path, enumerable: false, writable: false },
+ iface: { value: iface, enumerable: false, writable: false },
+ valid: { get: function() { return valid }, enumerable: false },
+ wait: {
+ enumerable: false,
+ writable: false,
+ value: function(func) {
+ if (func)
+ waits.promise.always(func);
+ return waits.promise;
+ }
+ },
+ call: {
+ value: function(name, args, options) { return client.call(path, iface, name, args, options) },
+ enumerable: false,
+ writable: false
+ },
+ data: { value: { }, enumerable: false }
+ });
+
+ if (!options)
+ options = { };
+
+ function define() {
+ if (!cache.meta[iface])
+ return;
+
+ const meta = cache.meta[iface];
+ defined = true;
+
+ Object.keys(meta.methods || { }).forEach(function(name) {
+ if (name[0].toLowerCase() == name[0])
+ return; /* Only map upper case */
+
+ /* Again, make sure these don't show up in enumerations */
+ Object.defineProperty(self, name, {
+ enumerable: false,
+ value: function() {
+ const dfd = cockpit.defer();
+ client.call(path, iface, name, Array.prototype.slice.call(arguments))
+ .done(function(reply) { dfd.resolve.apply(dfd, reply) })
+ .fail(function(ex) { dfd.reject(ex) });
+ return dfd.promise;
+ }
+ });
+ });
+
+ Object.keys(meta.properties || { }).forEach(function(name) {
+ if (name[0].toLowerCase() == name[0])
+ return; /* Only map upper case */
+
+ const config = {
+ enumerable: true,
+ get: function() { return self.data[name] },
+ set: function(v) { throw Error(name + "is not writable") }
+ };
+
+ const prop = meta.properties[name];
+ if (prop.flags && prop.flags.indexOf('w') !== -1) {
+ config.set = function(v) {
+ client.call(path, "org.freedesktop.DBus.Properties", "Set",
+ [iface, name, cockpit.variant(prop.type, v)])
+ .fail(function(ex) {
+ console.log("Couldn't set " + iface + " " + name +
+ " at " + path + ": " + ex);
+ });
+ };
+ }
+
+ /* Again, make sure these don't show up in enumerations */
+ Object.defineProperty(self, name, config);
+ });
+ }
+
+ function update(props) {
+ if (props) {
+ Object.assign(self.data, props);
+ if (!defined)
+ define();
+ valid = true;
+ } else {
+ valid = false;
+ }
+ self.dispatchEvent("changed", props);
+ }
+
+ cache.connect(path, iface, update, true);
+ update(cache.lookup(path, iface));
+
+ function signal(path, iface, name, args) {
+ self.dispatchEvent("signal", name, args);
+ if (name[0].toLowerCase() != name[0]) {
+ args = args.slice();
+ args.unshift(name);
+ self.dispatchEvent.apply(self, args);
+ }
+ }
+
+ client.subscribe({ path, interface: iface }, signal, options.subscribe !== false);
+
+ function waited(ex) {
+ if (valid)
+ waits.resolve();
+ else
+ waits.reject(ex);
+ }
+
+ /* If watching then do a proper watch, otherwise object is done */
+ if (options.watch !== false)
+ client.watch({ path, interface: iface }).always(waited);
+ else
+ waited();
+ }
+
+ function DBusProxies(client, cache, iface, path_namespace, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ let waits;
+
+ Object.defineProperties(self, {
+ client: { value: client, enumerable: false, writable: false },
+ iface: { value: iface, enumerable: false, writable: false },
+ path_namespace: { value: path_namespace, enumerable: false, writable: false },
+ wait: {
+ enumerable: false,
+ writable: false,
+ value: function(func) {
+ if (func)
+ waits.always(func);
+ return waits;
+ }
+ }
+ });
+
+ /* Subscribe to signals once for all proxies */
+ const match = { interface: iface, path_namespace };
+
+ /* Callbacks added by proxies */
+ client.subscribe(match);
+
+ /* Watch for property changes */
+ if (options.watch !== false) {
+ waits = client.watch(match);
+ } else {
+ waits = cockpit.defer().resolve().promise;
+ }
+
+ /* Already added watch/subscribe, tell proxies not to */
+ options = { watch: false, subscribe: false, ...options };
+
+ function update(props, path) {
+ let proxy = self[path];
+ if (path) {
+ if (!props && proxy) {
+ delete self[path];
+ self.dispatchEvent("removed", proxy);
+ } else if (props) {
+ if (!proxy) {
+ proxy = self[path] = client.proxy(iface, path, options);
+ self.dispatchEvent("added", proxy);
+ }
+ self.dispatchEvent("changed", proxy);
+ }
+ }
+ }
+
+ cache.connect(null, iface, update, false);
+ cache.each(iface, update);
+ }
+
+ function DBusClient(name, options) {
+ const self = this;
+ event_mixin(self, { });
+
+ const args = { };
+ let track = false;
+ let owner = null;
+
+ if (options) {
+ if (options.track)
+ track = true;
+
+ delete options.track;
+ Object.assign(args, options);
+ }
+ args.payload = "dbus-json3";
+ if (name)
+ args.name = name;
+ self.options = options;
+ self.unique_name = null;
+
+ dbus_debug("dbus open: ", args);
+
+ let channel = cockpit.channel(args);
+ const subscribers = { };
+ let calls = { };
+ let cache;
+
+ /* The problem we closed with */
+ let closed;
+
+ self.constructors = { "*": DBusProxy };
+
+ /* Allows waiting on the channel if necessary */
+ self.wait = channel.wait;
+
+ function ensure_cache() {
+ if (!cache)
+ cache = new DBusCache();
+ }
+
+ function send(payload) {
+ if (channel?.valid) {
+ dbus_debug("dbus:", payload);
+ channel.send(payload);
+ return true;
+ }
+ return false;
+ }
+
+ function matches(signal, match) {
+ if (match.path && signal[0] !== match.path)
+ return false;
+ if (match.path_namespace && signal[0].indexOf(match.path_namespace) !== 0)
+ return false;
+ if (match.interface && signal[1] !== match.interface)
+ return false;
+ if (match.member && signal[2] !== match.member)
+ return false;
+ if (match.arg0 && (!signal[3] || signal[3][0] !== match.arg0))
+ return false;
+ return true;
+ }
+
+ function on_message(event, payload) {
+ dbus_debug("dbus:", payload);
+ let msg;
+ try {
+ msg = JSON.parse(payload);
+ } catch (ex) {
+ console.warn("received invalid dbus json message:", ex);
+ }
+ if (msg === undefined) {
+ channel.close({ problem: "protocol-error" });
+ return;
+ }
+ const dfd = (msg.id !== undefined) ? calls[msg.id] : undefined;
+ if (msg.reply) {
+ if (dfd) {
+ const options = { };
+ if (msg.type)
+ options.type = msg.type;
+ if (msg.flags)
+ options.flags = msg.flags;
+ dfd.resolve(msg.reply[0] || [], options);
+ delete calls[msg.id];
+ }
+ return;
+ } else if (msg.error) {
+ if (dfd) {
+ dfd.reject(new DBusError(msg.error));
+ delete calls[msg.id];
+ }
+ return;
+ }
+
+ /*
+ * The above promise resolutions or failures are triggered via
+ * later_invoke(). In order to preserve ordering guarantees we
+ * also have to process other events that way too.
+ */
+ later_invoke(function() {
+ if (msg.signal) {
+ for (const id in subscribers) {
+ const subscription = subscribers[id];
+ if (subscription.callback) {
+ if (matches(msg.signal, subscription.match))
+ subscription.callback.apply(self, msg.signal);
+ }
+ }
+ } else if (msg.notify) {
+ notify(msg.notify);
+ } else if (msg.meta) {
+ meta(msg.meta);
+ } else if (msg.owner !== undefined) {
+ self.dispatchEvent("owner", msg.owner);
+
+ /*
+ * We won't get this signal with the same
+ * owner twice so if we've seen an owner
+ * before that means it has changed.
+ */
+ if (track && owner)
+ self.close();
+
+ owner = msg.owner;
+ } else {
+ dbus_debug("received unexpected dbus json message:", payload);
+ }
+ });
+ }
+
+ function meta(data) {
+ ensure_cache();
+ Object.assign(cache.meta, data);
+ self.dispatchEvent("meta", data);
+ }
+
+ function notify(data) {
+ ensure_cache();
+ for (const path in data) {
+ for (const iface in data[path]) {
+ const props = data[path][iface];
+ if (!props)
+ cache.remove(path, iface);
+ else
+ cache.update(path, iface, props);
+ }
+ }
+ self.dispatchEvent("notify", data);
+ }
+
+ this.notify = notify;
+
+ function close_perform(options) {
+ closed = options.problem || "disconnected";
+ const outstanding = calls;
+ calls = { };
+ for (const id in outstanding) {
+ outstanding[id].reject(new DBusError(closed, options.message));
+ }
+ self.dispatchEvent("close", options);
+ }
+
+ this.close = function close(options) {
+ if (typeof options == "string")
+ options = { problem: options };
+ if (!options)
+ options = { };
+ if (channel)
+ channel.close(options);
+ else
+ close_perform(options);
+ };
+
+ function on_ready(event, message) {
+ dbus_debug("dbus ready:", options);
+ self.unique_name = message["unique-name"];
+ }
+
+ function on_close(event, options) {
+ dbus_debug("dbus close:", options);
+ channel.removeEventListener("ready", on_ready);
+ channel.removeEventListener("message", on_message);
+ channel.removeEventListener("close", on_close);
+ channel = null;
+ close_perform(options);
+ }
+
+ channel.addEventListener("ready", on_ready);
+ channel.addEventListener("message", on_message);
+ channel.addEventListener("close", on_close);
+
+ let last_cookie = 1;
+
+ this.call = function call(path, iface, method, args, options) {
+ const dfd = cockpit.defer();
+ const id = String(last_cookie);
+ last_cookie++;
+ const method_call = {
+ ...options,
+ call: [path, iface, method, args || []],
+ id
+ };
+
+ const msg = JSON.stringify(method_call);
+ if (send(msg))
+ calls[id] = dfd;
+ else
+ dfd.reject(new DBusError(closed));
+
+ return dfd.promise;
+ };
+
+ self.signal = function signal(path, iface, member, args, options) {
+ if (!channel || !channel.valid)
+ return;
+
+ const message = { ...options, signal: [path, iface, member, args || []] };
+
+ send(JSON.stringify(message));
+ };
+
+ this.subscribe = function subscribe(match, callback, rule) {
+ const subscription = {
+ match: { ...match },
+ callback
+ };
+
+ if (rule !== false)
+ send(JSON.stringify({ "add-match": subscription.match }));
+
+ let id;
+ if (callback) {
+ id = String(last_cookie);
+ last_cookie++;
+ subscribers[id] = subscription;
+ }
+
+ return {
+ remove: function() {
+ let prev;
+ if (id) {
+ prev = subscribers[id];
+ if (prev)
+ delete subscribers[id];
+ }
+ if (rule !== false && prev)
+ send(JSON.stringify({ "remove-match": prev.match }));
+ }
+ };
+ };
+
+ self.watch = function watch(path) {
+ const match = is_plain_object(path) ? { ...path } : { path: String(path) };
+
+ const id = String(last_cookie);
+ last_cookie++;
+ const dfd = cockpit.defer();
+
+ const msg = JSON.stringify({ watch: match, id });
+ if (send(msg))
+ calls[id] = dfd;
+ else
+ dfd.reject(new DBusError(closed));
+
+ const ret = dfd.promise;
+ ret.remove = function remove() {
+ if (id in calls) {
+ dfd.reject(new DBusError("cancelled"));
+ delete calls[id];
+ }
+ send(JSON.stringify({ unwatch: match }));
+ };
+ return ret;
+ };
+
+ self.proxy = function proxy(iface, path, options) {
+ if (!iface)
+ iface = name;
+ iface = String(iface);
+ if (!path)
+ path = "/" + iface.replaceAll(".", "/");
+ let Constructor = self.constructors[iface];
+ if (!Constructor)
+ Constructor = self.constructors["*"];
+ if (!options)
+ options = { };
+ ensure_cache();
+ return new Constructor(self, cache, iface, String(path), options);
+ };
+
+ self.proxies = function proxies(iface, path_namespace, options) {
+ if (!iface)
+ iface = name;
+ if (!path_namespace)
+ path_namespace = "/";
+ if (!options)
+ options = { };
+ ensure_cache();
+ return new DBusProxies(self, cache, String(iface), String(path_namespace), options);
+ };
+ }
+
+ /* Well known buses */
+ const shared_dbus = {
+ internal: null,
+ session: null,
+ system: null,
+ };
+
+ /* public */
+ cockpit.dbus = function dbus(name, options) {
+ if (!options)
+ options = { bus: "system" };
+
+ /*
+ * Figure out if this we should use a shared bus.
+ *
+ * This is only the case if a null name *and* the
+ * options are just a simple { "bus": "xxxx" }
+ */
+ const keys = Object.keys(options);
+ const bus = options.bus;
+ const shared = !name && keys.length == 1 && bus in shared_dbus;
+
+ if (shared && shared_dbus[bus])
+ return shared_dbus[bus];
+
+ const client = new DBusClient(name, options);
+
+ /*
+ * Store the shared bus for next time. Override the
+ * close function to only work when a problem is
+ * indicated.
+ */
+ if (shared) {
+ const old_close = client.close;
+ client.close = function() {
+ if (arguments.length > 0)
+ old_close.apply(client, arguments);
+ };
+ client.addEventListener("close", function() {
+ if (shared_dbus[bus] == client)
+ shared_dbus[bus] = null;
+ });
+ shared_dbus[bus] = client;
+ }
+
+ return client;
+ };
+
+ cockpit.variant = function variant(type, value) {
+ return { v: value, t: type };
+ };
+
+ cockpit.byte_array = function byte_array(string) {
+ return window.btoa(string);
+ };
+
+ /* File access
+ */
+
+ cockpit.file = function file(path, options) {
+ options = options || { };
+ const binary = options.binary;
+
+ const self = {
+ path,
+ read,
+ replace,
+ modify,
+
+ watch,
+
+ close
+ };
+
+ const base_channel_options = { ...options };
+ delete base_channel_options.syntax;
+
+ function parse(str) {
+ if (options.syntax?.parse)
+ return options.syntax.parse(str);
+ else
+ return str;
+ }
+
+ function stringify(obj) {
+ if (options.syntax?.stringify)
+ return options.syntax.stringify(obj);
+ else
+ return obj;
+ }
+
+ let read_promise = null;
+ let read_channel;
+
+ function read() {
+ if (read_promise)
+ return read_promise;
+
+ const dfd = cockpit.defer();
+ const opts = {
+ ...base_channel_options,
+ payload: "fsread1",
+ path
+ };
+
+ function try_read() {
+ read_channel = cockpit.channel(opts);
+ const content_parts = [];
+ read_channel.addEventListener("message", function (event, message) {
+ content_parts.push(message);
+ });
+ read_channel.addEventListener("close", function (event, message) {
+ read_channel = null;
+
+ if (message.problem == "change-conflict") {
+ try_read();
+ return;
+ }
+
+ read_promise = null;
+
+ if (message.problem) {
+ const error = new BasicError(message.problem, message.message);
+ fire_watch_callbacks(null, null, error);
+ dfd.reject(error);
+ return;
+ }
+
+ let content;
+ if (message.tag == "-")
+ content = null;
+ else {
+ try {
+ content = parse(join_data(content_parts, binary));
+ } catch (e) {
+ fire_watch_callbacks(null, null, e);
+ dfd.reject(e);
+ return;
+ }
+ }
+
+ fire_watch_callbacks(content, message.tag);
+ dfd.resolve(content, message.tag);
+ });
+ }
+
+ try_read();
+
+ read_promise = dfd.promise;
+ return read_promise;
+ }
+
+ let replace_channel = null;
+
+ function replace(new_content, expected_tag) {
+ const dfd = cockpit.defer();
+
+ let file_content;
+ try {
+ file_content = (new_content === null) ? null : stringify(new_content);
+ } catch (e) {
+ dfd.reject(e);
+ return dfd.promise;
+ }
+
+ if (replace_channel)
+ replace_channel.close("abort");
+
+ const opts = {
+ ...base_channel_options,
+ payload: "fsreplace1",
+ path,
+ tag: expected_tag
+ };
+ replace_channel = cockpit.channel(opts);
+
+ replace_channel.addEventListener("close", function (event, message) {
+ replace_channel = null;
+ if (message.problem) {
+ dfd.reject(new BasicError(message.problem, message.message));
+ } else {
+ fire_watch_callbacks(new_content, message.tag);
+ dfd.resolve(message.tag);
+ }
+ });
+
+ iterate_data(file_content, function(data) {
+ replace_channel.send(data);
+ });
+
+ replace_channel.control({ command: "done" });
+ return dfd.promise;
+ }
+
+ function modify(callback, initial_content, initial_tag) {
+ const dfd = cockpit.defer();
+
+ function update(content, tag) {
+ let new_content = callback(content);
+ if (new_content === undefined)
+ new_content = content;
+ replace(new_content, tag)
+ .done(function (new_tag) {
+ dfd.resolve(new_content, new_tag);
+ })
+ .fail(function (error) {
+ if (error.problem == "change-conflict")
+ read_then_update();
+ else
+ dfd.reject(error);
+ });
+ }
+
+ function read_then_update() {
+ read()
+ .done(update)
+ .fail(function (error) {
+ dfd.reject(error);
+ });
+ }
+
+ if (initial_content === undefined)
+ read_then_update();
+ else
+ update(initial_content, initial_tag);
+
+ return dfd.promise;
+ }
+
+ const watch_callbacks = [];
+ let n_watch_callbacks = 0;
+
+ let watch_channel = null;
+ let watch_tag;
+
+ function ensure_watch_channel(options) {
+ if (n_watch_callbacks > 0) {
+ if (watch_channel)
+ return;
+
+ const opts = {
+ payload: "fswatch1",
+ path,
+ superuser: base_channel_options.superuser,
+ };
+ watch_channel = cockpit.channel(opts);
+ watch_channel.addEventListener("message", function (event, message_string) {
+ let message;
+ try {
+ message = JSON.parse(message_string);
+ } catch (e) {
+ message = null;
+ }
+ if (message && message.path == path && message.tag && message.tag != watch_tag) {
+ if (options && options.read !== undefined && !options.read)
+ fire_watch_callbacks(null, message.tag);
+ else
+ read();
+ }
+ });
+ } else {
+ if (watch_channel) {
+ watch_channel.close();
+ watch_channel = null;
+ }
+ }
+ }
+
+ function fire_watch_callbacks(/* content, tag, error */) {
+ watch_tag = arguments[1] || null;
+ invoke_functions(watch_callbacks, self, arguments);
+ }
+
+ function watch(callback, options) {
+ if (callback)
+ watch_callbacks.push(callback);
+ n_watch_callbacks += 1;
+ ensure_watch_channel(options);
+
+ watch_tag = null;
+ read();
+
+ return {
+ remove: function () {
+ if (callback) {
+ const index = watch_callbacks.indexOf(callback);
+ if (index > -1)
+ watch_callbacks[index] = null;
+ }
+ n_watch_callbacks -= 1;
+ ensure_watch_channel(options);
+ }
+ };
+ }
+
+ function close() {
+ if (read_channel)
+ read_channel.close("cancelled");
+ if (replace_channel)
+ replace_channel.close("cancelled");
+ if (watch_channel)
+ watch_channel.close("cancelled");
+ }
+
+ return self;
+ };
+
+ /* ---------------------------------------------------------------------
+ * Localization
+ */
+
+ let po_data = { };
+ let po_plural;
+
+ cockpit.language = "en";
+ cockpit.language_direction = "ltr";
+ const test_l10n = window.localStorage.test_l10n;
+
+ cockpit.locale = function locale(po) {
+ let lang = cockpit.language;
+ let lang_dir = cockpit.language_direction;
+ let header;
+
+ if (po) {
+ Object.assign(po_data, po);
+ header = po[""];
+ } else if (po === null) {
+ po_data = { };
+ }
+
+ if (header) {
+ if (header["plural-forms"])
+ po_plural = header["plural-forms"];
+ if (header.language)
+ lang = header.language;
+ if (header["language-direction"])
+ lang_dir = header["language-direction"];
+ }
+
+ cockpit.language = lang;
+ cockpit.language_direction = lang_dir;
+ };
+
+ cockpit.translate = function translate(/* ... */) {
+ let what;
+
+ /* Called without arguments, entire document */
+ if (arguments.length === 0)
+ what = [document];
+
+ /* Called with a single array like argument */
+ else if (arguments.length === 1 && arguments[0].length)
+ what = arguments[0];
+
+ /* Called with 1 or more element arguments */
+ else
+ what = arguments;
+
+ /* Translate all the things */
+ const wlen = what.length;
+ for (let w = 0; w < wlen; w++) {
+ /* The list of things to translate */
+ let list = null;
+ if (what[w].querySelectorAll)
+ list = what[w].querySelectorAll("[translatable], [translate]");
+ if (!list)
+ continue;
+
+ /* Each element */
+ for (let i = 0; i < list.length; i++) {
+ const el = list[i];
+
+ let val = el.getAttribute("translate") || el.getAttribute("translatable") || "yes";
+ if (val == "no")
+ continue;
+
+ /* Each thing to translate */
+ const tasks = val.split(" ");
+ val = el.getAttribute("translate-context") || el.getAttribute("context");
+ for (let t = 0; t < tasks.length; t++) {
+ if (tasks[t] == "yes" || tasks[t] == "translate")
+ el.textContent = cockpit.gettext(val, el.textContent);
+ else if (tasks[t])
+ el.setAttribute(tasks[t], cockpit.gettext(val, el.getAttribute(tasks[t]) || ""));
+ }
+
+ /* Mark this thing as translated */
+ el.removeAttribute("translatable");
+ el.removeAttribute("translate");
+ }
+ }
+ };
+
+ cockpit.gettext = function gettext(context, string) {
+ /* Missing first parameter */
+ if (arguments.length == 1) {
+ string = context;
+ context = undefined;
+ }
+
+ const key = context ? context + '\u0004' + string : string;
+ if (po_data) {
+ const translated = po_data[key];
+ if (translated?.[1])
+ string = translated[1];
+ }
+
+ if (test_l10n === 'true')
+ return "»" + string + "«";
+
+ return string;
+ };
+
+ function imply(val) {
+ return (val === true ? 1 : val || 0);
+ }
+
+ cockpit.ngettext = function ngettext(context, string1, stringN, num) {
+ /* Missing first parameter */
+ if (arguments.length == 3) {
+ num = stringN;
+ stringN = string1;
+ string1 = context;
+ context = undefined;
+ }
+
+ const key = context ? context + '\u0004' + string1 : string1;
+ if (po_data && po_plural) {
+ const translated = po_data[key];
+ if (translated) {
+ const i = imply(po_plural(num)) + 1;
+ if (translated[i])
+ return translated[i];
+ }
+ }
+ if (num == 1)
+ return string1;
+ return stringN;
+ };
+
+ cockpit.noop = function noop(arg0, arg1) {
+ return arguments[arguments.length - 1];
+ };
+
+ /* Only for _() calls here in the cockpit code */
+ const _ = cockpit.gettext;
+
+ cockpit.message = function message(arg) {
+ if (arg.message)
+ return arg.message;
+
+ let problem = null;
+ if (arg.problem)
+ problem = arg.problem;
+ else
+ problem = arg + "";
+ if (problem == "terminated")
+ return _("Your session has been terminated.");
+ else if (problem == "no-session")
+ return _("Your session has expired. Please log in again.");
+ else if (problem == "access-denied")
+ return _("Not permitted to perform this action.");
+ else if (problem == "authentication-failed")
+ return _("Login failed");
+ else if (problem == "authentication-not-supported")
+ return _("The server refused to authenticate using any supported methods.");
+ else if (problem == "unknown-hostkey")
+ return _("Untrusted host");
+ else if (problem == "unknown-host")
+ return _("Untrusted host");
+ else if (problem == "invalid-hostkey")
+ return _("Host key is incorrect");
+ else if (problem == "internal-error")
+ return _("Internal error");
+ else if (problem == "timeout")
+ return _("Connection has timed out.");
+ else if (problem == "no-cockpit")
+ return _("Cockpit is not installed on the system.");
+ else if (problem == "no-forwarding")
+ return _("Cannot forward login credentials");
+ else if (problem == "disconnected")
+ return _("Server has closed the connection.");
+ else if (problem == "not-supported")
+ return _("Cockpit is not compatible with the software on the system.");
+ else if (problem == "no-host")
+ return _("Cockpit could not contact the given host.");
+ else if (problem == "too-large")
+ return _("Too much data");
+ else
+ return problem;
+ };
+
+ function HttpError(arg0, arg1, message) {
+ this.status = parseInt(arg0, 10);
+ this.reason = arg1;
+ this.message = message || arg1;
+ this.problem = null;
+
+ this.valueOf = function() {
+ return this.status;
+ };
+ this.toString = function() {
+ return this.status + " " + this.message;
+ };
+ }
+
+ function http_debug() {
+ if (window.debugging == "all" || window.debugging?.includes("http"))
+ console.debug.apply(console, arguments);
+ }
+
+ function find_header(headers, name) {
+ if (!headers)
+ return undefined;
+ name = name.toLowerCase();
+ for (const head in headers) {
+ if (head.toLowerCase() == name)
+ return headers[head];
+ }
+ return undefined;
+ }
+
+ function HttpClient(endpoint, options) {
+ const self = this;
+
+ self.options = options;
+ options.payload = "http-stream2";
+
+ const active_requests = [];
+
+ if (endpoint !== undefined) {
+ if (endpoint.indexOf && endpoint.indexOf("/") === 0) {
+ options.unix = endpoint;
+ } else {
+ const port = parseInt(endpoint, 10);
+ if (!isNaN(port))
+ options.port = port;
+ else
+ throw Error("The endpoint must be either a unix path or port number");
+ }
+ }
+
+ if (options.address) {
+ if (!options.capabilities)
+ options.capabilities = [];
+ options.capabilities.push("address");
+ }
+
+ function param(obj) {
+ return Object.keys(obj).map(function(k) {
+ return encodeURIComponent(k) + '=' + encodeURIComponent(obj[k]);
+ })
+.join('&')
+.split('%20')
+.join('+'); /* split/join because phantomjs */
+ }
+
+ self.request = function request(req) {
+ const dfd = cockpit.defer();
+ const ret = dfd.promise;
+
+ if (!req.path)
+ req.path = "/";
+ if (!req.method)
+ req.method = "GET";
+ if (req.params) {
+ if (req.path.indexOf("?") === -1)
+ req.path += "?" + param(req.params);
+ else
+ req.path += "&" + param(req.params);
+ }
+ delete req.params;
+
+ const input = req.body;
+ delete req.body;
+
+ const headers = req.headers;
+ delete req.headers;
+
+ Object.assign(req, options);
+
+ /* Combine the headers */
+ if (options.headers && headers)
+ req.headers = { ...options.headers, ...headers };
+ else if (options.headers)
+ req.headers = options.headers;
+ else
+ req.headers = headers;
+
+ http_debug("http request:", JSON.stringify(req));
+
+ /* We need a channel for the request */
+ const channel = cockpit.channel(req);
+
+ if (input !== undefined) {
+ if (input !== "") {
+ http_debug("http input:", input);
+ iterate_data(input, function(data) {
+ channel.send(data);
+ });
+ }
+ http_debug("http done");
+ channel.control({ command: "done" });
+ }
+
+ /* Callbacks that want to stream or get headers */
+ let streamer = null;
+ let responsers = null;
+
+ let resp = null;
+
+ const buffer = channel.buffer(function(data) {
+ /* Fire any streamers */
+ if (resp && resp.status >= 200 && resp.status <= 299 && streamer)
+ return streamer.call(ret, data);
+ return 0;
+ });
+
+ function on_control(event, options) {
+ /* Anyone looking for response details? */
+ if (options.command == "response") {
+ resp = options;
+ if (responsers) {
+ resp.headers = resp.headers || { };
+ invoke_functions(responsers, ret, [resp.status, resp.headers]);
+ }
+ }
+ }
+
+ function on_close(event, options) {
+ const pos = active_requests.indexOf(ret);
+ if (pos >= 0)
+ active_requests.splice(pos, 1);
+
+ if (options.problem) {
+ http_debug("http problem: ", options.problem);
+ dfd.reject(new BasicError(options.problem));
+ } else {
+ const body = buffer.squash();
+
+ /* An error, fail here */
+ if (resp && (resp.status < 200 || resp.status > 299)) {
+ let message;
+ const type = find_header(resp.headers, "Content-Type");
+ if (type && !channel.binary) {
+ if (type.indexOf("text/plain") === 0)
+ message = body;
+ }
+ http_debug("http status: ", resp.status);
+ dfd.reject(new HttpError(resp.status, resp.reason, message), body);
+ } else {
+ http_debug("http done");
+ dfd.resolve(body);
+ }
+ }
+
+ channel.removeEventListener("control", on_control);
+ channel.removeEventListener("close", on_close);
+ }
+
+ channel.addEventListener("control", on_control);
+ channel.addEventListener("close", on_close);
+
+ ret.stream = function(callback) {
+ streamer = callback;
+ return ret;
+ };
+ ret.response = function(callback) {
+ if (responsers === null)
+ responsers = [];
+ responsers.push(callback);
+ return ret;
+ };
+ ret.input = function(message, stream) {
+ if (message !== null && message !== undefined) {
+ http_debug("http input:", message);
+ iterate_data(message, function(data) {
+ channel.send(data);
+ });
+ }
+ if (!stream) {
+ http_debug("http done");
+ channel.control({ command: "done" });
+ }
+ return ret;
+ };
+ ret.close = function(problem) {
+ http_debug("http closing:", problem);
+ channel.close(problem);
+ return ret;
+ };
+
+ active_requests.push(ret);
+ return ret;
+ };
+
+ self.get = function get(path, params, headers) {
+ return self.request({
+ method: "GET",
+ params,
+ path,
+ body: "",
+ headers
+ });
+ };
+
+ self.post = function post(path, body, headers) {
+ headers = headers || { };
+
+ if (is_plain_object(body) || Array.isArray(body)) {
+ body = JSON.stringify(body);
+ if (find_header(headers, "Content-Type") === undefined)
+ headers["Content-Type"] = "application/json";
+ } else if (body === undefined || body === null) {
+ body = "";
+ } else if (typeof body !== "string") {
+ body = String(body);
+ }
+
+ return self.request({
+ method: "POST",
+ path,
+ body,
+ headers
+ });
+ };
+
+ self.close = function close(problem) {
+ const reqs = active_requests.slice();
+ for (let i = 0; i < reqs.length; i++)
+ reqs[i].close(problem);
+ };
+ }
+
+ /* public */
+ cockpit.http = function(endpoint, options) {
+ if (is_plain_object(endpoint) && options === undefined) {
+ options = endpoint;
+ endpoint = undefined;
+ }
+ return new HttpClient(endpoint, options || { });
+ };
+
+ /* ---------------------------------------------------------------------
+ * Permission
+ */
+
+ function check_superuser() {
+ return new Promise((resolve, reject) => {
+ const ch = cockpit.channel({ payload: "null", superuser: "require" });
+ ch.wait()
+ .then(() => resolve(true))
+ .catch(() => resolve(false))
+ .always(() => ch.close());
+ });
+ }
+
+ function Permission(options) {
+ const self = this;
+ event_mixin(self, { });
+
+ const api = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
+ api.addEventListener("changed", maybe_reload);
+
+ function maybe_reload() {
+ if (api.valid && self.allowed !== null) {
+ if (self.allowed != (api.Current != "none"))
+ window.location.reload(true);
+ }
+ }
+
+ self.allowed = null;
+ self.user = options ? options.user : null; // pre-fill for unit tests
+ self.is_superuser = options ? options._is_superuser : null; // pre-fill for unit tests
+
+ let group = null;
+ let admin = false;
+
+ if (options)
+ group = options.group;
+
+ if (options?.admin)
+ admin = true;
+
+ function decide(user) {
+ if (user.id === 0)
+ return true;
+
+ if (group)
+ return !!(user.groups || []).includes(group);
+
+ if (admin)
+ return self.is_superuser;
+
+ if (user.id === undefined)
+ return null;
+
+ return false;
+ }
+
+ if (self.user && self.is_superuser !== null) {
+ self.allowed = decide(self.user);
+ } else {
+ Promise.all([cockpit.user(), check_superuser()])
+ .then(([user, is_superuser]) => {
+ self.user = user;
+ self.is_superuser = is_superuser;
+ const allowed = decide(user);
+ if (self.allowed !== allowed) {
+ self.allowed = allowed;
+ maybe_reload();
+ self.dispatchEvent("changed");
+ }
+ });
+ }
+
+ self.close = function close() {
+ /* no-op for now */
+ };
+ }
+
+ cockpit.permission = function permission(arg) {
+ return new Permission(arg);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Metrics
+ *
+ */
+
+ function MetricsChannel(interval, options_list, cache) {
+ const self = this;
+ event_mixin(self, { });
+
+ if (options_list.length === undefined)
+ options_list = [options_list];
+
+ const channels = [];
+ let following = false;
+
+ self.series = cockpit.series(interval, cache, fetch_for_series);
+ self.archives = null;
+ self.meta = null;
+
+ function fetch_for_series(beg, end, for_walking) {
+ if (!for_walking)
+ self.fetch(beg, end);
+ else
+ self.follow();
+ }
+
+ function transfer(options_list, callback, is_archive) {
+ if (options_list.length === 0)
+ return;
+
+ if (!is_archive) {
+ if (following)
+ return;
+ following = true;
+ }
+
+ const options = {
+ payload: "metrics1",
+ interval,
+ source: "internal",
+ ...options_list[0]
+ };
+
+ delete options.archive_source;
+
+ const channel = cockpit.channel(options);
+ channels.push(channel);
+
+ let meta = null;
+ let last = null;
+ let beg;
+
+ channel.addEventListener("close", function(ev, close_options) {
+ if (!is_archive)
+ following = false;
+
+ if (options_list.length > 1 &&
+ (close_options.problem == "not-supported" || close_options.problem == "not-found")) {
+ transfer(options_list.slice(1), callback);
+ } else if (close_options.problem) {
+ if (close_options.problem != "terminated" &&
+ close_options.problem != "disconnected" &&
+ close_options.problem != "authentication-failed" &&
+ (close_options.problem != "not-found" || !is_archive) &&
+ (close_options.problem != "not-supported" || !is_archive)) {
+ console.warn("metrics channel failed: " + close_options.problem);
+ }
+ } else if (is_archive) {
+ if (!self.archives) {
+ self.archives = true;
+ self.dispatchEvent('changed');
+ }
+ }
+ });
+
+ channel.addEventListener("message", function(ev, payload) {
+ const message = JSON.parse(payload);
+
+ /* A meta message? */
+ const message_len = message.length;
+ if (message_len === undefined) {
+ meta = message;
+ let timestamp = 0;
+ if (meta.now && meta.timestamp)
+ timestamp = meta.timestamp + (Date.now() - meta.now);
+ beg = Math.floor(timestamp / interval);
+ callback(beg, meta, null, options_list[0]);
+
+ /* Trigger to outside interest that meta changed */
+ self.meta = meta;
+ self.dispatchEvent('changed');
+
+ /* A data message */
+ } else if (meta) {
+ /* Data decompression */
+ for (let i = 0; i < message_len; i++) {
+ const data = message[i];
+ if (last) {
+ for (let j = 0; j < last.length; j++) {
+ const dataj = data[j];
+ if (dataj === null || dataj === undefined) {
+ data[j] = last[j];
+ } else {
+ const dataj_len = dataj.length;
+ if (dataj_len !== undefined) {
+ const lastj = last[j];
+ const lastj_len = last[j].length;
+ let k;
+ for (k = 0; k < dataj_len; k++) {
+ if (dataj[k] === null)
+ dataj[k] = lastj[k];
+ }
+ for (; k < lastj_len; k++)
+ dataj[k] = lastj[k];
+ }
+ }
+ }
+ }
+ last = data;
+ }
+
+ /* Return the data */
+ callback(beg, meta, message, options_list[0]);
+
+ /* Bump timestamp for the next message */
+ beg += message_len;
+ meta.timestamp += (interval * message_len);
+ }
+ });
+ }
+
+ function drain(beg, meta, message, options) {
+ /* Generate a mapping object if necessary */
+ let mapping = meta.mapping;
+ if (!mapping) {
+ mapping = { };
+ meta.metrics.forEach(function(metric, i) {
+ const map = { "": i };
+ const name = options.metrics_path_names?.[i] ?? metric.name;
+ mapping[name] = map;
+ if (metric.instances) {
+ metric.instances.forEach(function(instance, i) {
+ if (instance === "")
+ instance = "/";
+ map[instance] = { "": i };
+ });
+ }
+ });
+ meta.mapping = mapping;
+ }
+
+ if (message)
+ self.series.input(beg, message, mapping);
+ }
+
+ self.fetch = function fetch(beg, end) {
+ const timestamp = beg * interval - Date.now();
+ const limit = end - beg;
+
+ const archive_options_list = [];
+ for (let i = 0; i < options_list.length; i++) {
+ if (options_list[i].archive_source) {
+ archive_options_list.push({
+ ...options_list[i],
+ source: options_list[i].archive_source,
+ timestamp,
+ limit
+ });
+ }
+ }
+
+ transfer(archive_options_list, drain, true);
+ };
+
+ self.follow = function follow() {
+ transfer(options_list, drain);
+ };
+
+ self.close = function close(options) {
+ const len = channels.length;
+ if (self.series)
+ self.series.close();
+
+ for (let i = 0; i < len; i++)
+ channels[i].close(options);
+ };
+ }
+
+ cockpit.metrics = function metrics(interval, options) {
+ return new MetricsChannel(interval, options);
+ };
+
+ /* ---------------------------------------------------------------------
+ * Ooops handling.
+ *
+ * If we're embedded, send oops to parent frame. Since everything
+ * could be broken at this point, just do it manually, without
+ * involving cockpit.transport or any of that logic.
+ */
+
+ cockpit.oops = function oops() {
+ if (window.parent !== window && window.name.indexOf("cockpit1:") === 0)
+ window.parent.postMessage("\n{ \"command\": \"oops\" }", transport_origin);
+ };
+
+ const old_onerror = window.onerror;
+ window.onerror = function(msg, url, line) {
+ // Errors with url == "" are not logged apparently, so let's
+ // not show the "Oops" for them either.
+ if (url != "")
+ cockpit.oops();
+ if (old_onerror)
+ return old_onerror(msg, url, line);
+ return false;
+ };
+
+ return cockpit;
+}
+
+// Register cockpit object as global, so that it can be used without ES6 modules
+// we need to do that here instead of in pkg/base1/cockpit.js, so that po.js can access cockpit already
+window.cockpit = factory();
+
+export default window.cockpit;
diff --git a/pkg/lib/console.css b/pkg/lib/console.css
new file mode 100644
index 0000000..d2ad1e8
--- /dev/null
+++ b/pkg/lib/console.css
@@ -0,0 +1,16 @@
+@import "xterm/css/xterm.css";
+
+.console-ct > .terminal {
+ display: flex;
+ block-size: 100%;
+}
+
+.terminal:focus .terminal-cursor {
+ border: none;
+ animation: blink 1s step-end infinite;
+}
+
+/* Ensure the console fits to its container (and doesn't attempt to go beyond the limits) */
+.xterm-screen, .xterm-viewport {
+ inline-size: auto !important;
+}
diff --git a/pkg/lib/context-menu.scss b/pkg/lib/context-menu.scss
new file mode 100644
index 0000000..1f1c7df
--- /dev/null
+++ b/pkg/lib/context-menu.scss
@@ -0,0 +1,20 @@
+.contextMenu {
+ position: fixed;
+ /* xterm accessibility tree has z-index 100 and we need to be in front of it
+ * to be able to handle mouse events.
+ */
+ z-index: 101;
+ /* Move the menu under the mouse */
+ transform: translate(calc(-1 * var(--pf-v5-global--spacer--sm)), calc(-1 * var(--pf-v5-global--spacer--sm)));
+
+ &Option .pf-v5-c-menu__item-text {
+ display: flex;
+ gap: var(--pf-v5-global--spacer--sm);
+ justify-content: space-between;
+ min-inline-size: 10rem;
+ }
+
+ &Shortcut {
+ opacity: 0.75;
+ }
+}
diff --git a/pkg/lib/credentials-ssh-private-keys.sh b/pkg/lib/credentials-ssh-private-keys.sh
new file mode 100644
index 0000000..e6ffc84
--- /dev/null
+++ b/pkg/lib/credentials-ssh-private-keys.sh
@@ -0,0 +1,34 @@
+#!/bin/sh
+set -u
+
+# The first thing we do is list loaded keys
+loaded=$(ssh-add -L)
+result="$?"
+
+set -e
+
+printf "$loaded"
+
+# Get info for each loaded key
+# ssh-keygen -l -f - is not
+# supported everywhere so use tempfile
+if [ $result -eq 0 ]; then
+ tempfile=$(mktemp)
+ echo "$loaded" | while read line; do
+ echo "$line" > "$tempfile"
+ printf "\v%s\v\v" "$line"
+ ssh-keygen -l -f "$tempfile" || true
+ done
+ rm $tempfile
+fi
+
+# Try to list keys in this directory
+cd "$1" || exit 0
+
+# After that each .pub file gets its on set of blocks
+for file in *.pub; do
+ printf "\v"
+ cat "$file"
+ printf "\v%s\v" "$file"
+ ssh-keygen -l -f "$file" || true
+done
diff --git a/pkg/lib/credentials-ssh-remove-key.sh b/pkg/lib/credentials-ssh-remove-key.sh
new file mode 100644
index 0000000..39ac038
--- /dev/null
+++ b/pkg/lib/credentials-ssh-remove-key.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+set -eu
+
+tempfile=$(mktemp)
+echo "$1" > "$tempfile"
+ret=0
+ssh-add -d "$tempfile" || ret=1
+rm "$tempfile"
+exit $ret
diff --git a/pkg/lib/credentials.js b/pkg/lib/credentials.js
new file mode 100644
index 0000000..d4a12aa
--- /dev/null
+++ b/pkg/lib/credentials.js
@@ -0,0 +1,344 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+import lister from "credentials-ssh-private-keys.sh";
+import remove_key from "credentials-ssh-remove-key.sh";
+
+const _ = cockpit.gettext;
+
+function Keys() {
+ const self = this;
+
+ self.path = null;
+ self.items = { };
+
+ let watch = null;
+ let proc = null;
+ let timeout = null;
+
+ cockpit.event_target(this);
+
+ cockpit.user()
+ .then(user => {
+ self.path = user.home + '/.ssh';
+ refresh();
+ });
+
+ function refresh() {
+ function on_message(ev, payload) {
+ const item = JSON.parse(payload);
+ const name = item.path;
+ if (name && name.indexOf("/") === -1 && name.slice(-4) === ".pub") {
+ if (item.event === "present" || item.event === "created" ||
+ item.event === "changed" || item.event === "deleted") {
+ window.clearInterval(timeout);
+ timeout = window.setTimeout(refresh, 100);
+ }
+ }
+ }
+
+ function on_close(ev, data) {
+ watch.removeEventListener("close", on_close);
+ watch.removeEventListener("message", on_message);
+ if (!data.problem || data.problem == "not-found") {
+ watch = null; /* Watch again */
+ } else {
+ console.warn("couldn't watch " + self.path + ": " + (data.message || data.problem));
+ watch = false; /* Don't watch again */
+ }
+ }
+
+ if (watch === null) {
+ watch = cockpit.channel({ payload: "fswatch1", path: self.path });
+ watch.addEventListener("close", on_close);
+ watch.addEventListener("message", on_message);
+ }
+
+ if (proc)
+ return;
+
+ window.clearTimeout(timeout);
+ timeout = null;
+
+ proc = cockpit.script(lister, [self.path], { err: "message" });
+ proc
+ .then(data => process(data))
+ .catch(ex => console.warn("failed to list keys in home directory: " + ex.message))
+ .finally(() => {
+ proc = null;
+
+ if (!timeout)
+ timeout = window.setTimeout(refresh, 5000);
+ });
+ }
+
+ function process(data) {
+ const blocks = data.split('\v');
+ let key;
+ const items = { };
+
+ /* First block is the data from ssh agent */
+ blocks[0].trim().split("\n")
+ .forEach(function(line) {
+ key = parse_key(line, items);
+ if (key)
+ key.loaded = true;
+ });
+
+ /* Next come individual triples of blocks */
+ blocks.slice(1).forEach(function(block, i) {
+ switch (i % 3) {
+ case 0:
+ key = parse_key(block, items);
+ break;
+ case 1:
+ if (key) {
+ block = block.trim();
+ if (block.slice(-4) === ".pub")
+ key.name = block.slice(0, -4);
+ else if (block)
+ key.name = block;
+ else
+ key.agent_only = true;
+ }
+ break;
+ case 2:
+ if (key)
+ parse_info(block, key);
+ break;
+ }
+ });
+
+ self.items = items;
+ self.dispatchEvent("changed");
+ }
+
+ function parse_key(line, items) {
+ const parts = line.trim().split(" ");
+ let id, type, comment;
+
+ /* SSHv1 keys */
+ if (!isNaN(parseInt(parts[0], 10))) {
+ id = parts[2];
+ type = "RSA1";
+ comment = parts.slice(3).join(" ");
+ } else if (parts[0].indexOf("ssh-") === 0) {
+ id = parts[1];
+ type = parts[0].substring(4).toUpperCase();
+ comment = parts.slice(2).join(" ");
+ } else if (parts[0].indexOf("ecdsa-") === 0) {
+ id = parts[1];
+ type = "ECDSA";
+ comment = parts.slice(2).join(" ");
+ } else {
+ return;
+ }
+
+ let key = items[id];
+ if (!key)
+ key = items[id] = { };
+
+ key.type = type;
+ key.comment = comment;
+ key.data = line;
+ return key;
+ }
+
+ function parse_info(line, key) {
+ const parts = line.trim().split(" ")
+ .filter(n => !!n);
+
+ key.size = parseInt(parts[0], 10);
+ if (isNaN(key.size))
+ key.size = null;
+
+ key.fingerprint = parts[1];
+
+ if (!key.name && parts[2] && parts[2].indexOf("/") !== -1)
+ key.name = parts[2];
+ }
+
+ function ensure_ssh_directory(file) {
+ return cockpit.script('dir=$(dirname "$1"); test -e "$dir" || mkdir -m 700 "$dir"', [file]);
+ }
+
+ function run_keygen(file, new_type, old_pass, new_pass, two_pass) {
+ const old_exps = [/.*Enter old passphrase: $/];
+ const new_exps = [/.*Enter passphrase.*/, /.*Enter new passphrase.*/, /.*Enter same passphrase again: $/];
+ const bad_exps = [/.*failed: passphrase is too short.*/];
+
+ return new Promise((resolve, reject) => {
+ let buffer = "";
+ let sent_new = false;
+ let failure = _("No such file or directory");
+
+ if (new_pass !== two_pass) {
+ reject(new Error(_("The passwords do not match.")));
+ return;
+ }
+
+ // Exactly one of new_type or old_pass must be given
+ console.assert((new_type == null) != (old_pass == null));
+
+ const cmd = ["ssh-keygen", "-f", file];
+ if (new_type)
+ cmd.push("-t", new_type);
+ else
+ cmd.push("-p");
+
+ const proc = cockpit.spawn(cmd, { pty: true, environ: ["LC_ALL=C"], err: "out", directory: self.path });
+
+ const timeout = window.setTimeout(() => {
+ failure = _("Prompting via ssh-keygen timed out");
+ proc.close("terminated");
+ }, 10 * 1000);
+
+ proc
+ .stream(data => {
+ buffer += data;
+ if (old_pass) {
+ for (let i = 0; i < old_exps.length; i++) {
+ if (old_exps[i].test(buffer)) {
+ buffer = "";
+ failure = _("Old password not accepted");
+ proc.input(old_pass + "\n", true);
+ return;
+ }
+ }
+ }
+
+ for (let i = 0; i < new_exps.length; i++) {
+ if (new_exps[i].test(buffer)) {
+ buffer = "";
+ proc.input(new_pass + "\n", true);
+ failure = _("Failed to change password");
+ sent_new = true;
+ return;
+ }
+ }
+
+ if (sent_new) {
+ for (let i = 0; i < bad_exps.length; i++) {
+ if (bad_exps[i].test(buffer)) {
+ failure = _("New password was not accepted");
+ return;
+ }
+ }
+ }
+ })
+ .then(resolve)
+ .catch(ex => {
+ if (ex.exit_status)
+ ex = new Error(failure);
+ reject(ex);
+ })
+ .finally(() => window.clearInterval(timeout));
+ });
+ }
+
+ self.change = function change(name, old_pass, new_pass, two_pass) {
+ return run_keygen(name, null, old_pass, new_pass, two_pass);
+ };
+
+ self.create = function create(name, type, new_pass, two_pass) {
+ return ensure_ssh_directory(name)
+ .then(() => run_keygen(name, type, null, new_pass, two_pass));
+ };
+
+ self.get_pubkey = function get_pubkey(name) {
+ return cockpit.file(name + ".pub").read();
+ };
+
+ self.load = function(name, password) {
+ const ask_exp = /.*Enter passphrase for .*/;
+ const perm_exp = /.*UNPROTECTED PRIVATE KEY FILE.*/;
+ const bad_exp = /.*Bad passphrase.*/;
+
+ let buffer = "";
+ let output = "";
+ let failure = _("Not a valid private key");
+ let sent_password = false;
+
+ return new Promise((resolve, reject) => {
+ const proc = cockpit.spawn(["ssh-add", name],
+ { pty: true, environ: ["LC_ALL=C"], err: "out", directory: self.path });
+
+ const timeout = window.setTimeout(() => {
+ failure = _("Prompting via ssh-add timed out");
+ proc.close("terminated");
+ }, 10 * 1000);
+
+ proc
+ .stream(data => {
+ buffer += data;
+ output += data;
+ if (perm_exp.test(buffer)) {
+ failure = _("Invalid file permissions");
+ buffer = "";
+ } else if (ask_exp.test(buffer)) {
+ buffer = "";
+ failure = _("Password not accepted");
+ proc.input(password + "\n", true);
+ sent_password = true;
+ } else if (bad_exp.test(buffer)) {
+ buffer = "";
+ proc.input("\n", true);
+ }
+ })
+ .then(() => {
+ refresh();
+ resolve();
+ })
+ .catch(ex => {
+ console.log(output);
+ if (ex.exit_status)
+ ex = new Error(failure);
+
+ ex.sent_password = sent_password;
+ reject(ex);
+ })
+ .finally(() => window.clearInterval(timeout));
+ });
+ };
+
+ self.unload = function unload(key) {
+ const options = { pty: true, err: "message", directory: self.path };
+
+ const proc = (key.name && !key.agent_only)
+ ? cockpit.spawn(["ssh-add", "-d", key.name], options)
+ : cockpit.script(remove_key, [key.data], options);
+
+ return proc.then(refresh);
+ };
+
+ self.close = function close() {
+ if (watch)
+ watch.close();
+ if (proc)
+ proc.close();
+ window.clearTimeout(timeout);
+ timeout = null;
+ };
+}
+
+export function keys_instance() {
+ return new Keys();
+}
diff --git a/pkg/lib/ct-card.scss b/pkg/lib/ct-card.scss
new file mode 100644
index 0000000..114c659
--- /dev/null
+++ b/pkg/lib/ct-card.scss
@@ -0,0 +1,63 @@
+@use "_global-variables.scss" as *;
+
+/* Rely on the margin from the Card for spacing */
+.ct-card.pf-v5-c-card .table {
+ margin-block-end: 0;
+}
+
+// FIXME: Once PF4 provides a property for removing padding: https://github.com/patternfly/patternfly-react/issues/5606
+.ct-card.pf-v5-c-card .pf-v5-c-card__body.contains-list {
+ padding-inline: 0;
+ padding-block-end: 0;
+
+ > .pf-v5-c-table > :last-child > tr:last-child {
+ border-block-end: none;
+ }
+
+ // Remove excess padding from compact tables toggles
+ // And adjust the padding to left align the toggles with the card header
+ > .pf-v5-c-table {
+ .pf-v5-c-table__toggle {
+ padding-inline-start: 0;
+
+ > .pf-v5-c-button {
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+ }
+ }
+ }
+}
+
+.ct-card.pf-v5-c-card .pf-v5-c-card__title-text {
+ font-weight: normal;
+ font-size: var(--pf-v5-global--FontSize--2xl);
+}
+
+// Remove excess top padding from top-level empty state in cards,
+// as card headers already add enough space
+.ct-card > .pf-v5-c-card__body > .pf-v5-c-empty-state {
+ --pf-v5-c-empty-state__body--MarginTop: 0;
+ padding-block: 0 var(--pf-v5-global--spacer--md);
+}
+
+.ct-cards-grid {
+ --ct-grid-columns: 2;
+ --pf-v5-l-gallery--GridTemplateColumns: repeat(var(--ct-grid-columns), 1fr);
+
+ > .pf-v5-c-card:not(.ct-card-info) {
+ // Extend all non-info cards to be full width;
+ // let ct-card-info fit 1 column of the grid
+ grid-column: 1 / -1;
+ }
+
+ @media screen and (max-width: $pf-v5-global--breakpoint--lg) {
+ // Shrink to 1 column when space is constrained
+ --ct-grid-columns: 1;
+ }
+}
+
+// Remove redundant padding from embedded toolbars (handled by header)
+// Toolbars in card headers are not a common scenario so no need to upstream this
+.ct-card.pf-v5-c-card .pf-v5-c-toolbar,
+.ct-card.pf-v5-c-card .pf-v5-c-toolbar__content {
+ padding: 0;
+}
diff --git a/pkg/lib/dialogs.jsx b/pkg/lib/dialogs.jsx
new file mode 100644
index 0000000..b63e861
--- /dev/null
+++ b/pkg/lib/dialogs.jsx
@@ -0,0 +1,131 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* DIALOG PRESENTATION PROTOCOL
+ *
+ * Example:
+ *
+ * import { WithDialogs, useDialogs } from "dialogs.jsx";
+ *
+ * const App = () =>
+ * <WithDialogs>
+ * <Page>
+ * <ExampleButton />
+ * </Page>
+ * </WithDialogs>;
+ *
+ * const ExampleButton = () => {
+ * const Dialogs = useDialogs();
+ * return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
+ * };
+ *
+ * const MyDialog = () => {
+ * const Dialogs = useDialogs();
+ * return (
+ * <Modal title="My dialog"
+ * isOpen
+ * onClose={Dialogs.close}>
+ * <p>Hello!</p>
+ * </Modal>);
+ * };
+ *
+ * This does two things: It maintains the state of whether the dialog
+ * is open, and it does it high up in the DOM, in a stable place.
+ * Even if ExampleButton is no longer part of the DOM, the dialog will
+ * stay open and remain useable.
+ *
+ * The "WithDialogs" component enables all its children to show
+ * dialogs. Such a dialog will stay open as long as the WithDialogs
+ * component itself is mounted. Thus, you should put the WithDialogs
+ * component somewhere high up in your component tree, maybe even as
+ * the very top-most component.
+ *
+ * If your Cockpit application has multiple pages and navigation
+ * between these pages is controlled by the browser URL, then each of
+ * these pages should have its own WithDialogs wrapper. This way, a
+ * dialog opened on one page closes when the user navigates away from
+ * that page. To make sure that React maintains separate states for
+ * WithDialogs components, give them unique "key" properties.
+ *
+ * A component that wants to show a dialogs needs to get hold of the
+ * current "Dialogs" context and then call it's "show" method. For a
+ * function component the Dialogs context is returned by
+ * "useDialogs()", as shown above in the example.
+ *
+ * A class component can declare a static context type and then use
+ * "this.context" to find the Dialogs object:
+ *
+ * import { DialogsContext } from "dialogs.jsx";
+ *
+ * class ExampleButton extends React.Component {
+ * static contextType = DialogsContext;
+ *
+ * function render() {
+ * const Dialogs = this.context;
+ * return <Button onClick={() => Dialogs.show(<MyDialog />)}>Open dialog</Button>;
+ * }
+ * }
+ *
+ *
+ * - Dialogs.show(component)
+ *
+ * Calling "Dialogs.show" will render the given component as a direct
+ * child of the inner-most enclosing "WithDialogs" component. The
+ * component is of course intended to be a dialog, such as
+ * Patternfly's "Modal". There is only ever one of these; a second
+ * call to "show" will remove the previously rendered component.
+ * Passing "null" will remove the currently rendered componenet, if any.
+ *
+ * - Dialogs.close()
+ *
+ * Same as "Dialogs.show(null)".
+ */
+
+import React, { useContext, useRef, useState } from "react";
+
+export const DialogsContext = React.createContext();
+export const useDialogs = () => useContext(DialogsContext);
+
+export const WithDialogs = ({ children }) => {
+ const is_open = useRef(false); // synchronous
+ const [dialog, setDialog] = useState(null);
+
+ const Dialogs = {
+ show: component => {
+ if (component && is_open.current)
+ console.error("Dialogs.show() called for",
+ JSON.stringify(component),
+ "while a dialog is already open:",
+ JSON.stringify(dialog));
+ is_open.current = !!component;
+ setDialog(component);
+ },
+ close: () => {
+ is_open.current = false;
+ setDialog(null);
+ },
+ isActive: () => dialog !== null
+ };
+
+ return (
+ <DialogsContext.Provider value={Dialogs}>
+ {children}
+ {dialog}
+ </DialogsContext.Provider>);
+};
diff --git a/pkg/lib/esbuild-cleanup-plugin.js b/pkg/lib/esbuild-cleanup-plugin.js
new file mode 100644
index 0000000..a3a7555
--- /dev/null
+++ b/pkg/lib/esbuild-cleanup-plugin.js
@@ -0,0 +1,17 @@
+import fs from 'fs';
+import path from 'path';
+
+// always start with a fresh dist/ directory, to change between development and production, or clean up gzipped files
+export const cleanPlugin = ({ outdir = './dist', subdir = '' } = {}) => ({
+ name: 'clean-dist',
+ setup(build) {
+ build.onStart(() => {
+ try {
+ fs.rmSync(path.resolve(outdir, subdir), { recursive: true });
+ } catch (e) {
+ if (e.code !== 'ENOENT')
+ throw e;
+ }
+ });
+ }
+});
diff --git a/pkg/lib/esbuild-common.js b/pkg/lib/esbuild-common.js
new file mode 100644
index 0000000..0ef5265
--- /dev/null
+++ b/pkg/lib/esbuild-common.js
@@ -0,0 +1,33 @@
+import { replace } from 'esbuild-plugin-replace';
+import { sassPlugin } from 'esbuild-sass-plugin';
+
+// List of directories to use when resolving import statements
+const nodePaths = ['pkg/lib'];
+
+export const esbuildStylesPlugins = [
+ // Redefine grid breakpoints to count with our shell
+ // See https://github.com/patternfly/patternfly-react/issues/3815 and
+ // [Redefine grid breakpoints] section in pkg/lib/_global-variables.scss for explanation
+ replace({
+ include: /\.css$/,
+ values: {
+ // Do not override the sm breakpoint as for width < 768px the left nav is hidden
+ '768px': '428px',
+ '992px': '652px',
+ '1200px': '876px',
+ '1450px': '1100px',
+ }
+ }),
+ sassPlugin({
+ loadPaths: [...nodePaths, 'node_modules'],
+ quietDeps: true,
+ async transform(source, resolveDir, path) {
+ if (path.includes('patternfly-5-cockpit.scss')) {
+ return source
+ .replace(/url.*patternfly-icons-fake-path.*;/g, 'url("../base1/fonts/patternfly.woff") format("woff");')
+ .replace(/@font-face[^}]*patternfly-fonts-fake-path[^}]*}/g, '');
+ }
+ return source;
+ }
+ }),
+];
diff --git a/pkg/lib/esbuild-compress-plugin.js b/pkg/lib/esbuild-compress-plugin.js
new file mode 100644
index 0000000..b4c4180
--- /dev/null
+++ b/pkg/lib/esbuild-compress-plugin.js
@@ -0,0 +1,50 @@
+/* There is https://www.npmjs.com/package/esbuild-plugin-compress but it does
+ * not work together with our PO plugin, they are incompatible due to requiring
+ * different values for `write:`. We may be able to change our plugins to work
+ * with `write: false`, but this is easy enough to implement ourselves.
+*/
+
+import path from 'path';
+import fs from "fs";
+import util from 'node:util';
+import child_process from 'node:child_process';
+
+const NAME = 'cockpitCompressPlugin';
+
+const exec = util.promisify(child_process.execFile);
+
+const getAllFiles = function(dirPath, arrayOfFiles) {
+ const files = fs.readdirSync(dirPath);
+
+ arrayOfFiles = arrayOfFiles || [];
+
+ files.forEach(function(file) {
+ if (fs.statSync(dirPath + "/" + file).isDirectory()) {
+ arrayOfFiles = getAllFiles(dirPath + "/" + file, arrayOfFiles);
+ } else {
+ arrayOfFiles.push(path.join(dirPath, "/", file));
+ }
+ });
+
+ return arrayOfFiles;
+};
+
+export const cockpitCompressPlugin = ({ subdir = '', exclude = null } = {}) => ({
+ name: NAME,
+ setup(build) {
+ build.onEnd(async () => {
+ const gzipPromises = [];
+ const path = "./dist/" + subdir;
+
+ for await (const dirent of getAllFiles(path)) {
+ if (exclude && exclude.test(dirent))
+ continue;
+ if (dirent.endsWith('.js') || dirent.endsWith('.css')) {
+ gzipPromises.push(exec('gzip', ['-n9', dirent]));
+ }
+ }
+ await Promise.all(gzipPromises);
+ return null;
+ });
+ }
+});
diff --git a/pkg/lib/esbuild-test-html-plugin.js b/pkg/lib/esbuild-test-html-plugin.js
new file mode 100644
index 0000000..8d62384
--- /dev/null
+++ b/pkg/lib/esbuild-test-html-plugin.js
@@ -0,0 +1,28 @@
+import fs from "fs";
+import path from 'path';
+import _ from 'lodash';
+
+const srcdir = process.env.SRCDIR || '.';
+const libdir = path.resolve(srcdir, "pkg", "lib");
+
+export const cockpitTestHtmlPlugin = ({ testFiles }) => ({
+ name: 'CockpitTestHtmlPlugin',
+ setup(build) {
+ build.onEnd(async () => {
+ const data = fs.readFileSync(path.resolve(libdir, "qunit-template.html.in"), "utf8");
+ testFiles.forEach(file => {
+ const test = path.parse(file).name;
+ const output = _.template(data.toString())({
+ title: test,
+ builddir: file.split("/").map(() => "../").join(""),
+ script: test + '.js',
+ });
+ const outdir = './qunit/' + path.dirname(file);
+ const outfile = test + ".html";
+
+ fs.mkdirSync(outdir, { recursive: true });
+ fs.writeFileSync(path.resolve(outdir, outfile), output);
+ });
+ });
+ }
+});
diff --git a/pkg/lib/get-timesync-backend.py b/pkg/lib/get-timesync-backend.py
new file mode 100644
index 0000000..b3a22c2
--- /dev/null
+++ b/pkg/lib/get-timesync-backend.py
@@ -0,0 +1,61 @@
+import os
+import subprocess
+
+# get-timesync-backend - determine which NTP backend unit timedatectl
+# will likely enable.
+
+roots = ["/etc", "/run", "/usr/local", "/usr/lib", "/lib"]
+
+
+def gather_files(name, suffix):
+ # This function creates a list of files in the same order that
+ # systemd will read them in.
+ #
+ # First we collect all files in all root directories. Duplicates
+ # are avoided by only storing files with a basename that hasn't
+ # been seen yet. The roots are processed in order so that files
+ # in /etc override identically named files in /usr, for example.
+ #
+ # The files are stored in a dict with their basename (such as
+ # "10-chrony.list") as the key and their full pathname (such as
+ # "/usr/lib/systemd/ntp-units.d/10-chrony.list") as the value.
+ #
+ # This arrangement allows for easy checks for duplicate basenames
+ # while retaining access to the full pathname later when creating
+ # the final result.
+ #
+ pathname_by_basename = {}
+ for r in roots:
+ dirname = os.path.join(r, name)
+ if os.path.isdir(dirname):
+ for basename in os.listdir(dirname):
+ if basename.endswith(suffix) and basename not in pathname_by_basename:
+ pathname_by_basename[basename] = os.path.join(dirname, basename)
+
+ # Then we create a list of the full pathnames, sorted by their
+ # basenames.
+ #
+ sorted_basenames = sorted(pathname_by_basename.keys())
+ return [pathname_by_basename[basename] for basename in sorted_basenames]
+
+
+def unit_exists(unit):
+ load_state = subprocess.check_output(["systemctl", "show", "--value", "-p", "LoadState", unit],
+ universal_newlines=True).strip()
+ return load_state != "not-found" and load_state != "masked"
+
+
+def first_unit(files):
+ for f in files:
+ with open(f) as c:
+ for ll in c.readlines():
+ w = ll.strip()
+ if w != "" and not w.startswith("#") and unit_exists(w):
+ return w
+ return None
+
+
+unit = first_unit(gather_files("systemd/ntp-units.d", ".list"))
+
+if unit:
+ print(unit)
diff --git a/pkg/lib/hooks.js b/pkg/lib/hooks.js
new file mode 100644
index 0000000..10c9297
--- /dev/null
+++ b/pkg/lib/hooks.js
@@ -0,0 +1,326 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+import { useState, useEffect, useRef, useReducer } from 'react';
+import deep_equal from "deep-equal";
+
+/* HOOKS
+ *
+ * These are some custom React hooks for Cockpit specific things.
+ *
+ * Overview:
+ *
+ * - usePageLocation: For following along with cockpit.location.
+ *
+ * - useLoggedInUser: For accessing information about the currently
+ * logged in user.
+ *
+ * - useFile: For reading and watching files.
+ *
+ * - useObject: For maintaining arbitrary stateful objects that get
+ * created from the properties of a component.
+ *
+ * - useEvent: For reacting to events emitted by arbitrary objects.
+ *
+ * - useInit: For running a function once.
+ *
+ * - useDeepEqualMemo: A utility hook that can help with things that
+ * need deep equal comparisons in places where React only offers
+ * Object identity comparisons, such as with useEffect.
+ */
+
+/* - usePageLocation()
+ *
+ * function Component() {
+ * const location = usePageLocation();
+ * const { path, options } = usePageLocation();
+ *
+ * ...
+ * }
+ *
+ * This returns the current value of cockpit.location and the
+ * component is re-rendered when it changes. "location" is always a
+ * valid object and never null.
+ *
+ * See https://cockpit-project.org/guide/latest/cockpit-location.html
+ */
+
+export function usePageLocation() {
+ const [location, setLocation] = useState(cockpit.location);
+
+ useEffect(() => {
+ function update() { setLocation(cockpit.location) }
+ cockpit.addEventListener("locationchanged", update);
+ return () => cockpit.removeEventListener("locationchanged", update);
+ }, []);
+
+ return location;
+}
+
+/* - useLoggedInUser()
+ *
+ * function Component() {
+ * const user_info = useLoggedInUser();
+ *
+ * ...
+ * }
+ *
+ * "user_info" is the object delivered by cockpit.user(), or null
+ * while that object is not yet available.
+ */
+
+const cockpit_user_promise = cockpit.user();
+let cockpit_user = null;
+cockpit_user_promise.then(user => { cockpit_user = user });
+
+export function useLoggedInUser() {
+ const [user, setUser] = useState(cockpit_user);
+ useEffect(() => { if (!cockpit_user) cockpit_user_promise.then(setUser); }, []);
+ return user;
+}
+
+/* - useDeepEqualMemo(value)
+ *
+ * function Component(options) {
+ * const memo_options = useDeepEqualMemo(options);
+ * useEffect(() => {
+ * const channel = cockpit.channel(..., memo_options);
+ * ...
+ * return () => channel.close();
+ * }, [memo_options]);
+ *
+ * ...
+ * }
+ *
+ * function ParentComponent() {
+ * const options = { superuser: true, host: "localhost" };
+ * return <Component options={options}/>
+ * }
+ *
+ * Sometimes a useEffect hook has a deeply nested object as one of its
+ * dependencies, such as options for a Cockpit channel. However,
+ * React will compare dependency values with Object.is, and would run
+ * the effect hook too often. In the example above, the "options"
+ * variable of Component is a different object on each render
+ * according to Object.is, but we only want to open a new channel when
+ * the value of a field such as "superuser" or "host" has actually
+ * changed.
+ *
+ * A call to useDeepEqualMemo will return some object that is deeply
+ * equal to its argument, and it will continue to return the same
+ * object (according to Object.is) until the parameter is not deeply
+ * equal to it anymore.
+ *
+ * For the example, this means that "memo_options" will always be the
+ * very same object, and the effect hook is only run once. If we
+ * would use "options" directly as a dependency of the effect hook,
+ * the channel would be closed and opened on every render. This is
+ * very inefficient, doesn't give the asynchronous channel time to do
+ * its job, and will also lead to infinite loops when events on the
+ * channel cause re-renders (which in turn will run the effect hook
+ * again, which will cause a new event, ...).
+ */
+
+export function useDeepEqualMemo(value) {
+ const ref = useRef(value);
+ if (!deep_equal(ref.current, value))
+ ref.current = value;
+ return ref.current;
+}
+
+/* - useFile(path, options)
+ * - useFileWithError(path, options)
+ *
+ * function Component() {
+ * const content = useFile("/etc/hostname", { superuser: "try" });
+ * const [content, error] = useFileWithError("/etc/hostname", { superuser: "try" });
+ *
+ * ...
+ * }
+ *
+ * The "path" and "options" parameters are passed unchanged to
+ * cockpit.file(). Thus, if you need to parse the content of the
+ * file, the best way to do that is via the "syntax" option.
+ *
+ * The "content" variable will reflect the content of the file
+ * "/etc/hostname". When the file changes on disk, the component will
+ * be re-rendered with the new content.
+ *
+ * When the file does not exist or there has been some error reading
+ * it, "content" will be false.
+ *
+ * The "error" variable will contain any errors encountered while
+ * reading the file. It is false when there are no errors.
+ *
+ * When the file does not exist, "error" will be false.
+ *
+ * The "content" and "error" variables will be null until the file has
+ * been read for the first time.
+ *
+ * useFile and useFileWithError are pretty much the same. useFile will
+ * hide the exact error from the caller, which makes it slightly
+ * cleaner to use when the exact error is not part of the UI. In the
+ * case of error, useFile will log that error to the console and
+ * return false.
+ */
+
+export function useFileWithError(path, options, hook_options) {
+ const [content_and_error, setContentAndError] = useState([null, null]);
+ const memo_options = useDeepEqualMemo(options);
+ const memo_hook_options = useDeepEqualMemo(hook_options);
+
+ useEffect(() => {
+ const handle = cockpit.file(path, memo_options);
+ handle.watch((data, tag, error) => {
+ setContentAndError([data || false, error || false]);
+ if (!data && memo_hook_options?.log_errors)
+ console.warn("Can't read " + path + ": " + (error ? error.toString() : "not found"));
+ });
+ return handle.close;
+ }, [path, memo_options, memo_hook_options]);
+
+ return content_and_error;
+}
+
+export function useFile(path, options) {
+ const [content] = useFileWithError(path, options, { log_errors: true });
+ return content;
+}
+
+/* - useObject(create, destroy, dependencies, comparators)
+ *
+ * function Component(param) {
+ * const obj = useObject(() => create_object(param),
+ * obj => obj.close(),
+ * [param], [deep_equal])
+ *
+ * ...
+ * }
+ *
+ * This will call "create_object(param)" before the first render of
+ * the component, and will call "obj.close()" after the last render.
+ *
+ * More precisely, create_object will be called as part of the first
+ * call to useObject, i.e., at the very beginning of the first render.
+ *
+ * When "param" changes compared to the previous call to useObject
+ * (according to the deep_equal function in the example above), the
+ * object will also be destroyed and a new one will be created for the
+ * new value of "param" (as part of the call to useObject).
+ *
+ * There is no time when the "obj" variable is null in the example
+ * above; the first render already has a fully created object. This
+ * is an advantage that useObject has over useEffect, which you might
+ * otherwise use to only create objects when dependencies have
+ * changed.
+ *
+ * And unlike useMemo, useObject will run a cleanup function when a
+ * component is removed. Also unlike useMemo, useObject guarantees
+ * that it will not ignore the dependencies.
+ *
+ * The dependencies are an array of values that are by default
+ * compared with Object.is. If you need to use a custom comparator
+ * function instead of Object.is, you can provide a second
+ * "comparators" array that parallels the "dependencies" array. The
+ * values at a given index in the old and new "dependencies" arrays
+ * are compared with the function at the same index in "comparators".
+ */
+
+function deps_changed(old_deps, new_deps, comps) {
+ return (!old_deps || old_deps.length != new_deps.length ||
+ old_deps.findIndex((o, i) => !(comps[i] || Object.is)(o, new_deps[i])) >= 0);
+}
+
+export function useObject(create, destroy, deps, comps) {
+ const ref = useRef(null);
+ const deps_ref = useRef(null);
+ const destroy_ref = useRef(null);
+
+ if (deps_changed(deps_ref.current, deps, comps || [])) {
+ if (ref.current && destroy)
+ destroy(ref.current);
+ ref.current = create();
+ deps_ref.current = deps;
+ }
+
+ destroy_ref.current = destroy;
+ useEffect(() => {
+ return () => destroy_ref.current?.(ref.current);
+ }, []);
+
+ return ref.current;
+}
+
+/* - useEvent(obj, event, handler)
+ *
+ * function Component(proxy) {
+ * useEvent(proxy, "changed");
+ *
+ * ...
+ * }
+ *
+ * The component will be re-rendered whenever "proxy" emits the
+ * "changed" signal. The "proxy" parameter can be null.
+ *
+ * When the optional "handler" is given, it will be called with the
+ * arguments of the event.
+ */
+
+export function useEvent(obj, event, handler) {
+ // We increase a (otherwise unused) state variable whenever the event
+ // happens. That reliably triggers a re-render.
+
+ const [, forceUpdate] = useReducer(x => x + 1, 0);
+
+ useEffect(() => {
+ function update() {
+ if (handler)
+ handler.apply(null, arguments);
+ forceUpdate();
+ }
+
+ obj?.addEventListener(event, update);
+ return () => obj?.removeEventListener(event, update);
+ }, [obj, event, handler]);
+}
+
+/* - useInit(func, deps, comps)
+ *
+ * function Component(arg) {
+ * useInit(() => {
+ * cockpit.spawn([ ..., arg ]);
+ * }, [arg]);
+ *
+ * ...
+ * }
+ *
+ * The function will be called once during the first render, and
+ * whenever "arg" changes.
+ *
+ * "useInit(func, deps, comps)" is the same as "useObject(func, null,
+ * deps, comps)" but if you want to emphasize that you just want to
+ * run a function (instead of creating a object), it is clearer to use
+ * the "useInit" name for that. Also, "deps" are optional for
+ * "useInit" and default to "[]".
+ */
+
+export function useInit(func, deps, comps, destroy = null) {
+ return useObject(func, destroy, deps || [], comps);
+}
diff --git a/pkg/lib/html2po.js b/pkg/lib/html2po.js
new file mode 100755
index 0000000..634ffc4
--- /dev/null
+++ b/pkg/lib/html2po.js
@@ -0,0 +1,235 @@
+#!/usr/bin/env node
+
+/*
+ * Extracts translatable strings from HTML files in the following forms:
+ *
+ * <tag translate>String</tag>
+ * <tag translate context="value">String</tag>
+ * <tag translate="...">String</tag>
+ * <tag translate-attr attr="String"></tag>
+ *
+ * Supports the following angular-gettext compatible forms:
+ *
+ * <translate>String</translate>
+ * <tag translate-plural="Plural">Singular</tag>
+ *
+ * Note that some of the use of the translated may not support all the strings
+ * depending on the code actually using these strings to translate the HTML.
+ */
+
+import fs from 'fs';
+import path from 'path';
+import htmlparser from 'htmlparser';
+import { ArgumentParser } from 'argparse';
+
+function fatal(message, code) {
+ console.log((filename || "html2po") + ": " + message);
+ process.exit(code || 1);
+}
+
+const parser = new ArgumentParser();
+parser.add_argument('-d', '--directory', { help: "Base directory for input files" });
+parser.add_argument('-o', '--output', { help: 'Output file', required: true });
+parser.add_argument('files', { nargs: '+', help: "One or more input files", metavar: "FILE" });
+const args = parser.parse_args();
+
+const input = args.files;
+const entries = { };
+
+/* Filename being parsed and offset of line number */
+let filename = null;
+let offsets = 0;
+
+/* The HTML parser we're using */
+const handler = new htmlparser.DefaultHandler(function(error, dom) {
+ if (error)
+ fatal(error);
+ else
+ walk(dom);
+});
+
+/* Now process each file in turn */
+step();
+
+function step() {
+ filename = input.shift();
+ if (filename === undefined) {
+ finish();
+ return;
+ }
+
+ /* Qualify the filename if necessary */
+ let full = filename;
+ if (args.directory)
+ full = path.join(args.directory, filename);
+
+ fs.readFile(full, { encoding: "utf-8" }, function(err, data) {
+ if (err)
+ fatal(err.message);
+
+ const parser = new htmlparser.Parser(handler, { includeLocation: true });
+ parser.parseComplete(data);
+ step();
+ });
+}
+
+/* Process an array of nodes */
+function walk(children) {
+ if (!children)
+ return;
+
+ children.forEach(function(child) {
+ const line = (child.location || { }).line || 0;
+ const offset = line - 1;
+
+ /* Scripts get their text processed as HTML */
+ if (child.type == 'script' && child.children) {
+ const parser = new htmlparser.Parser(handler, { includeLocation: true });
+
+ /* Make note of how far into the outer HTML file we are */
+ offsets += offset;
+
+ child.children.forEach(function(node) {
+ parser.parseChunk(node.raw);
+ });
+ parser.done();
+
+ offsets -= offset;
+
+ /* Tags get extracted as usual */
+ } else if (child.type == 'tag') {
+ tag(child);
+ }
+ });
+}
+
+/* Process a single loaded tag */
+function tag(node) {
+ let tasks, line, entry;
+ const attrs = node.attribs || { };
+ let nest = true;
+
+ /* Extract translate strings */
+ if ("translate" in attrs || "translatable" in attrs) {
+ tasks = (attrs.translate || attrs.translatable || "yes").split(" ");
+
+ /* Calculate the line location taking into account nested parsing */
+ line = (node.location || { }).line || 0;
+ line += offsets;
+
+ entry = {
+ msgctxt: attrs['translate-context'] || attrs.context,
+ msgid_plural: attrs['translate-plural'],
+ locations: [filename + ":" + line]
+ };
+
+ /* For each thing listed */
+ tasks.forEach(function(task) {
+ const copy = Object.assign({}, entry);
+
+ /* The element text itself */
+ if (task == "yes" || task == "translate") {
+ copy.msgid = extract(node.children);
+ nest = false;
+
+ /* An attribute */
+ } else if (task) {
+ copy.msgid = attrs[task];
+ }
+
+ if (copy.msgid)
+ push(copy);
+ });
+ }
+
+ /* Walk through all the children */
+ if (nest)
+ walk(node.children);
+}
+
+/* Push an entry onto the list */
+function push(entry) {
+ const key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
+ const prev = entries[key];
+ if (prev) {
+ prev.locations = prev.locations.concat(entry.locations);
+ } else {
+ entries[key] = entry;
+ }
+}
+
+/* Extract the given text */
+function extract(children) {
+ if (!children)
+ return null;
+
+ const str = [];
+ children.forEach(function(node) {
+ if (node.type == 'tag' && node.children)
+ str.push(extract(node.children));
+ else if (node.type == 'text' && node.data)
+ str.push(node.data);
+ });
+
+ const msgid = str.join("");
+
+ if (msgid != msgid.trim()) {
+ console.error("FATAL: string contains leading or trailing whitespace:", msgid);
+ process.exit(1);
+ }
+
+ return msgid;
+}
+
+/* Escape a string for inclusion in po file */
+function escape(string) {
+ const bs = string.split('\\')
+ .join('\\\\')
+ .split('"')
+ .join('\\"');
+ return bs.split("\n").map(function(line) {
+ return '"' + line + '"';
+ }).join("\n");
+}
+
+/* Finish by writing out the strings */
+function finish() {
+ const result = [
+ 'msgid ""',
+ 'msgstr ""',
+ '"Project-Id-Version: PACKAGE_VERSION\\n"',
+ '"MIME-Version: 1.0\\n"',
+ '"Content-Type: text/plain; charset=UTF-8\\n"',
+ '"Content-Transfer-Encoding: 8bit\\n"',
+ '"X-Generator: Cockpit html2po\\n"',
+ '',
+ ];
+
+ for (const msgid in entries) {
+ const entry = entries[msgid];
+ result.push('#: ' + entry.locations.join(" "));
+ if (entry.msgctxt)
+ result.push('msgctxt ' + escape(entry.msgctxt));
+ result.push('msgid ' + escape(entry.msgid));
+ if (entry.msgid_plural) {
+ result.push('msgid_plural ' + escape(entry.msgid_plural));
+ result.push('msgstr[0] ""');
+ result.push('msgstr[1] ""');
+ } else {
+ result.push('msgstr ""');
+ }
+ result.push('');
+ }
+
+ const data = result.join('\n');
+ if (!args.output) {
+ process.stdout.write(data);
+ process.exit(0);
+ } else {
+ fs.writeFile(args.output, data, function(err) {
+ if (err)
+ fatal(err.message);
+ process.exit(0);
+ });
+ }
+}
diff --git a/pkg/lib/inotify.py b/pkg/lib/inotify.py
new file mode 100644
index 0000000..84d6768
--- /dev/null
+++ b/pkg/lib/inotify.py
@@ -0,0 +1,72 @@
+#
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import ctypes
+import os
+import struct
+import sys
+
+IN_CLOSE_WRITE = 0x00000008
+IN_MOVED_FROM = 0x00000040
+IN_MOVED_TO = 0x00000080
+IN_CREATE = 0x00000100
+IN_DELETE = 0x00000200
+IN_DELETE_SELF = 0x00000400
+IN_MOVE_SELF = 0x00000800
+IN_IGNORED = 0x00008000
+
+
+class Inotify:
+ def __init__(self):
+ self._libc = ctypes.CDLL(None, use_errno=True)
+ self._get_errno_func = ctypes.get_errno
+
+ self._libc.inotify_init.argtypes = []
+ self._libc.inotify_init.restype = ctypes.c_int
+ self._libc.inotify_add_watch.argtypes = [ctypes.c_int, ctypes.c_char_p,
+ ctypes.c_uint32]
+ self._libc.inotify_add_watch.restype = ctypes.c_int
+ self._libc.inotify_rm_watch.argtypes = [ctypes.c_int, ctypes.c_int]
+ self._libc.inotify_rm_watch.restype = ctypes.c_int
+
+ self.fd = self._libc.inotify_init()
+
+ def add_watch(self, path, mask):
+ path = ctypes.create_string_buffer(path.encode(sys.getfilesystemencoding()))
+ wd = self._libc.inotify_add_watch(self.fd, path, mask)
+ if wd < 0:
+ sys.stderr.write("can't add watch for %s: %s\n" % (path, os.strerror(self._get_errno_func())))
+ return wd
+
+ def rem_watch(self, wd):
+ if self._libc.inotify_rm_watch(self.fd, wd) < 0:
+ sys.stderr.write("can't remove watch: %s\n" % (os.strerror(self._get_errno_func())))
+
+ def process(self, callback):
+ buf = os.read(self.fd, 4096)
+ pos = 0
+ while pos < len(buf):
+ (wd, mask, cookie, name_len) = struct.unpack('iIII', buf[pos:pos + 16])
+ pos += 16
+ (name,) = struct.unpack('%ds' % name_len, buf[pos:pos + name_len])
+ pos += name_len
+ callback(wd, mask, name.decode().rstrip('\0'))
+
+ def run(self, callback):
+ while True:
+ self.process(callback)
diff --git a/pkg/lib/journal.css b/pkg/lib/journal.css
new file mode 100644
index 0000000..23303b1
--- /dev/null
+++ b/pkg/lib/journal.css
@@ -0,0 +1,161 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+.cockpit-log-panel:empty {
+ border: none;
+}
+
+.cockpit-log-panel {
+ overflow-x: unset;
+}
+
+.cockpit-log-panel .panel-body {
+ padding: 0;
+}
+
+.cockpit-log-panel .pf-v5-c-card__body .panel-heading,
+.cockpit-log-panel .panel-body .panel-heading {
+ border-block-start: 0;
+ background-color: var(--ct-color-bg);
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ padding-block: var(--pf-v5-global--spacer--sm);
+ inline-size: auto;
+ color: var(--ct-color-list-text);
+ display: flex;
+}
+
+.cockpit-log-panel .pf-v5-c-card__body .panel-heading {
+ /* Align sub-heading within a PF4 card to the heading of the card */
+ padding-inline-start: var(--pf-v5-global--spacer--lg);
+}
+
+.cockpit-log-panel .panel-body .panel-heading:not(:first-child)::after {
+ content: "\a0";
+ display: block;
+ flex: auto;
+ background: linear-gradient(var(--ct-color-bg) 50%, var(--ct-color-border) calc(50% + 1px), var(--ct-color-bg) calc(50% + 2px));
+ margin-block: 0;
+ margin-inline: 0.5rem 0;
+}
+
+.cockpit-logline {
+ --log-icon: 24px;
+ --log-time: 3rem;
+ --log-message: 1fr;
+ --log-service-min: 0;
+ --log-service: minmax(var(--log-service-min), max-content);
+ background-color: var(--ct-color-list-bg);
+ font-size: var(--font-small);
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ display: grid;
+ grid-template-columns: var(--log-icon) var(--log-time) var(--log-message) var(--log-service);
+ grid-gap: var(--pf-v5-global--spacer--sm);
+ align-items: baseline;
+}
+
+.cockpit-log-panel .cockpit-logline:hover {
+ background-color: var(--ct-color-list-hover-bg);
+ cursor: pointer;
+}
+
+.cockpit-log-panel .cockpit-logline:hover .cockpit-log-message:not(.cockpit-logmsg-reboot) {
+ color: var(--ct-color-list-hover-text);
+ text-decoration: underline;
+}
+
+.cockpit-log-panel .cockpit-logline + .panel-heading {
+ border-block-start-width: 1px;
+}
+
+/* Don't show headers without content */
+.cockpit-log-panel .panel-heading:last-child {
+ display: none !important;
+}
+
+.cockpit-logmsg-reboot {
+ font-style: italic;
+}
+
+.cockpit-log-warning {
+ display: flex;
+ align-self: center;
+ justify-content: center;
+}
+
+.empty-message {
+ inline-size: 100%;
+ color: var(--pf-v5-global--Color--200);
+ display: block;
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ text-align: center;
+}
+
+.cockpit-log-time,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ color: var(--pf-v5-global--Color--200);
+}
+
+.cockpit-log-time {
+ color: var(--pf-v5-global--Color--200);
+ font-family: monospace;
+ font-size: var(--pf-v5-global--FontSize--xs);
+ justify-self: end;
+ white-space: nowrap;
+}
+
+.cockpit-log-message,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ flex: auto;
+}
+
+.cockpit-log-message,
+.cockpit-log-service,
+.cockpit-log-service-reduced {
+ font-size: var(--pf-v5-global--FontSize--sm);
+}
+
+.cockpit-log-service-container > .pf-v5-c-badge {
+ margin-inline-start: var(--pf-v5-global--spacer--xs);
+}
+
+.cockpit-log-service-container {
+ display: flex;
+ align-items: baseline;
+}
+
+@media screen and (max-width: 428px) {
+ .cockpit-logline {
+ /* Remove space for service */
+ --log-service: 0;
+ }
+
+ .cockpit-log-service,
+ .cockpit-log-service-reduced,
+ .cockpit-log-service-container {
+ /* Move service under message */
+ grid-area: 2 / 3;
+ }
+}
diff --git a/pkg/lib/journal.js b/pkg/lib/journal.js
new file mode 100644
index 0000000..7659a77
--- /dev/null
+++ b/pkg/lib/journal.js
@@ -0,0 +1,453 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2015 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import * as timeformat from "timeformat";
+
+const _ = cockpit.gettext;
+
+export const journal = { };
+
+/**
+ * journalctl([match, ...], [options])
+ * @match: any number of journal match strings
+ * @options: an object containing further options
+ *
+ * Load and (by default) stream journal entries as
+ * json objects. This function returns a jQuery deferred
+ * object which delivers the various journal entries.
+ *
+ * The various @match strings are journalctl matches.
+ * Zero, one or more can be specified. They must be in
+ * string format, or arrays of strings.
+ *
+ * The optional @options object can contain the following:
+ * * "count": number of entries to load and/or pre-stream.
+ * Default is 10
+ * * "follow": if set to false just load entries and don't
+ * stream further journal data. Default is true.
+ * * "directory": optional directory to load journal files
+ * * "boot": when set only list entries from this specific
+ * boot id, or if null then the current boot.
+ * * "since": if specified list entries since the date/time
+ * * "until": if specified list entries until the date/time
+ * * "cursor": a cursor to start listing entries from
+ * * "after": a cursor to start listing entries after
+ * * "priority": if specified list entries below the specific priority, inclusive
+ *
+ * Returns a jQuery deferred promise. You can call these
+ * functions on the deferred to handle the responses. Note that
+ * there are additional non-jQuery methods.
+ *
+ * .done(function(entries) { }): Called when done, @entries is
+ * an array of all journal entries loaded. If .stream()
+ * has been invoked then @entries will be empty.
+ * .fail(function(ex) { }): called if the operation fails
+ * .stream(function(entries) { }): called when we receive entries
+ * entries. Called once per batch of journal @entries,
+ * whether following or not.
+ * .stop(): stop following or retrieving entries.
+ */
+
+journal.build_cmd = function build_cmd(/* ... */) {
+ const matches = [];
+ const options = { follow: true };
+ for (let i = 0; i < arguments.length; i++) {
+ const arg = arguments[i];
+ if (typeof arg == "string") {
+ matches.push(arg);
+ } else if (typeof arg == "object") {
+ if (arg instanceof Array) {
+ matches.push.apply(matches, arg);
+ } else {
+ Object.assign(options, arg);
+ break;
+ }
+ } else {
+ console.warn("journal.journalctl called with invalid argument:", arg);
+ }
+ }
+
+ if (options.count === undefined) {
+ if (options.follow)
+ options.count = 10;
+ else
+ options.count = null;
+ }
+
+ const cmd = ["journalctl", "-q"];
+ if (!options.count)
+ cmd.push("--no-tail");
+ else
+ cmd.push("--lines=" + options.count);
+
+ cmd.push("--output=" + (options.output || "json"));
+
+ if (options.directory)
+ cmd.push("--directory=" + options.directory);
+ if (options.boot)
+ cmd.push("--boot=" + options.boot);
+ else if (options.boot !== undefined)
+ cmd.push("--boot");
+ if (options.since)
+ cmd.push("--since=" + options.since);
+ if (options.until)
+ cmd.push("--until=" + options.until);
+ if (options.cursor)
+ cmd.push("--cursor=" + options.cursor);
+ if (options.after)
+ cmd.push("--after=" + options.after);
+ if (options.priority)
+ cmd.push("--priority=" + options.priority);
+ if (options.grep)
+ cmd.push("--grep=" + options.grep);
+
+ /* journalctl doesn't allow reverse and follow together */
+ if (options.reverse)
+ cmd.push("--reverse");
+ else if (options.follow)
+ cmd.push("--follow");
+
+ cmd.push("--");
+ cmd.push.apply(cmd, matches);
+ return cmd;
+};
+
+journal.journalctl = function journalctl(/* ... */) {
+ const cmd = journal.build_cmd.apply(null, arguments);
+
+ const dfd = cockpit.defer();
+ const promise = dfd.promise();
+ let buffer = "";
+ let entries = [];
+ let streamers = [];
+ let interval = null;
+
+ function fire_streamers() {
+ let ents, i;
+ if (streamers.length && entries.length > 0) {
+ ents = entries;
+ entries = [];
+ for (i = 0; i < streamers.length; i++)
+ streamers[i].apply(promise, [ents]);
+ } else {
+ window.clearInterval(interval);
+ interval = null;
+ }
+ }
+
+ const proc = cockpit.spawn(cmd, { batch: 8192, latency: 300, superuser: "try" })
+ .stream(function(data) {
+ if (buffer)
+ data = buffer + data;
+ buffer = "";
+
+ const lines = data.split("\n");
+ const last = lines.length - 1;
+ lines.forEach(function(line, i) {
+ if (i == last) {
+ buffer = line;
+ } else if (line && line.indexOf("-- ") !== 0) {
+ try {
+ entries.push(JSON.parse(line));
+ } catch (e) {
+ console.warn(e, line);
+ }
+ }
+ });
+
+ if (streamers.length && interval === null)
+ interval = window.setInterval(fire_streamers, 300);
+ })
+ .done(function() {
+ fire_streamers();
+ dfd.resolve(entries);
+ })
+ .fail(function(ex) {
+ /* The journalctl command fails when no entries are matched
+ * so we just ignore this status code */
+ if (ex.problem == "cancelled" ||
+ ex.exit_status === 1) {
+ fire_streamers();
+ dfd.resolve(entries);
+ } else {
+ dfd.reject(ex);
+ }
+ })
+ .always(function() {
+ window.clearInterval(interval);
+ });
+
+ promise.stream = function stream(callback) {
+ streamers.push(callback);
+ return this;
+ };
+ promise.stop = function stop() {
+ streamers = [];
+ promise.stopped = true;
+ proc.close("cancelled");
+ };
+ return promise;
+};
+
+journal.printable = function printable(value, key) {
+ if (value === undefined || value === null)
+ return _("[no data]");
+ else if (typeof (value) == "string")
+ return value;
+ else if (value.length !== undefined && value.length <= 1000 && key == "MESSAGE")
+ return new TextDecoder().decode(new Uint8Array(value));
+ else {
+ return _("[binary data]");
+ }
+};
+
+/* Render the journal entries by passing suitable DOM elements back to
+ the caller via the 'output_funcs'.
+
+ Rendering is context aware. It will insert 'reboot' markers, for
+ example, and collapse repeated lines. You can extend the output at
+ the bottom and also at the top.
+
+ A new renderer is created by calling 'journal.renderer' like
+ so:
+
+ const renderer = journal.renderer(funcs);
+
+ You can feed new entries into the renderer by calling various
+ methods on the returned object:
+
+ - renderer.append(journal_entry)
+ - renderer.append_flush()
+ - renderer.prepend(journal_entry)
+ - renderer.prepend_flush()
+
+ A 'journal_entry' is one element of the result array returned by a
+ call to 'Query' with the 'cockpit.journal_fields' as the fields to
+ return.
+
+ Calling 'append' will append the given entry to the end of the
+ output, naturally, and 'prepend' will prepend it to the start.
+
+ The output might lag behind what has been input via 'append' and
+ 'prepend', and you need to call 'append_flush' and 'prepend_flush'
+ respectively to ensure that the output is up-to-date. Flushing a
+ renderer does not introduce discontinuities into the output. You
+ can continue to feed entries into the renderer after flushing and
+ repeated lines will be correctly collapsed across the flush, for
+ example.
+
+ The renderer will call methods of the 'output_funcs' object to
+ produce the desired output:
+
+ - output_funcs.append(rendered)
+ - output_funcs.remove_last()
+ - output_funcs.prepend(rendered)
+ - output_funcs.remove_first()
+
+ The 'rendered' argument is the return value of one of the rendering
+ functions described below. The 'append' and 'prepend' methods
+ should add this element to the output, naturally, and 'remove_last'
+ and 'remove_first' should remove the indicated element.
+
+ If you never call 'prepend' on the renderer, 'output_func.prepend'
+ isn't called either. If you never call 'renderer.prepend' after
+ 'renderer.prepend_flush', then 'output_func.remove_first' will
+ never be called. The same guarantees exist for the 'append' family
+ of functions.
+
+ The actual rendering is also done by calling methods on
+ 'output_funcs':
+
+ - output_funcs.render_line(ident, prio, message, count, time, cursor)
+ - output_funcs.render_day_header(day)
+ - output_funcs.render_reboot_separator()
+*/
+
+journal.renderer = function renderer(output_funcs) {
+ if (!output_funcs.render_line)
+ console.error("Invalid renderer provided");
+
+ function copy_object(o) {
+ const c = { }; for (const p in o) c[p] = o[p]; return c;
+ }
+
+ // A 'entry' object describes a journal entry in formatted form.
+ // It has fields 'bootid', 'ident', 'prio', 'message', 'time',
+ // 'day', all of which are strings.
+
+ function format_entry(journal_entry) {
+ const d = journal_entry.__REALTIME_TIMESTAMP / 1000; // timestamps are in µs
+ return {
+ cursor: journal_entry.__CURSOR,
+ full: journal_entry,
+ day: timeformat.date(d),
+ time: timeformat.time(d),
+ bootid: journal_entry._BOOT_ID,
+ ident: journal_entry.SYSLOG_IDENTIFIER || journal_entry._COMM,
+ prio: journal_entry.PRIORITY,
+ message: journal.printable(journal_entry.MESSAGE, "MESSAGE")
+ };
+ }
+
+ function entry_is_equal(a, b) {
+ return (a && b &&
+ a.day == b.day &&
+ a.bootid == b.bootid &&
+ a.ident == b.ident &&
+ a.prio == b.prio &&
+ a.message == b.message);
+ }
+
+ // A state object describes a line that should be eventually
+ // output. It has an 'entry' field as per description above, and
+ // also 'count', 'last_time', and 'first_time', which record
+ // repeated entries. Additionally:
+ //
+ // line_present: When true, the line has been output already with
+ // some preliminary data. It needs to be removed before
+ // outputting more recent data.
+ //
+ // header_present: The day header has been output preliminarily
+ // before the actual log lines. It needs to be removed before
+ // prepending more lines. If both line_present and
+ // header_present are true, then the header comes first in the
+ // output, followed by the line.
+
+ function render_state_line(state) {
+ return output_funcs.render_line(state.entry.ident,
+ state.entry.prio,
+ state.entry.message,
+ state.count,
+ state.last_time,
+ state.entry.full);
+ }
+
+ // We keep the state of the first and last journal lines,
+ // respectively, in order to collapse repeated lines, and to
+ // insert reboot markers and day headers.
+ //
+ // Normally, there are two state objects, but if only a single
+ // line has been output so far, top_state and bottom_state point
+ // to the same object.
+
+ let top_state, bottom_state;
+
+ top_state = bottom_state = { };
+
+ function start_new_line() {
+ // If we now have two lines, split the state
+ if (top_state === bottom_state && top_state.entry) {
+ top_state = copy_object(bottom_state);
+ }
+ }
+
+ function top_output() {
+ if (top_state.header_present) {
+ output_funcs.remove_first();
+ top_state.header_present = false;
+ }
+ if (top_state.line_present) {
+ output_funcs.remove_first();
+ top_state.line_present = false;
+ }
+ if (top_state.entry) {
+ output_funcs.prepend(render_state_line(top_state));
+ top_state.line_present = true;
+ }
+ }
+
+ function prepend(journal_entry) {
+ const entry = format_entry(journal_entry);
+
+ if (entry_is_equal(top_state.entry, entry)) {
+ top_state.count += 1;
+ top_state.first_time = entry.time;
+ } else {
+ top_output();
+
+ if (top_state.entry) {
+ if (entry.bootid != top_state.entry.bootid)
+ output_funcs.prepend(output_funcs.render_reboot_separator());
+ if (entry.day != top_state.entry.day)
+ output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
+ }
+
+ start_new_line();
+ top_state.entry = entry;
+ top_state.count = 1;
+ top_state.first_time = top_state.last_time = entry.time;
+ top_state.line_present = false;
+ }
+ }
+
+ function prepend_flush() {
+ top_output();
+ if (top_state.entry) {
+ output_funcs.prepend(output_funcs.render_day_header(top_state.entry.day));
+ top_state.header_present = true;
+ }
+ }
+
+ function bottom_output() {
+ if (bottom_state.line_present) {
+ output_funcs.remove_last();
+ bottom_state.line_present = false;
+ }
+ if (bottom_state.entry) {
+ output_funcs.append(render_state_line(bottom_state));
+ bottom_state.line_present = true;
+ }
+ }
+
+ function append(journal_entry) {
+ const entry = format_entry(journal_entry);
+
+ if (entry_is_equal(bottom_state.entry, entry)) {
+ bottom_state.count += 1;
+ bottom_state.last_time = entry.time;
+ } else {
+ bottom_output();
+
+ if (!bottom_state.entry || entry.day != bottom_state.entry.day) {
+ output_funcs.append(output_funcs.render_day_header(entry.day));
+ bottom_state.header_present = true;
+ }
+ if (bottom_state.entry && entry.bootid != bottom_state.entry.bootid)
+ output_funcs.append(output_funcs.render_reboot_separator());
+
+ start_new_line();
+ bottom_state.entry = entry;
+ bottom_state.count = 1;
+ bottom_state.first_time = bottom_state.last_time = entry.time;
+ bottom_state.line_present = false;
+ }
+ }
+
+ function append_flush() {
+ bottom_output();
+ }
+
+ return {
+ prepend,
+ prepend_flush,
+ append,
+ append_flush
+ };
+};
diff --git a/pkg/lib/long-running-process.js b/pkg/lib/long-running-process.js
new file mode 100644
index 0000000..0c085a9
--- /dev/null
+++ b/pkg/lib/long-running-process.js
@@ -0,0 +1,166 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* Manage a long-running precious process that runs independently from a Cockpit
+ * session in a transient systemd service unit. See
+ * examples/long-running-process/README.md for details.
+ *
+ * The unit will run as root, on the system systemd manager, so that every privileged
+ * Cockpit session shares the same unit. The same approach works in principle on
+ * the user's systemd instance, but the current code does not support that as it
+ * is not a common use case for Cockpit.
+ */
+
+/* global cockpit */
+
+// systemd D-Bus API names
+const O_SD_OBJ = "/org/freedesktop/systemd1";
+const I_SD_MGR = "org.freedesktop.systemd1.Manager";
+const I_SD_UNIT = "org.freedesktop.systemd1.Unit";
+const I_DBUS_PROP = "org.freedesktop.DBus.Properties";
+
+/* Possible LongRunningProcess.state values */
+export const ProcessState = {
+ INIT: 'init',
+ STOPPED: 'stopped',
+ RUNNING: 'running',
+ FAILED: 'failed',
+};
+
+export class LongRunningProcess {
+ /* serviceName: systemd unit name to start or reattach to
+ * updateCallback: function that gets called whenever the state changed; first and only
+ * argument is `this` LongRunningProcess instance.
+ */
+ constructor(serviceName, updateCallback) {
+ this.systemdClient = cockpit.dbus("org.freedesktop.systemd1", { superuser: "require" });
+ this.serviceName = serviceName;
+ this.updateCallback = updateCallback;
+ this._setState(ProcessState.INIT);
+ this.startTimestamp = null; // µs since epoch
+ this.terminated = false;
+
+ // Watch for start event of the service
+ this.systemdClient.subscribe({ interface: I_SD_MGR, member: "JobNew" }, (path, iface, signal, args) => {
+ if (args[2] == this.serviceName)
+ this._checkState();
+ });
+
+ // Check if it is already running
+ this._checkState();
+ }
+
+ /* Start long-running process. Only call this in states STOPPED or FAILED.
+ * This runs as root, thus will be shared with all privileged Cockpit sessions.
+ * Return cockpit.spawn promise. You need to handle exceptions, but not success.
+ */
+ run(argv, options) {
+ if (this.state !== ProcessState.STOPPED && this.state !== ProcessState.FAILED)
+ throw new Error(`cannot start LongRunningProcess in state ${this.state}`);
+
+ // no need to directly react to this -- JobNew and _checkState() will pick up when the unit runs
+ return cockpit.spawn(["systemd-run", "--unit", this.serviceName, "--service-type=oneshot", "--no-block", "--"].concat(argv),
+ { superuser: "require", err: "message", ...options });
+ }
+
+ /* Stop long-running process while it is RUNNING, or reset a FAILED one */
+ terminate() {
+ if (this.state !== ProcessState.RUNNING && this.state !== ProcessState.FAILED)
+ throw new Error(`cannot terminate LongRunningProcess in state ${this.state}`);
+
+ /* This sends a SIGTERM to the unit, causing it to go into "failed" state. This would not
+ * happen with `systemd-run -p SuccessExitStatus=0`, but that does not yet work on older
+ * OSes with systemd ≤ 241 So let checkState() know that a failure is due to termination. */
+ this.terminated = true;
+ return this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "StopUnit", [this.serviceName, "replace"], { type: "ss" });
+ }
+
+ /*
+ * below are internal private methods
+ */
+
+ _setState(state) {
+ /* PropertiesChanged often gets fired multiple times with the same values, avoid UI flicker */
+ if (state === this.state)
+ return;
+ this.state = state;
+ this.terminated = false;
+ if (this.updateCallback)
+ this.updateCallback(this);
+ }
+
+ _setStateFromProperties(activeState, stateChangeTimestamp) {
+ switch (activeState) {
+ case 'activating':
+ this.startTimestamp = stateChangeTimestamp;
+ this._setState(ProcessState.RUNNING);
+ break;
+ case 'failed':
+ this.startTimestamp = null; // TODO: can we derive this from InvocationID?
+ if (this.terminated) {
+ /* terminating causes failure; reset that and do not announce it as failed */
+ this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "ResetFailedUnit", [this.serviceName], { type: "s" });
+ } else {
+ this._setState(ProcessState.FAILED);
+ }
+ break;
+ case 'inactive':
+ this._setState(ProcessState.STOPPED);
+ break;
+ case 'deactivating':
+ /* ignore these transitions */
+ break;
+ default:
+ throw new Error(`unexpected state of unit ${this.serviceName}: ${activeState}`);
+ }
+ }
+
+ // check if the transient unit for our command is running
+ _checkState() {
+ this.systemdClient.call(O_SD_OBJ, I_SD_MGR, "GetUnit", [this.serviceName], { type: "s" })
+ .then(([unitObj]) => {
+ /* Some time may pass between getting JobNew and the unit actually getting activated;
+ * we may get an inactive unit here; watch for state changes. This will also update
+ * the UI if the unit stops. */
+ this.subscription = this.systemdClient.subscribe(
+ { interface: I_DBUS_PROP, member: "PropertiesChanged" },
+ (path, iface, signal, args) => {
+ if (path === unitObj && args[1].ActiveState && args[1].StateChangeTimestamp)
+ this._setStateFromProperties(args[1].ActiveState.v, args[1].StateChangeTimestamp.v);
+ });
+
+ this.systemdClient.call(unitObj, I_DBUS_PROP, "GetAll", [I_SD_UNIT], { type: "s" })
+ .then(([props]) => this._setStateFromProperties(props.ActiveState.v, props.StateChangeTimestamp.v))
+ .catch(ex => {
+ throw new Error(`unexpected failure of GetAll(${unitObj}): ${ex.toString()}`);
+ });
+ })
+ .catch(ex => {
+ if (ex.name === "org.freedesktop.systemd1.NoSuchUnit") {
+ if (this.subscription) {
+ this.subscription.remove();
+ this.subscription = null;
+ }
+ this._setState(ProcessState.STOPPED);
+ } else {
+ throw new Error(`unexpected failure of GetUnit(${this.serviceName}): ${ex.toString()}`);
+ }
+ });
+ }
+}
diff --git a/pkg/lib/machine-info.js b/pkg/lib/machine-info.js
new file mode 100644
index 0000000..a5b2119
--- /dev/null
+++ b/pkg/lib/machine-info.js
@@ -0,0 +1,259 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+const _ = cockpit.gettext;
+
+export const cpu_ram_info = () =>
+ cockpit.spawn(["cat", "/proc/meminfo", "/proc/cpuinfo"])
+ .then(text => {
+ const info = { };
+ const memtotal_match = text.match(/MemTotal:[^0-9]*([0-9]+) [kK]B/);
+ const total_kb = memtotal_match && parseInt(memtotal_match[1], 10);
+ if (total_kb)
+ info.memory = total_kb * 1024;
+
+ const available_match = text.match(/MemAvailable:[^0-9]*([0-9]+) [kK]B/);
+ const available_kb = available_match && parseInt(available_match[1], 10);
+ if (available_kb)
+ info.available_memory = available_kb * 1024;
+
+ const swap_match = text.match(/SwapTotal:[^0-9]*([0-9]+) [kK]B/);
+ const swap_total_kb = swap_match && parseInt(swap_match[1], 10);
+ if (swap_total_kb)
+ info.swap = swap_total_kb * 1024;
+
+ let model_match = text.match(/^model name\s*:\s*(.*)$/m);
+ if (!model_match)
+ model_match = text.match(/^cpu\s*:\s*(.*)$/m); // PowerPC
+ if (!model_match)
+ model_match = text.match(/^vendor_id\s*:\s*(.*)$/m); // s390x
+ if (model_match)
+ info.cpu_model = model_match[1];
+
+ info.cpus = 0;
+ const re = /^(processor|cpu number)\s*:/gm;
+ while (re.test(text))
+ info.cpus += 1;
+ return info;
+ });
+
+// https://www.dmtf.org/sites/default/files/standards/documents/DSP0134_2.7.1.pdf
+const chassis_types = [
+ undefined,
+ _("Other"),
+ _("Unknown"),
+ _("Desktop"),
+ _("Low profile desktop"),
+ _("Pizza box"),
+ _("Mini tower"),
+ _("Tower"),
+ _("Portable"),
+ _("Laptop"),
+ _("Notebook"),
+ _("Handheld"),
+ _("Docking station"),
+ _("All-in-one"),
+ _("Sub-Notebook"),
+ _("Space-saving computer"),
+ _("Lunch box"), /* 0x10 */
+ _("Main server chassis"),
+ _("Expansion chassis"),
+ _("Sub-Chassis"),
+ _("Bus expansion chassis"),
+ _("Peripheral chassis"),
+ _("RAID chassis"),
+ _("Rack mount chassis"),
+ _("Sealed-case PC"),
+ _("Multi-system chassis"),
+ _("Compact PCI"), /* 0x1A */
+ _("Advanced TCA"),
+ _("Blade"),
+ _("Blade enclosure"),
+ _("Tablet"),
+ _("Convertible"),
+ _("Detachable"), /* 0x20 */
+ _("IoT gateway"),
+ _("Embedded PC"),
+ _("Mini PC"),
+ _("Stick PC"),
+];
+
+function parseDMIFields(text) {
+ const info = {};
+ text.split("\n").forEach(line => {
+ const sep = line.indexOf(':');
+ if (sep <= 0)
+ return;
+ const file = line.slice(0, sep);
+ const key = file.slice(file.lastIndexOf('/') + 1);
+ let value = line.slice(sep + 1);
+
+ // clean up after lazy OEMs
+ if (value.match(/to be filled by o\.?e\.?m\.?/i))
+ value = "";
+
+ info[key] = value;
+
+ if (key === "chassis_type")
+ info[key + "_str"] = chassis_types[parseInt(value)] || chassis_types[2]; // fall back to "Unknown"
+ });
+ return info;
+}
+
+export function dmi_info() {
+ // the grep often/usually exits with 2, that's okay as long as we find *some* information
+ return cockpit.script("grep -r . /sys/class/dmi/id || true", null,
+ { err: "message", superuser: "try" })
+ .then((output) => parseDMIFields(output));
+}
+
+// decode a binary Uint8Array with a trailing null byte
+function decode_proc_str(s) {
+ return cockpit.utf8_decoder().decode(s.slice(0, -1));
+}
+
+export function devicetree_info() {
+ let model, serial;
+
+ return Promise.all([
+ // these succeed with content === null if files are absent
+ cockpit.file("/proc/device-tree/model", { binary: true }).read()
+ .then(content => { model = content ? decode_proc_str(content) : null }),
+ cockpit.file("/proc/device-tree/serial-number", { binary: true }).read()
+ .then(content => { serial = content ? decode_proc_str(content) : null }),
+ ])
+ .then(() => ({ model, serial }));
+}
+
+/* we expect udev db paragraphs like this:
+ *
+ P: /devices/virtual/mem/null
+ N: null
+ E: DEVMODE=0666
+ E: DEVNAME=/dev/null
+ E: SUBSYSTEM=mem
+*/
+
+const udevPathRE = /^P: (.*)$/;
+const udevPropertyRE = /^E: (\w+)=(.*)$/;
+
+function parseUdevDB(text) {
+ const info = {};
+ text.split("\n\n").forEach(paragraph => {
+ let syspath = null;
+ const props = {};
+
+ paragraph = paragraph.trim();
+ if (!paragraph)
+ return;
+
+ paragraph.split("\n").forEach(line => {
+ let match = line.match(udevPathRE);
+ if (match) {
+ syspath = match[1];
+ } else {
+ match = line.match(udevPropertyRE);
+ if (match)
+ props[match[1]] = match[2];
+ }
+ });
+
+ if (syspath)
+ info[syspath] = props;
+ else
+ console.log("udev database paragraph is missing P:", paragraph);
+ });
+ return info;
+}
+
+export function udev_info() {
+ return cockpit.spawn(["udevadm", "info", "--export-db"], { err: "message" })
+ .then(output => parseUdevDB(output));
+}
+
+const memoryRE = /^([ \w]+): (.*)/;
+
+// Process the dmidecode output and create a mapping of locator to DIMM properties
+function parseMemoryInfo(text) {
+ const info = {};
+ text.split("\n\n").forEach(paragraph => {
+ let locator = null;
+ let bankLocator = null;
+ const props = {};
+ paragraph = paragraph.trim();
+ if (!paragraph)
+ return;
+
+ paragraph.split("\n").forEach(line => {
+ line = line.trim();
+ const match = line.match(memoryRE);
+ if (match)
+ props[match[1]] = match[2];
+ });
+
+ locator = props.Locator;
+ bankLocator = props['Bank Locator'];
+ if (locator)
+ info[bankLocator + locator] = props;
+ });
+ return processMemory(info);
+}
+
+// Select the useful properties to display
+function processMemory(info) {
+ const memoryArray = [];
+
+ for (const dimm in info) {
+ const memoryProperty = info[dimm];
+
+ let memorySize = memoryProperty.Size || _("Unknown");
+ if (memorySize.includes("MB")) {
+ const memorySizeValue = parseInt(memorySize, 10);
+ memorySize = cockpit.format(_("$0 GiB"), memorySizeValue / 1024);
+ }
+
+ let memoryTechnology = memoryProperty["Memory technology"];
+ if (!memoryTechnology || memoryTechnology == "<OUT OF SPEC>")
+ memoryTechnology = _("Unknown");
+
+ let memoryRank = memoryProperty.Rank || _("Unknown");
+ if (memoryRank == 1)
+ memoryRank = _("Single rank");
+ if (memoryRank == 2)
+ memoryRank = _("Dual rank");
+
+ memoryArray.push({
+ locator: (memoryProperty['Bank Locator'] + ': ' + memoryProperty.Locator) || _("Unknown"),
+ technology: memoryTechnology,
+ type: memoryProperty.Type || _("Unknown"),
+ size: memorySize,
+ state: memoryProperty["Total Width"] == "Unknown" ? _("Absent") : _("Present"),
+ rank: memoryRank,
+ speed: memoryProperty.Speed || _("Unknown")
+ });
+ }
+
+ return memoryArray;
+}
+
+export function memory_info() {
+ return cockpit.spawn(["dmidecode", "-t", "memory"], { environ: ["LC_ALL=C"], err: "message", superuser: "try" })
+ .then(output => parseMemoryInfo(output));
+}
diff --git a/pkg/lib/manifest2po.js b/pkg/lib/manifest2po.js
new file mode 100755
index 0000000..8300af4
--- /dev/null
+++ b/pkg/lib/manifest2po.js
@@ -0,0 +1,179 @@
+#!/usr/bin/env node
+
+/*
+ * Extracts translatable strings from manifest.json files.
+ *
+ */
+
+import fs from 'fs';
+import path from 'path';
+import { ArgumentParser } from 'argparse';
+
+function fatal(message, code) {
+ console.log((filename || "manifest2po") + ": " + message);
+ process.exit(code || 1);
+}
+
+const parser = new ArgumentParser();
+parser.add_argument('-d', '--directory', { help: "Base directory for input files" });
+parser.add_argument('-o', '--output', { help: 'Output file', required: true });
+parser.add_argument('files', { nargs: '+', help: "One or more input files", metavar: "FILE" });
+const args = parser.parse_args();
+
+const input = args.files;
+const entries = { };
+
+/* Filename being parsed */
+let filename = null;
+
+/* Now process each file in turn */
+step();
+
+function step() {
+ filename = input.shift();
+ if (filename === undefined) {
+ finish();
+ return;
+ }
+
+ if (path.basename(filename) != "manifest.json")
+ return step();
+
+ /* Qualify the filename if necessary */
+ let full = filename;
+ if (args.directory)
+ full = path.join(args.directory, filename);
+
+ fs.readFile(full, { encoding: "utf-8" }, function(err, data) {
+ if (err)
+ fatal(err.message);
+
+ // There are variables which when not substituted can cause JSON.parse to fail
+ // Dummy replace them. None variable is going to be translated anyway
+ const safe_data = data.replace(/@.+?@/gi, 1);
+ process_manifest(JSON.parse(safe_data));
+
+ return step();
+ });
+}
+
+function process_manifest(manifest) {
+ if (manifest.menu)
+ process_menu(manifest.menu);
+ if (manifest.tools)
+ process_menu(manifest.tools);
+ if (manifest.bridges)
+ process_bridges(manifest.bridges);
+ if (manifest.docs)
+ process_docs(manifest.docs);
+}
+
+function process_keywords(keywords) {
+ keywords.forEach(v => {
+ v.matches.forEach(keyword =>
+ push({
+ msgid: keyword,
+ locations: [filename + ":0"]
+ })
+ );
+ });
+}
+
+function process_docs(docs) {
+ docs.forEach(doc => {
+ push({
+ msgid: doc.label,
+ locations: [filename + ":0"]
+ });
+ });
+}
+
+function process_menu(menu) {
+ for (const m in menu) {
+ if (menu[m].label) {
+ push({
+ msgid: menu[m].label,
+ locations: [filename + ":0"]
+ });
+ }
+ if (menu[m].keywords)
+ process_keywords(menu[m].keywords);
+ if (menu[m].docs)
+ process_docs(menu[m].docs);
+ }
+}
+
+function process_bridges(bridges) {
+ for (const b in bridges) {
+ if (bridges[b].label) {
+ push({
+ msgid: bridges[b].label,
+ locations: [filename + ":0"]
+ });
+ }
+ }
+}
+
+/* Push an entry onto the list */
+function push(entry) {
+ const key = entry.msgid + "\0" + entry.msgid_plural + "\0" + entry.msgctxt;
+ const prev = entries[key];
+ if (prev) {
+ prev.locations = prev.locations.concat(entry.locations);
+ } else {
+ entries[key] = entry;
+ }
+}
+
+/* Escape a string for inclusion in po file */
+function escape(string) {
+ const bs = string.split('\\')
+ .join('\\\\')
+ .split('"')
+ .join('\\"');
+ return bs.split("\n").map(function(line) {
+ return '"' + line + '"';
+ }).join("\n");
+}
+
+/* Finish by writing out the strings */
+function finish() {
+ const result = [
+ 'msgid ""',
+ 'msgstr ""',
+ '"Project-Id-Version: PACKAGE_VERSION\\n"',
+ '"MIME-Version: 1.0\\n"',
+ '"Content-Type: text/plain; charset=UTF-8\\n"',
+ '"Content-Transfer-Encoding: 8bit\\n"',
+ '"X-Generator: Cockpit manifest2po\\n"',
+ '',
+ ];
+
+ for (const msgid in entries) {
+ const entry = entries[msgid];
+ result.push('#: ' + entry.locations.join(" "));
+ if (entry.msgctxt)
+ result.push('msgctxt ' + escape(entry.msgctxt));
+ result.push('msgid ' + escape(entry.msgid));
+ if (entry.msgid_plural) {
+ result.push('msgid_plural ' + escape(entry.msgid_plural));
+ result.push('msgstr[0] ""');
+ result.push('msgstr[1] ""');
+ } else {
+ result.push('msgstr ""');
+ }
+ result.push('');
+ }
+
+ const data = result.join('\n');
+ if (!args.output) {
+ process.stdout.write(data);
+ process.exit(0);
+ } else {
+ fs.writeFile(args.output, data, function(err) {
+ if (err)
+ fatal(err.message);
+ process.exit(0);
+ });
+ }
+}
diff --git a/pkg/lib/menu-select-widget.scss b/pkg/lib/menu-select-widget.scss
new file mode 100644
index 0000000..81778df
--- /dev/null
+++ b/pkg/lib/menu-select-widget.scss
@@ -0,0 +1,35 @@
+// FIXME: Remove this custom implementation once a component exists upstream.
+// PF overrides to fake a multiselect widget (as one does not currently exist in PF4).
+// A menu gives us the interaction we want, but the styling is a bit off.
+// Therefore, we're changing the visuals here locally.
+// PF4 upstream request for multi-select @ https://github.com/patternfly/patternfly/issues/4027
+.ct-menu-select-widget.pf-v5-c-menu {
+ // Divider is silly between the widgets in this context
+ .pf-v5-c-divider {
+ display: none;
+
+ + .pf-v5-c-menu__content {
+ // There should be minimal space between the widgets (replacing the divider)
+ margin-block-start: var(--pf-v5-global--spacer--sm);
+ }
+ }
+
+ .pf-v5-c-menu__content {
+ // An overflow multi-select widget needs an outline
+ border: 1px solid var(--pf-v5-global--BorderColor--100);
+ }
+
+ // Search should not be inset when there's no border containing it
+ .pf-v5-c-menu__search {
+ padding: 0;
+ }
+
+ // Keep the background on a selected item even when it doesn't have
+ // focus, allowing keyboard control to have the only background color
+ // when active but also keep the background color when the list loses
+ // focus (such as when the keyboard or mouse navigates outside,
+ // including initial rendering of the list.
+ .pf-v5-c-menu__list:not(:focus-within) .pf-m-selected {
+ background-color: var(--pf-v5-c-menu__list-item--hover--BackgroundColor);
+ }
+}
diff --git a/pkg/lib/notifications.js b/pkg/lib/notifications.js
new file mode 100644
index 0000000..115f12b
--- /dev/null
+++ b/pkg/lib/notifications.js
@@ -0,0 +1,167 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* NOTIFICATIONS
+
+A page can broadcast notifications to the rest of Cockpit. For
+example, the "Software updates" page can send out a notification when
+it detects that software updates are available. The shell will then
+highlight the menu entry for "Software updates" and the "System"
+overview page will also mention it in its "Operating system" section.
+
+The details are all still experimental and subject to change.
+
+As a first step, there are only simple "page status" notifications.
+When we address "event" style notifications, page status notifications
+might become a special case of them. Or not.
+
+A page status is either null, or a JSON value with the following
+fields:
+
+ - type (string, optional)
+
+ If specified, one of "info", "warning", "error". The shell will put
+ an appropriate icon next to the navigation entry for this page, for
+ example.
+
+ Omitting 'type' means that the page has no special status and is the
+ same as using "null" as the whole status value. This can be used to
+ broadcast values in the 'details' field to other pages without
+ forcing an icon into the navigation menu.
+
+ - title (string, optional)
+
+ A short, human readable, localized description of the status,
+ suitable for a tooltip.
+
+ - details (JSON value, optional)
+
+ An arbitrary value. The "System" overview page might monitor a
+ couple of pages for their status and it will use 'details' to display
+ a richer version of the status than possible with just type and
+ title. The recognized properties are:
+
+ * icon: custom icon name (defaults to standard icon corresponding to type)
+ * pficon: PatternFly icon name; e.g. "enhancement", "bug", "security", "spinner", "check";
+ see get_pficon() in pkg/systemd/page-status.jsx
+ * link: custom link target (defaults to page name); if false, the
+ notification will not be a link
+
+Usage:
+
+ import { page_status } from "notifications";
+
+ - page_status.set_own(STATUS)
+
+ Sets the status of the page making the call, completely overwriting
+ the current status. For example,
+
+ page_status.set_own({
+ type: "info",
+ title: _("Software updates available"),
+ details: {
+ num_updates: 10,
+ num_security_updates: 5
+ }
+ });
+
+ page_status.set_own({
+ type: null
+ title: _("System is up to date"),
+ details: {
+ last_check: 81236457
+ }
+ });
+
+ Calling this function with the same STATUS value multiple times is
+ cheap: only the first call will actually broadcast the new status.
+
+ - page_status.get(PAGE, [HOST])
+
+ Retrieves the current status of page PAGE of HOST. When HOST is
+ omitted, it defaults to the default host of the calling page.
+
+ PAGE is the same string that Cockpit uses in its URLs to identify a
+ page, such as "system/terminal" or "storage".
+
+ Until the page_status object is fully initialized (see 'valid'
+ below), this function will return 'undefined'.
+
+ - page_status.addEventListener("changed", event => { ... })
+
+ The "changed" event is emitted whenever any page status changes.
+
+ - page_status.valid
+
+ The page_status objects needs to initialize itself asynchronously and
+ 'valid' is false until this is done. When 'valid' changes to true, a
+ "changed" event is emitted.
+
+*/
+
+import cockpit from "cockpit";
+import deep_equal from "deep-equal";
+
+class PageStatus {
+ constructor() {
+ cockpit.event_target(this);
+ window.addEventListener("storage", event => {
+ if (event.key == "cockpit:page_status") {
+ this.dispatchEvent("changed");
+ }
+ });
+
+ this.cur_own = null;
+
+ this.valid = false;
+ cockpit.transport.wait(() => {
+ this.valid = true;
+ this.dispatchEvent("changed");
+ });
+ }
+
+ get(page, host) {
+ let page_status;
+
+ if (!this.valid)
+ return undefined;
+
+ if (host === undefined)
+ host = cockpit.transport.host;
+
+ try {
+ page_status = JSON.parse(sessionStorage.getItem("cockpit:page_status"));
+ } catch {
+ return null;
+ }
+
+ if (page_status?.[host])
+ return page_status[host][page] || null;
+ return null;
+ }
+
+ set_own(status) {
+ if (!deep_equal(status, this.cur_own)) {
+ this.cur_own = status;
+ cockpit.transport.control("notify", { page_status: status });
+ }
+ }
+}
+
+export const page_status = new PageStatus();
diff --git a/pkg/lib/os-release.js b/pkg/lib/os-release.js
new file mode 100644
index 0000000..d9aca1c
--- /dev/null
+++ b/pkg/lib/os-release.js
@@ -0,0 +1,38 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+function parse_simple_vars(text) {
+ const res = { };
+ for (const l of text.split('\n')) {
+ const pos = l.indexOf('=');
+ if (pos > 0) {
+ const name = l.substring(0, pos);
+ let val = l.substring(pos + 1);
+ if (val[0] == '"' && val[val.length - 1] == '"')
+ val = val.substring(1, val.length - 1);
+ res[name] = val;
+ }
+ }
+ return res;
+}
+
+/* Return /etc/os-release as object */
+export const read_os_release = () => cockpit.file("/etc/os-release", { syntax: { parse: parse_simple_vars } }).read();
diff --git a/pkg/lib/packagekit.js b/pkg/lib/packagekit.js
new file mode 100644
index 0000000..c11cd32
--- /dev/null
+++ b/pkg/lib/packagekit.js
@@ -0,0 +1,528 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017, 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+import { superuser } from 'superuser';
+
+const _ = cockpit.gettext;
+
+// see https://github.com/PackageKit/PackageKit/blob/main/lib/packagekit-glib2/pk-enum.h
+export const Enum = {
+ EXIT_SUCCESS: 1,
+ EXIT_FAILED: 2,
+ EXIT_CANCELLED: 3,
+ ROLE_REFRESH_CACHE: 13,
+ ROLE_UPDATE_PACKAGES: 22,
+ INFO_UNKNOWN: -1,
+ INFO_LOW: 3,
+ INFO_ENHANCEMENT: 4,
+ INFO_NORMAL: 5,
+ INFO_BUGFIX: 6,
+ INFO_IMPORTANT: 7,
+ INFO_SECURITY: 8,
+ INFO_DOWNLOADING: 10,
+ INFO_UPDATING: 11,
+ INFO_INSTALLING: 12,
+ INFO_REMOVING: 13,
+ INFO_REINSTALLING: 19,
+ INFO_DOWNGRADING: 20,
+ STATUS_WAIT: 1,
+ STATUS_DOWNLOAD: 8,
+ STATUS_INSTALL: 9,
+ STATUS_UPDATE: 10,
+ STATUS_CLEANUP: 11,
+ STATUS_SIGCHECK: 14,
+ STATUS_WAITING_FOR_LOCK: 30,
+ FILTER_INSTALLED: (1 << 2),
+ FILTER_NOT_INSTALLED: (1 << 3),
+ FILTER_NEWEST: (1 << 16),
+ FILTER_ARCH: (1 << 18),
+ FILTER_NOT_SOURCE: (1 << 21),
+ ERROR_ALREADY_INSTALLED: 9,
+ TRANSACTION_FLAG_SIMULATE: (1 << 2),
+};
+
+export const transactionInterface = "org.freedesktop.PackageKit.Transaction";
+
+let _dbus_client = null;
+
+/**
+ * Get PackageKit D-Bus client
+ *
+ * This will get lazily initialized and re-initialized after PackageKit
+ * disconnects (due to a crash or idle timeout).
+ */
+function dbus_client() {
+ if (_dbus_client === null) {
+ _dbus_client = cockpit.dbus("org.freedesktop.PackageKit", { superuser: "try", track: true });
+ _dbus_client.addEventListener("close", () => {
+ console.log("PackageKit went away from D-Bus");
+ _dbus_client = null;
+ });
+ }
+
+ return _dbus_client;
+}
+
+// Reconnect when privileges change
+superuser.addEventListener("changed", () => { _dbus_client = null });
+
+/**
+ * Call a PackageKit method
+ */
+export function call(objectPath, iface, method, args, opts) {
+ return dbus_client().call(objectPath, iface, method, args, opts);
+}
+
+/**
+ * Figure out whether PackageKit is available and usable
+ */
+export function detect() {
+ function dbus_detect() {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.DBus.Properties",
+ "Get", ["org.freedesktop.PackageKit", "VersionMajor"])
+ .then(() => true,
+ () => false);
+ }
+
+ return cockpit.spawn(["findmnt", "-T", "/usr", "-n", "-o", "VFS-OPTIONS"])
+ .then(options => {
+ if (options.split(",").indexOf("ro") >= 0)
+ return false;
+ else
+ return dbus_detect();
+ })
+ .catch(dbus_detect);
+}
+
+/**
+ * Watch a running PackageKit transaction
+ *
+ * transactionPath (string): D-Bus object path of the PackageKit transaction
+ * signalHandlers, notifyHandler: As in method #transaction
+ * Returns: If notifyHandler is set, Cockpit promise that resolves when the watch got set up
+ */
+export function watchTransaction(transactionPath, signalHandlers, notifyHandler) {
+ const subscriptions = [];
+ let notifyReturn;
+ const client = dbus_client();
+
+ // Listen for PackageKit crashes while the transaction runs
+ function onClose(event, ex) {
+ console.warn("PackageKit went away during transaction", transactionPath, ":", JSON.stringify(ex));
+ if (signalHandlers.ErrorCode)
+ signalHandlers.ErrorCode("close", _("PackageKit crashed"));
+ if (signalHandlers.Finished)
+ signalHandlers.Finished(Enum.EXIT_FAILED);
+ }
+ client.addEventListener("close", onClose);
+
+ if (signalHandlers) {
+ Object.keys(signalHandlers).forEach(handler => subscriptions.push(
+ client.subscribe({ interface: transactionInterface, path: transactionPath, member: handler },
+ (path, iface, signal, args) => signalHandlers[handler](...args)))
+ );
+ }
+
+ if (notifyHandler) {
+ notifyReturn = client.watch(transactionPath);
+ subscriptions.push(notifyReturn);
+ client.addEventListener("notify", reply => {
+ const iface = reply?.detail?.[transactionPath]?.[transactionInterface];
+ if (iface)
+ notifyHandler(iface, transactionPath);
+ });
+ }
+
+ // unsubscribe when transaction finished
+ subscriptions.push(client.subscribe(
+ { interface: transactionInterface, path: transactionPath, member: "Finished" },
+ () => {
+ subscriptions.map(s => s.remove());
+ client.removeEventListener("close", onClose);
+ })
+ );
+
+ return notifyReturn;
+}
+
+/**
+ * Run a PackageKit transaction
+ *
+ * method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
+ * If undefined, only a transaction will be created without calling a method on it
+ * arglist (array): "in" arguments of @method
+ * signalHandlers (object): maps PackageKit.Transaction signal names to handlers
+ * notifyHandler (function): handler for https://cockpit-project.org/guide/latest/cockpit-dbus.html#cockpit-dbus-onnotify
+ * signals, called on property changes with (changed_properties, transaction_path)
+ * Returns: Promise that resolves with transaction path on success, or rejects on an error
+ *
+ * Note that most often you don't really need the transaction path, but want to
+ * listen to the "Finished" signal.
+ *
+ * Example:
+ * transaction("GetUpdates", [0], {
+ * Package: (info, packageId, _summary) => { ... },
+ * ErrorCode: (code, details) => { ... },
+ * },
+ * changedProps => { ... } // notify handler
+ * )
+ * .then(transactionPath => { ... })
+ * .catch(ex => { handle exception });
+ */
+export function transaction(method, arglist, signalHandlers, notifyHandler) {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.PackageKit", "CreateTransaction", [])
+ .then(([transactionPath]) => {
+ if (!signalHandlers && !notifyHandler)
+ return transactionPath;
+
+ const watchPromise = watchTransaction(transactionPath, signalHandlers, notifyHandler) || Promise.resolve();
+ return watchPromise.then(() => {
+ if (method) {
+ return call(transactionPath, transactionInterface, method, arglist)
+ .then(() => transactionPath);
+ } else {
+ return transactionPath;
+ }
+ });
+ });
+}
+
+export class TransactionError extends Error {
+ constructor(code, detail) {
+ super(detail);
+ this.detail = detail;
+ this.code = code;
+ }
+}
+
+/**
+ * Run a long cancellable PackageKit transaction
+ *
+ * method (string): D-Bus method name on the https://www.freedesktop.org/software/PackageKit/gtk-doc/Transaction.html interface
+ * arglist (array): "in" arguments of @method
+ * progress_cb: Callback that receives a {waiting, percentage, cancel} object regularly; if cancel is not null, it can
+ * be called to cancel the current transaction. if wait is true, PackageKit is waiting for its lock (i. e.
+ * on another package operation)
+ * signalHandlers, notifyHandler: As in method #transaction, but ErrorCode and Finished are handled internally
+ * Returns: Promise that resolves when the transaction finished successfully, or rejects with TransactionError
+ * on failure.
+ */
+export function cancellableTransaction(method, arglist, progress_cb, signalHandlers) {
+ if (signalHandlers?.ErrorCode || signalHandlers?.Finished)
+ throw Error("cancellableTransaction handles ErrorCode and Finished signals internally");
+
+ return new Promise((resolve, reject) => {
+ let cancelled = false;
+ let status;
+ let allow_wait_status = false;
+ const progress_data = {
+ waiting: false,
+ absolute_percentage: 0,
+ cancel: null
+ };
+
+ function changed(props, transaction_path) {
+ function cancel() {
+ call(transaction_path, transactionInterface, "Cancel", []);
+ cancelled = true;
+ }
+
+ if (progress_cb) {
+ if ("Status" in props)
+ status = props.Status;
+ progress_data.waiting = allow_wait_status && (status === Enum.STATUS_WAIT || status === Enum.STATUS_WAITING_FOR_LOCK);
+ if ("AllowCancel" in props)
+ progress_data.cancel = props.AllowCancel ? cancel : null;
+ if ("Percentage" in props && props.Percentage <= 100)
+ progress_data.absolute_percentage = props.Percentage;
+
+ progress_cb(progress_data);
+ }
+ }
+
+ // We ignore STATUS_WAIT and friends during the first second of a transaction. They
+ // are always reported briefly even when a transaction doesn't really need to wait.
+ window.setTimeout(() => {
+ allow_wait_status = true;
+ changed({});
+ }, 1000);
+
+ transaction(method, arglist,
+ Object.assign({
+ // avoid calling progress_cb after ending the transaction, to avoid flickering cancel buttons
+ ErrorCode: (code, detail) => {
+ progress_cb = null;
+ reject(new TransactionError(cancelled ? "cancelled" : code, detail));
+ },
+ Finished: exit => {
+ progress_cb = null;
+ resolve(exit);
+ },
+ }, signalHandlers || {}),
+ changed)
+ .catch(ex => {
+ progress_cb = null;
+ reject(ex);
+ });
+ });
+}
+
+/**
+ * Check Red Hat subscription-manager if if this is an unregistered RHEL
+ * system. If subscription-manager is not installed or required (not a
+ * Red Hat product), nothing happens.
+ *
+ * callback: Called with a boolean (true: registered, false: not registered)
+ * after querying subscription-manager once, and whenever the value
+ * changes.
+ */
+export function watchRedHatSubscription(callback) {
+ const sm = cockpit.dbus("com.redhat.RHSM1");
+
+ function check() {
+ sm.call(
+ "/com/redhat/RHSM1/Entitlement", "com.redhat.RHSM1.Entitlement", "GetStatus", ["", ""])
+ .then(result => {
+ const status = JSON.parse(result[0]);
+ callback(status.valid);
+ })
+ .catch(ex => console.warn("Failed to query RHEL subscription status:", JSON.stringify(ex)));
+ }
+
+ // check if subscription is required on this system, i.e. whether there are any installed products
+ sm.call("/com/redhat/RHSM1/Products", "com.redhat.RHSM1.Products", "ListInstalledProducts", ["", {}, ""])
+ .then(result => {
+ const products = JSON.parse(result[0]);
+ if (products.length === 0)
+ return;
+
+ // check if this is an unregistered RHEL system
+ sm.subscribe(
+ {
+ path: "/com/redhat/RHSM1/Entitlement",
+ interface: "com.redhat.RHSM1.Entitlement",
+ member: "EntitlementChanged"
+ },
+ () => check()
+ );
+
+ check();
+ })
+ .catch(ex => {
+ if (ex.problem != "not-found")
+ console.warn("Failed to query RHSM products:", JSON.stringify(ex));
+ });
+}
+
+/* Support for installing missing packages.
+ *
+ * First call check_missing_packages to determine whether something
+ * needs to be installed, then call install_missing_packages to
+ * actually install them.
+ *
+ * check_missing_packages resolves to an object that can be passed to
+ * install_missing_packages. It contains these fields:
+ *
+ * - missing_names: Packages that were requested, are currently not installed,
+ * and can be installed.
+ *
+ * - missing_ids: The full PackageKit IDs corresponding to missing_names
+ *
+ * - unavailable_names: Packages that were requested, are currently not installed,
+ * but can't be found in any repository.
+ *
+ * If unavailable_names is empty, a simulated installation of the missing packages
+ * is done and the result also contains these fields:
+ *
+ * - extra_names: Packages that need to be installed as dependencies of
+ * missing_names.
+ *
+ * - remove_names: Packages that need to be removed.
+ *
+ * - download_size: Bytes that need to be downloaded.
+ */
+
+export function check_missing_packages(names, progress_cb) {
+ const data = {
+ missing_ids: [],
+ missing_names: [],
+ unavailable_names: [],
+ };
+
+ if (names.length === 0)
+ return Promise.resolve(data);
+
+ function refresh() {
+ return cancellableTransaction("RefreshCache", [false], progress_cb);
+ }
+
+ function resolve() {
+ const installed_names = { };
+
+ return cancellableTransaction("Resolve",
+ [Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_NEWEST, names],
+ progress_cb,
+ {
+ Package: (info, package_id) => {
+ const parts = package_id.split(";");
+ const repos = parts[3].split(":");
+ if (repos.indexOf("installed") >= 0) {
+ installed_names[parts[0]] = true;
+ } else {
+ data.missing_ids.push(package_id);
+ data.missing_names.push(parts[0]);
+ }
+ },
+ })
+ .then(() => {
+ names.forEach(name => {
+ if (!installed_names[name] && data.missing_names.indexOf(name) == -1)
+ data.unavailable_names.push(name);
+ });
+ return data;
+ });
+ }
+
+ function simulate(data) {
+ data.install_ids = [];
+ data.remove_ids = [];
+ data.extra_names = [];
+ data.remove_names = [];
+
+ if (data.missing_ids.length > 0 && data.unavailable_names.length === 0) {
+ return cancellableTransaction("InstallPackages",
+ [Enum.TRANSACTION_FLAG_SIMULATE, data.missing_ids],
+ progress_cb,
+ {
+ Package: (info, package_id) => {
+ const name = package_id.split(";")[0];
+ if (info == Enum.INFO_REMOVING) {
+ data.remove_ids.push(package_id);
+ data.remove_names.push(name);
+ } else if (info == Enum.INFO_INSTALLING ||
+ info == Enum.INFO_UPDATING) {
+ data.install_ids.push(package_id);
+ if (data.missing_names.indexOf(name) == -1)
+ data.extra_names.push(name);
+ }
+ }
+ })
+ .then(() => {
+ data.missing_names.sort();
+ data.extra_names.sort();
+ data.remove_names.sort();
+ return data;
+ });
+ } else {
+ return data;
+ }
+ }
+
+ function get_details(data) {
+ data.download_size = 0;
+ if (data.install_ids.length > 0) {
+ return cancellableTransaction("GetDetails",
+ [data.install_ids],
+ progress_cb,
+ {
+ Details: details => {
+ if (details.size)
+ data.download_size += details.size.v;
+ }
+ })
+ .then(() => data);
+ } else {
+ return data;
+ }
+ }
+
+ return refresh().then(resolve)
+ .then(simulate)
+ .then(get_details);
+}
+
+/* Check a list of packages whether they are installed.
+ *
+ * This is a lightweight version of check_missing_packages() which does not
+ * refresh, simulates, or retrieves details. It just checks which of the given package
+ * names are already installed, and returns a Set of the missing ones.
+ */
+export function check_uninstalled_packages(names) {
+ const uninstalled = new Set(names);
+
+ if (names.length === 0)
+ return Promise.resolve(uninstalled);
+
+ return cancellableTransaction("Resolve",
+ [Enum.FILTER_ARCH | Enum.FILTER_NOT_SOURCE | Enum.FILTER_INSTALLED, names],
+ null, // don't need progress, this is fast
+ {
+ Package: (info, package_id) => {
+ const parts = package_id.split(";");
+ uninstalled.delete(parts[0]);
+ },
+ })
+ .then(() => uninstalled);
+}
+
+/* Carry out what check_missing_packages has planned.
+ *
+ * In addition to the usual "waiting", "percentage", and "cancel"
+ * fields, the object reported by progress_cb also includes "info" and
+ * "package" from the "Package" signal.
+ */
+
+export function install_missing_packages(data, progress_cb) {
+ if (!data || data.missing_ids.length === 0)
+ return Promise.resolve();
+
+ let last_progress, last_info, last_name;
+
+ function report_progess() {
+ progress_cb({
+ waiting: last_progress.waiting,
+ percentage: last_progress.percentage,
+ cancel: last_progress.cancel,
+ info: last_info,
+ package: last_name
+ });
+ }
+
+ return cancellableTransaction("InstallPackages", [0, data.missing_ids],
+ p => {
+ last_progress = p;
+ report_progess();
+ },
+ {
+ Package: (info, id) => {
+ last_info = info;
+ last_name = id.split(";")[0];
+ report_progess();
+ }
+ });
+}
+
+/**
+ * Get the used backendName in PackageKit.
+ */
+export function getBackendName() {
+ return call("/org/freedesktop/PackageKit", "org.freedesktop.DBus.Properties",
+ "Get", ["org.freedesktop.PackageKit", "BackendName"]);
+}
diff --git a/pkg/lib/page.scss b/pkg/lib/page.scss
new file mode 100644
index 0000000..3da4f61
--- /dev/null
+++ b/pkg/lib/page.scss
@@ -0,0 +1,197 @@
+@use "@patternfly/patternfly/base/patternfly-themes.scss";
+@use "@patternfly/patternfly/patternfly-theme-dark.scss";
+@use "./patternfly/patternfly-5-overrides.scss";
+@import "global-variables";
+
+/* Globally resize headings */
+h1 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--4xl);
+}
+
+h2 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--3xl);
+}
+
+h3 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--2xl);
+}
+
+h4 {
+ --ct-heading-font-size: var(--pf-v5-global--FontSize--lg);
+}
+
+// Only apply a custom font size when a heading does NOT have a PF4 class
+h1, h2, h3, h4 {
+ &:not([class*="pf-"]):not([data-pf-content="true"]) {
+ font-size: var(--ct-heading-font-size);
+ }
+}
+
+/* End of headings resize */
+
+a {
+ cursor: pointer;
+}
+
+.disabled {
+ pointer-events: auto;
+}
+
+.btn {
+ min-block-size: 26px;
+ min-inline-size: 26px;
+}
+
+.btn.disabled, .pf-v5-c-button.disabled {
+ pointer-events: auto;
+}
+
+.btn.disabled:hover, .pf-v5-c-button.disabled:hover {
+ z-index: auto;
+}
+
+.btn-group {
+ /* Fix button groups from wrapping in narrow widths */
+ display: inline-flex;
+}
+
+a.disabled {
+ cursor: not-allowed !important;
+ text-decoration: none;
+ pointer-events: none;
+ color: #8b8d8f;
+}
+
+a.disabled:hover {
+ text-decoration: none;
+}
+
+.highlight-ct {
+ background-color: var(--ct-color-link-hover-bg);
+}
+
+.curtains-ct {
+ inline-size: 100%;
+}
+
+/* Animation of new items */
+.ct-new-item {
+ animation: ctNewRow 4s ease-in;
+}
+
+:root {
+ --ct-animation-new-background: #fdf4dd;
+}
+
+.pf-v5-theme-dark {
+ --ct-animation-new-background: #353428;
+}
+
+/* Animation background is instantly yellow and fades out halfway through */
+@keyframes ctNewRow {
+ 0% {
+ background-color: var(--ct-animation-new-background);
+ }
+
+ 50% {
+ background-color: var(--ct-animation-new-background);
+ }
+}
+
+/* Dialog patterns */
+
+.dialog-wait-ct {
+ /* Right align footer idle messages after the buttons */
+ margin-inline-start: auto;
+ display: flex;
+ column-gap: var(--pf-v5-global--spacer--sm);
+ align-items: center;
+}
+
+:root {
+ /* Cockpit custom colors */
+ --ct-color-light-red: #f8cccc;
+ --ct-color-red-hat-red : #e00;
+
+ // Blend between --pf-v5-global--palette--black-200 and 300
+ --ct-global--palette--black-250: #e6e6e6;
+
+ /* Semantic colors */
+ --ct-color-fg: var(--pf-v5-global--color--100);
+ --ct-color-bg: var(--pf-v5-global--BackgroundColor--100);
+ --ct-color-text: var(--ct-color-fg);
+
+ --ct-color-link : var(--pf-v5-global--active-color--100);
+ --ct-color-link-visited: var(--pf-v5-global--active-color--100);
+
+ --ct-color-subtle-copy: var(--pf-v5-global--disabled-color--100);
+
+ // General border color (semantic shortcut, instead of specifying the color directly)
+ --ct-color-border: var(--pf-v5-global--BorderColor--100);
+
+ // Used for highlighting link blocks (with a light background blue)
+ --ct-color-link-hover-bg : var(--pf-v5-global--palette--light-blue-100);
+
+ /* Colors used for custom lists */
+ // as seen in Journal, Listing, Table widgets and pages like Machines, Updates, Services
+ --ct-color-list-text : var(--ct-color-text);
+ --ct-color-list-link : var(--ct-color-link);
+ --ct-color-list-bg : var(--ct-color-bg);
+ --ct-color-list-border : var(--ct-color-border);
+ --ct-color-list-hover-text : var(--ct-color-link);
+ --ct-color-list-hover-bg : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-hover-border : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-hover-icon : var(--pf-v5-global--palette--light-blue-400);
+ --ct-color-list-selected-text : var(--ct-color-link);
+ --ct-color-list-selected-bg : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-selected-border : var(--pf-v5-global--BackgroundColor--150);
+ --ct-color-list-active-text : var(--pf-v5-global--palette--blue-500);
+ --ct-color-list-active-bg : var(--ct-color-bg);
+ --ct-color-list-active-border : var(--ct-color-list-border);
+ --ct-color-list-critical-bg : var(--pf-v5-global--palette--red-50);
+ --ct-color-list-critical-border : #e6bcbc; // red-500 mixed with white @ 50%
+ --ct-color-list-critical-alert-text: var(--pf-v5-global--palette--red-200);
+}
+
+.pf-v5-theme-dark {
+ --ct-color-list-critical-bg : #261213; // red-100 mixed with black-850 @ 20%
+ --ct-color-list-critical-border : var(--pf-v5-global--danger-color--200);
+ --ct-color-list-critical-alert-text: var(--pf-v5-global--palette--red-8888);
+}
+
+[hidden] { display: none !important; }
+
+// Let PF4 handle the scrolling through page component otherwise we might get double scrollbar
+html:not(.index-page) body {
+ overflow-block: hidden;
+
+ // Ensure UI fills the entire page (and does not run over)
+ .ct-page-fill {
+ block-size: 100% !important;
+ }
+}
+
+.ct-icon-info-circle {
+ color: var(--pf-v5-global--info-color--100);
+}
+
+.ct-icon-exclamation-triangle {
+ color: var(--pf-v5-global--warning-color--100);
+}
+
+.ct-icon-times-circle {
+ color: var(--pf-v5-global--danger-color--100);
+}
+
+// Action buttons in headers add extra space. Offset that with a negative margin
+// to compensate, so headings are always the same height regardless of action
+// buttons or not.
+.pf-v5-c-page__main-breadcrumb .pf-v5-c-button {
+ --offset: calc(-1 * var(--pf-v5-global--spacer--sm));
+ margin-block: var(--offset);
+}
+
+// To be used only from testlib.py for pixel-tests
+main.pixel-test {
+ overflow-y: clip;
+}
diff --git a/pkg/lib/pam_user_parser.js b/pkg/lib/pam_user_parser.js
new file mode 100644
index 0000000..278f710
--- /dev/null
+++ b/pkg/lib/pam_user_parser.js
@@ -0,0 +1,95 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2013 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+function parse_passwd_content(content) {
+ if (!content) {
+ console.warn("Couldn't read /etc/passwd");
+ return [];
+ }
+
+ const ret = [];
+ const lines = content.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i])
+ continue;
+ const column = lines[i].split(':');
+ ret.push({
+ name: column[0],
+ password: column[1],
+ uid: parseInt(column[2], 10),
+ gid: parseInt(column[3], 10),
+ gecos: (column[4] || '').replace(/,*$/, ''),
+ home: column[5] || '',
+ shell: column[6] || '',
+ });
+ }
+
+ return ret;
+}
+
+export const etc_passwd_syntax = {
+ parse: parse_passwd_content
+};
+
+function parse_group_content(content) {
+ // /etc/group file is used to set only secondary groups of users. The primary group is saved in /etc/passwd-
+ content = (content || "").trim();
+ if (!content) {
+ console.warn("Couldn't read /etc/group");
+ return [];
+ }
+
+ const ret = [];
+ const lines = content.split('\n');
+
+ for (let i = 0; i < lines.length; i++) {
+ if (!lines[i])
+ continue;
+ const column = lines[i].split(':');
+ ret.push({
+ name: column[0],
+ password: column[1],
+ gid: parseInt(column[2], 10),
+ userlist: column[3].split(','),
+ });
+ }
+
+ return ret;
+}
+
+export const etc_group_syntax = {
+ parse: parse_group_content
+};
+
+function parse_shells_content(content) {
+ content = (content || "").trim();
+ if (!content) {
+ console.warn("Couldn't read /etc/shells");
+ return [];
+ }
+
+ const lines = content.split('\n');
+
+ return lines.filter(line => !line.includes("#") && line.trim());
+}
+
+export const etc_shells_syntax = {
+ parse: parse_shells_content
+};
diff --git a/pkg/lib/patternfly/_fonts.scss b/pkg/lib/patternfly/_fonts.scss
new file mode 100644
index 0000000..841eee7
--- /dev/null
+++ b/pkg/lib/patternfly/_fonts.scss
@@ -0,0 +1,38 @@
+@mixin printRedHatFont(
+ $weightValue: 400,
+ $weightName: "Regular",
+ $familyName: "RedHatText",
+ $style: "normal",
+ $relative: true
+) {
+ $filePath: "../../static/fonts" + "/" + $familyName + "-" + $weightName;
+
+ @font-face {
+ font-family: $familyName;
+ src: url("#{$filePath}.woff2") format("woff2");
+ font-style: #{$style};
+ font-weight: $weightValue;
+ text-rendering: optimizelegibility;
+ }
+}
+
+@include printRedHatFont(700, "Bold", $familyName: "RedHatDisplay");
+@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(900, "Black", $familyName: "RedHatDisplay");
+@include printRedHatFont(900, "BlackItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(300, "Italic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(400, "Medium", $familyName: "RedHatDisplay");
+@include printRedHatFont(400, "MediumItalic", $style: "italic", $familyName: "RedHatDisplay");
+@include printRedHatFont(300, "Regular", $familyName: "RedHatDisplay");
+@include printRedHatFont(700, "Bold");
+@include printRedHatFont(700, "BoldItalic", $style: "italic");
+@include printRedHatFont(400, "Italic", $style: "italic");
+@include printRedHatFont(700, "Medium");
+@include printRedHatFont(700, "MediumItalic", $style: "italic");
+@include printRedHatFont(400, "Regular");
+@include printRedHatFont(700, "Bold", $familyName: "RedHatMono");
+@include printRedHatFont(700, "BoldItalic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(400, "Italic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(500, "Medium", $familyName: "RedHatMono");
+@include printRedHatFont(500, "MediumItalic", $style: "italic", $familyName: "RedHatMono");
+@include printRedHatFont(400, "Regular", $familyName: "RedHatMono");
diff --git a/pkg/lib/patternfly/patternfly-5-cockpit.scss b/pkg/lib/patternfly/patternfly-5-cockpit.scss
new file mode 100644
index 0000000..8008e20
--- /dev/null
+++ b/pkg/lib/patternfly/patternfly-5-cockpit.scss
@@ -0,0 +1,9 @@
+/* Set fake font and icon path variables */
+$pf-v5-global--font-path: "patternfly-fonts-fake-path";
+$pf-v5-global--fonticon-path: "patternfly-icons-fake-path";
+$pf-v5-global--disable-fontawesome: true !default; // Disable Font Awesome 5 Free
+
+@import "@patternfly/patternfly/patternfly-base.scss";
+
+/* Import our own fonts since the PF4 font-face rules are filtered out in build.js */
+@import "./fonts";
diff --git a/pkg/lib/patternfly/patternfly-5-overrides.scss b/pkg/lib/patternfly/patternfly-5-overrides.scss
new file mode 100644
index 0000000..8cbf65f
--- /dev/null
+++ b/pkg/lib/patternfly/patternfly-5-overrides.scss
@@ -0,0 +1,391 @@
+/*** PF5 overrides ***/
+// Pull in variables (for breakpoints)
+@use "global-variables" as *;
+
+// PF Select is deprecated - no issue reported upstream - this needs to be removed from our codebase
+// Make select have the expected width
+.pf-v5-c-select[data-popper-reference-hidden="false"] {
+ inline-size: auto;
+}
+
+/* WORKAROUND: Navigation problems with Tertiary Nav widget on mobile */
+/* See: https://github.com/patternfly/patternfly-design/issues/840 */
+/* Helper mod to wrap pf-v5-c-nav__tertiary */
+.ct-m-nav__tertiary-wrap {
+ .pf-v5-c-nav__list {
+ flex-wrap: wrap;
+ }
+
+ .pf-v5-c-nav__scroll-button {
+ display: none;
+ }
+}
+
+/* Helper mod to center pf-v5-c-nav__tertiary when it wraps */
+.ct-m-nav__tertiary-center {
+ .pf-v5-c-nav__list {
+ justify-content: center;
+ }
+}
+
+/* Fix overflow issue with tabs, especially seen in small sizes, like mobile
+seen in:
+- https://github.com/cockpit-project/cockpit-podman/pull/897#issuecomment-1127637202
+- https://github.com/patternfly/patternfly/issues/1625
+- https://github.com/patternfly/patternfly/pull/2757
+- https://github.com/patternfly/patternfly/issues/4800
+- https://github.com/patternfly/patternfly-design/issues/840
+- https://github.com/patternfly/patternfly-design/issues/1034
+- https://github.com/cockpit-project/cockpit-podman/issues/845
+
+This disables the large and halfway useless overflow buttons and causes the tabs
+to wrap around when there isn't enough space.
+*/
+.pf-v5-c-tabs__list {
+ flex-wrap: wrap;
+}
+
+/* Fix select menu rendering */
+ul.pf-v5-c-select__menu {
+ /* Don't get too tall */
+ max-block-size: min(20rem, 50vh);
+ /* Don't have a horizontal scrollbar */
+ overflow-y: auto;
+}
+
+/* Adjust padding on form selects to resemble PF non-form selects */
+/* (This can be seen when the longest text is selected on a non-stretched select) */
+/* Upstream: https://github.com/patternfly/patternfly/issues/4387 */
+/* Cockpit-Podman: https://github.com/cockpit-project/cockpit-podman/issues/755 */
+select.pf-v5-c-form-control {
+ --pf-v5-c-form-control--PaddingRight: 41px;
+ --pf-v5-c-form-control--PaddingLeft: 8px;
+
+ // Firefox's select text has additional padding (4px)
+ @-moz-document url-prefix() {
+ --pf-v5-c-form-control--PaddingRight: 37px;
+ --pf-v5-c-form-control--PaddingLeft: 4px;
+ }
+}
+
+// Workaround the transparent background for HTML select options
+// https://github.com/patternfly/patternfly/issues/5695
+.pf-v5-c-form-control option {
+ background-color: var(--pf-v5-c-form-control--BackgroundColor);
+ color: var(--pf-v5-c-form-control--Color);
+}
+
+// The default gap between the rows in horizontal lists is too large
+.pf-v5-c-description-list.pf-m-horizontal-on-sm,
+.pf-v5-c-description-list.pf-m-horizontal {
+ --pf-v5-c-description-list--RowGap: 1rem;
+}
+
+.pf-v5-c-description-list {
+ // When using horizontal ruler inside description list it's just for the spacing - don't show it
+ > hr {
+ border-block-start: none;
+ }
+}
+
+.pf-v5-c-modal-box.pf-m-align-top {
+ // We utilize custom footers in dialogs
+ // Make sure that the buttons always appear in the next line from the inline alerts
+ .pf-v5-c-modal-box__footer {
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ > div:not(.pf-v5-c-button):not(.dialog-wait-ct) {
+ flex: 0 0 100%;
+ }
+ }
+}
+
+// don't hide long dialog titles
+.pf-v5-c-modal-box__title-text {
+ white-space: normal;
+}
+
+.pf-v5-c-card {
+ // https://github.com/patternfly/patternfly/issues/3959
+ --pf-v5-c-card__header-toggle--MarginTop: 0;
+
+ .pf-v5-c-card__header:not(.ct-card-expandable-header),
+ .pf-v5-c-card__header:not(.ct-card-expandable-header) .pf-v5-c-card__header-main {
+ // upstream fix (pending): https://github.com/patternfly/patternfly/pull/3714
+ display: flex;
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+ justify-content: space-between;
+ }
+
+ .pf-v5-c-card__header:not(.ct-card-expandable-header) {
+ > .pf-v5-c-card__actions {
+ flex-wrap: wrap;
+ row-gap: var(--pf-v5-global--spacer--sm);
+
+ // PF4 CardActions act up when using buttons while the title is large of font
+ // https://github.com/patternfly/patternfly/issues/3713
+ // https://github.com/patternfly/patternfly/issues/4362
+ margin: unset;
+ padding-inline: var(--pf-v5-c-card__actions--PaddingLeft) unset;
+ }
+ }
+}
+
+// Add some spacing to nested form groups - PF4 does not support these yet
+// https://github.com/patternfly/patternfly-design/issues/1012
+.pf-v5-c-form__group-control {
+ .pf-v5-c-form__group, .pf-v5-c-form__section {
+ padding-block-start: var(--pf-v5-global--spacer--md);
+ }
+}
+
+// Alerts use elements that have fonts set in other frameworks (including PF3);
+// generally, this is an H4 that often has a font size and sometimes family set.
+// Therefore, it should inherit from the alert font set at the pf-v5-c-alert level.
+// https://github.com/patternfly/patternfly/issues/4206
+.pf-v5-c-alert__title {
+ font-size: inherit;
+ font-family: inherit;
+}
+
+.pf-v5-c-toolbar {
+ // Make summary content use the same vertical space as the filter toggle,
+ // when possible.
+ // https://github.com/patternfly/patternfly-design/issues/1055
+ &.ct-compact {
+ @media screen and (max-width: $pf-v5-global--breakpoint--lg - 1) {
+ display: flex;
+ flex-wrap: wrap;
+
+ > .pf-v5-c-toolbar__content:first-child {
+ flex: auto;
+ }
+
+ .pf-v5-c-toolbar__content-section {
+ inline-size: auto;
+ }
+ }
+ }
+}
+
+// When there is an Alert above the Form add some spacing
+.pf-v5-c-modal-box .pf-v5-c-alert + .pf-v5-c-form {
+ padding-block-start: var(--pf-v5-global--FontSize--sm);
+}
+
+// HACK: Not possible to specify text, so needs some hacks, see https://github.com/patternfly/patternfly-react/issues/6140
+.pf-v5-c-toolbar__toggle {
+ .pf-v5-c-button.pf-m-plain {
+ color: var(--pf-v5-c-button--m-link--Color);
+
+ .pf-v5-c-button__icon {
+ margin-inline-end: var(--pf-v5-global--spacer--sm);
+ }
+ }
+}
+
+// Flex should use gap, not a margin hack
+// https://github.com/patternfly/patternfly/issues/4523
+// Override default spacing from lg -> md
+.pf-v5-l-flex {
+ gap: var(--pf-v5-l-flex--spacer-base);
+
+ &:not([class*="pf-m-space-items-"]) {
+ > * {
+ --pf-v5-l-flex--spacer--column: 0;
+ }
+ gap: var(--pf-v5-l-flex--spacer--md);
+ }
+
+ // Negate the margin hack used by immediate flex children
+ // (except for nested flex, as we want to mind the gap)
+ > :not(.pf-v5-l-flex) {
+ --pf-v5-l-flex--spacer-base: 0;
+ }
+
+ // Undo all spacer modification adjustments
+ &[class*="pf-m-space-items-"] {
+ > * {
+ --pf-v5-l-flex--spacer--column: 0;
+ }
+ }
+
+ // Re-add spacer modification adjustments on the flex layout widget
+ // (using class attribute matching for handling breakpoint -on- also)
+ @each $size in (none, xs, sm, md, lg, xl, 2xl, 3xl, 4xl) {
+ &[class*="pf-m-space-items-#{$size}"] {
+ --pf-v5-l-flex--spacer-base: var(--pf-v5-l-flex--spacer--#{$size});
+ }
+ }
+}
+
+// Realign the radio and checks: https://github.com/patternfly/patternfly/issues/5802
+.pf-v5-c-radio,
+.pf-v5-c-check {
+ // `baseline` is different in Firefox than Chrome & WebKit; use `normal`
+ align-items: normal;
+
+ // Remove incorrect PF settings on the children
+ &__label,
+ &__input {
+ margin-block: auto;
+ align-self: unset;
+ transform: unset;
+ }
+
+ // Slightly shift the radio and check widgets
+ &__input {
+ // Shift up the checks/radios for (most) browsers
+ transform: translateY(-1px);
+ // Mozilla doesn't need the translation, so undo it
+ -moz-transform: none; // stylelint-disable property-no-vendor-prefix
+ // If the size is not specified, browsers may size it between 12px - 16px; so let's set it to the font size (which winds up 16px)
+ block-size: var(--pf-v5-global--FontSize--md);
+ // Use the height for width too
+ aspect-ratio: 1;
+ }
+}
+
+// InputMenus now use the PF Panel component which mistakenly uses position:
+// relative, when it needs to be set to absolute.
+// Additionally, it needs to be full width to properly align to the widget the
+// popover panel describes.
+// https://github.com/patternfly/patternfly-react/issues/7592
+.pf-v5-c-search-input__menu.pf-v5-c-panel {
+ position: absolute;
+ inline-size: 100%;
+}
+
+// Breadcrumb links should have the correct pointing hand cursor.
+//
+// PatternFly requires a "to" attribute for an actual link, but we use some
+// funky onClick JS for navigating and override it with a className.
+//
+// Therefore, instead of having a proper <a href="..."> being rendered, we need
+// to override the link. This is a problem with a (correct) assumption in PF
+// and our (incorrect) way of not using links (but using JavaScript) for
+// linking.
+//
+// Nevertheless, Cockpit needs to be adapted for this to work as expected.
+.pf-v5-c-breadcrumb__link {
+ cursor: pointer;
+}
+
+//Page headers are inconsistent with shadows and borders
+// https://github.com/patternfly/patternfly/issues/5184
+.pf-v5-c-page__main-group,
+.pf-v5-c-page__main-nav,
+.pf-v5-c-page__main-section.pf-m-light:not(:last-child) {
+ z-index: var(--pf-v5-c-page--section--m-shadow-bottom--ZIndex);
+ box-shadow: var(--pf-v5-c-page--section--m-shadow-bottom--BoxShadow);
+}
+
+// Dark mode fixes for several PF components
+.pf-v5-theme-dark {
+ // Change background color behind cards
+ // (matches PF surge website; PF doesn't specify otherwise)
+ .pf-v5-c-page__main-section {
+ --pf-v5-c-page__main-section--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-300);
+ }
+
+ // Adapt breadcrumb bar to be similar color as PF website
+ // (We use header bars in slightly different ways from PF)
+ // https://github.com/patternfly/patternfly/issues/5301
+ .pf-v5-c-page__main-breadcrumb {
+ --pf-v5-c-page__main-breadcrumb--BackgroundColor: var(--pf-v5-global--BackgroundColor--dark-100);
+ background-color: var(--pf-v5-global--BackgroundColor--dark-100);
+ }
+
+ // Fix input group background and borders
+ // (Looks fixed in PF5, but not in PF4)
+ .pf-v5-c-text-input-group {
+ background-color: var(--pf-v5-global--BackgroundColor--400);
+
+ .pf-v5-c-text-input-group__text {
+ &::before {
+ border-block-start-color: transparent;
+ border-inline-end-color: transparent;
+ border-inline-start-color: transparent;
+ }
+
+ &:is(:focus,:hover)::after {
+ border-block-end-color: var(--pf-v5-global--active-color--100);
+ }
+
+ &:not(:focus):not(:hover)::after {
+ border-block-end-color: var(--pf-v5-global--BorderColor--400);
+ }
+ }
+ }
+
+ // FIXME: https://github.com/patternfly/patternfly/issues/5278
+ .pf-v5-c-modal-box .pf-v5-c-table {
+ background-color: inherit;
+ }
+}
+
+// Fix icons in buttons
+.pf-v5-c-button__icon.pf-m-start {
+ margin-inline: 0 var(--pf-v5-c-button__icon--m-start--MarginRight);
+}
+
+// Drop side padding in mobile mode,
+// intended mainly for PF PageSection elements (pf-v5-c-page__main-section).
+// It's similar to adding padding={{ default: 'noPadding', sm: 'padding' }},
+// except this only affects the sides, not the top and bottom.
+@media screen and (max-width: $pf-v5-global--breakpoint--sm) {
+ .pf-v5-c-page__main > section.pf-v5-c-page__main-section:not(.pf-m-padding) {
+ padding-inline: 0;
+ }
+}
+
+// Patch tabular number 0s to not have the slash inside
+// https://github.com/RedHatOfficial/RedHatFont/issues/53
+// https://github.com/patternfly/patternfly/issues/5308
+@font-face {
+ /* red-hat-text-regular */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 400;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAAe8ABAAAAAAHEQAAAdfAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAGVA/RkZUTRwaGBuaeBxuBmAAgkIRCAqBAIEICwwAATYCJAMUBCAFk0wHNRsqG1FUjgoEfz9ujIGNa3r1k8ghpztyImrCBYeonxUhtHdiJ+VxOsW1XvMb+MfL1IXbjodirvftJrlLmXk8oXAIGtz3ZWMrDLGtwvP7dO1vUK18MEuOuwr1HGbdxvxtJkjRURZ0AACm/+Q57n/s4iCgJJL8gTb8spMpWXyTfaB1cheGpDVP+q9RXC7EbPF87t79W+ptEWcSRJxLBcrqiyc9mJ/A5OTLl8rkeDKpTCaVSqRSeRKJRCKRJ6lV11TVDu9HyuJ0/Y9TL5b/hy5+R0aejKwkK8mXBOLAMYRbAP/3rn1aYAcsaY7cXWsUktB1QLOX5Gj3FzgHqMqYN5+yBWRFikmVJYERwEKRA5YVBoWr1JXdhjkRb+jfXuUqsphizGjMnud5RoEapc7yJhDeW1SeZtBWnpvP6D8mOcO8dhBSraNojaD2yybgbQPwhjbA2xMCHdEVJEokAu2t9Yp/EWNaoYXAQK3LPs8IEysenyvh2++a4Tsfb0To2HHUVRk68cYrZfpQy05dn0A66VSbbF6hjflJMm9Q4RWVFEBMaI5CFkyaph/NGFGHKzOpeYPeeusrrfoL30CHAjIyjDLJn/GXjQJSMhSG16nl6boWG2QPw4vDk5q39d2BXlhvqrHx5IoCRAZcxvNOMgGQdGa3It3wh0L1cq0yZS93LQasSKAXZKHF5T/jpNed7iewLTa3BLO3HQ1chV4/PqWW5xsv9scjxfLEvg6W66ymBWQLnF4HwG05Qpjl+0Mt3yccNMDFALN63HnhmwhUAJgBUEJ01QXSVgLLZUjnGeAUbel0vddcbCr2SsAlQulrmmtzEqdHsVAOfeWleo/JPLQOamTU66LjeNhzp6KqAKB1MLTQBx8Uae18DIfaBXcA43mTUQeLmrBAwtF8Fy1NAslad5ibzH4kuMkgqErYAITmyx40CQlJ7dTem8p1V1IvnRFDsnzeUQe+aPuhHgG95VfxBtGxZoZJIF8r8p9XmYjEGxId7kNBPkX6ZksLGbHULnNBv5t2vKjt1nwQy2RXecnhRbVxpSMPi83HqMSQdRjE6NC0oEOdCTlVp3RmQebQbDTfd+E4OkYMPkagMB7MMPPbJzLYBXAVDQJYK3WrCOgXKXJ5hgdPgi+g90EfCHrjAAyGAxYFWZXHcGXhJ9Tg7OiH21m2gmfWFKi20Wo8Fn1TaG9iJiMNhT01isE6Qvcm9RoGQS/YLXVkx2XaQv4maOZD2wWPr8rbmZXzhscBbT4r2qJHotccXba3jCDkSZftjYHIKk5GPyXQfSgKNpUYOdkzRxW9rW7DaFEgoHJdZoaGC2BoFF4xMOqN5kS0/PNMJBgD6B4HQxayhVWEIHBcS5MOpQdgb4JQPebjAXMo3pabQwRzohBGV30Go/tBZr+aP5dVaWTQLzQE6snLfFBLG3ktDbDB3oQ4jHosgoyqKDMaRfEYTk7gKBgLbFo3Jg7V1Irfb3Wuo8B9gPiWAENpKFBgIAEWaqFSZiiY6kuBqS2vuH+AaBCASCSFGoe/icLdLQrRYgBUoD1KDAXTAZQASFkXTvv+yB20DbvmmL/ndTqfMVDiT7SITwPFfydBBs3A8LD73aoGzdv55V+pTP9sAhCIvP2w2u7IgN+p+LZo4exWW8+1pJYgkAtG5QiyRQ4LqoD/PmITkA9s5V6sEqh0pYFECCwFwNC4RAD2+YcAoWOMJUBqxioIUBgUVwlQ6hmvAZVOmQI0dM/ZBGjalwNqXYqLEKCdLkUbAdq723oBHfStHhGgo97VF0AndzuEAJ0NaP4EdDdaT9MiynZ4Avgg9I3OkDrHLCjMj01QGhttUOkfX6BhZHaFpidagdrgYo9vu8DL0D6G90voYGp1HDqaWL2CTjF8/IPOZjef/gu6W66bVsgVfikhX4CRupq6TeRZwEUeuRzkksTIPSUXcdkYOVJhgZxSIssCjBVK2wwGX4gFKhadLZcy4LM7L+qOxxOyhaQk6Ejnchm2Atvk4XJp9Wykhn90HbqUvDyHbUGlLZ8X/RB+yEFlzV/sENPhgW+dyD7HbRtnvgK64z9z/WD4IdzyL/vrCb5KUqRgL+V9gGn0v/D+GnY0nbhh2vk9osu2G1CDTZtcqzd2+tEFO4khorWB4FwD7QhObbCD7uVubobyQza9yZpYiWo0ZM6BRlOJ/k62KJ+tiHYGkk+RUutIGj2xdQeb8gqJOq9Tr3dzHPI6XKhEaJ0NeZLzsG+S4qLA/IQTxDHLLORUvlCOrzI5QwlPrpjyThGjIGgmECZItiwss4xT+CKps5O+SKGEZEm4yHduMQitPPLg2zjbRkKQJVtsYxZYSVeC5uvSOZDPcOecCUs5sLfwTQescCutiZZg9H66MvflaXyLncfLRP+Q9s7YYoJBaHRhkz5y+KMHkrxNcmEXqS7LMFm5svK7cxOE8r9LB9albLEE+80Cl7eGlTrlqR0nS1dQgkQ3bYuWJLrmZTiraE+GjP8IPrtDF85Ce3Y081ATaMVR2G+FkHpE1ksdy3WWFgAA);
+}
+
+@font-face {
+ /* red-hat-text-italic */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: italic;
+ font-weight: 400;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAARQAAsAAAAADYwAAAQEAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBKIE/CwwAATYCJAMUBCAFkz0HIBvTDBGVnFUBfhzYbVYrgpXBW2mcjbMQzKj2CJ7Xi/85N8mCj8I31EGoJLfybs1VZl6ZyBK4Vh2jT3nLs5sj4dUngEuZ1e+dluoRtq0zKIlQcJ/3KFXzHQpsqVVIHAiJKjWtCRXZx1eZiYvUsXGRKq/wZqa/mnANebooLlGQzkvbYH3zQmMn02IhKYosT9HMsus4eAn7YwxsuHz9BsIUU7K//3+D7O+YisshfkyPYohAhgwDBFggExggNwS5XGBKp5Ns77wjgCgJZxAJSUd/+EtkvGcTotPRBgWoIcdUbAJoACBJzValtf/57znl3Xdr570fp36y5XnFPKe8Vz//gw/i1Pe/mp7yyQfnv/fhbu++FwW2/uD992tAS899ft8Trzjx6K1fv+LYrcLjHn6ydsziJQfvs/T+vXbd9uCNvNW7bXOQc2rfHfXM6M2tz2QH3HJDdeOhZ6/bcNr60+5qHVurzlm/8V2ceq8KTOj8lrZLLwhEuvjgRyZds3N7wW+VevYjfH7H+xfAF1/9ejG89NDNSUUCgYkJtRFiwh566jiLuf8msSHpIqvTNiYIFPpLICEElgAYEmcJwLYeEiA0YowASSmWC5AZEGcIkOseLwlQaMZfApR0TeMFKNs2/SRARTvbX4DquPJK1Fyav4G6PsXlaOhVfIymS8s90dK//MXJ3XSvDJeJvIoH8BD6RAtJI2YiMyfWITcqrkehX3yGkmGpjLIH0kJUDMrWS6vK41GLYdmdqJtS7IqGCcVdaMaw4ie0zCrfelfdjCoT/mNmMPqRhFKFoYAn4EN3lRy6ymXQGkHdzZk0qOVSDE28sMpAUpCmwthIibhcJeFtfkk4UoOOS9ucfNVJoSCkBNJCfGRp0GM6WHHSAca2e5XjNc9wTbxfzu6Ea9L+bzesFFy9ku2ECd4U8btM2SS6R3W0Dm7Nvct9MWBCd3CGAGwwMUlgN3wZ49tTAJ/DEwjFDmZ29mJ3cIrNNpFQ8POCMF6hO2OBiJHTJl2hWM1IqAmEdGPw2aecpAiDHtYuOAoWBOgaRC0j30/SgKTns3uMlCTSsaA5LIXzPVMpqbNQ3zXGyb+rCAq66SboZlBgH0TKIZRZb8TDpTTgfRmXZlLpYu2obzb24skIX0S9eU9FapWzKOozbZflzSW6GXkjQoskWjn00StBUChOXManswhawW0pKUkYMcWhCC3HQCq5Tpb2YA+VVMJP/LjFHXYFe5++22b/HhYBgX2DvWDJMKYOJMjwm/C3tCQUO7kgcYS2wn4xVlSi3TUef1ruwTWOT/dsRyfVaLGfUW7bTbBL7u/Spt0zJvsrQ671aEScUIObukdTouZX0C1qsUxUSreOW5fgH/+/9g2dIwMA);
+}
+
+@font-face {
+ /* red-hat-text-medium */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: normal;
+ font-weight: 700;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAARYAAsAAAAADewAAAQLAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBBIEUCwwAATYCJAMUBCAFlEEHIBssDRHF5AH4ceCUZY9pfajQk3kIirq5CjGXcNU3dzw879+/rX1uIKlLfhd0AMukgzVCNamGesTI7ZlyoyVrc1NnSh8mwlFTCbdmKtP+Dit8tlI4uJx+ywNd5wGHgUayB46pjw3cEzAMA9obaA2wrQFmmmYhpw1jFzajZpAvnYxflbQxTcLQpXLM0Ca6k6JAlq87j+w4K3uxFI1geN+hw/TSSqvs7/9fkv0drbEL4rd0DbUEMmSoJkA3mUA1+dRCPvkEWuVy9Wvk3gogChCASEg2jMM3kQFbCpHLeQX5QRHkQwN0AuQDQJKKgBaFvt38Uaz7eMt7a7/Z9PEZ8aON78yfr31ftGdVrYts/HLjRxs+3vR+/avCjj48fmJf0pGHJ47HC07O3LomD7Q8+b81b41tT7+YcqBX76kHP6toeve6uOr7HbdHcPpTA4FI2z6r8ajDzNduv61o9gG+OvXRHvj62x9Z3dxvaeV5iEISCMRN8R8greRLWf67/xgxnHRSmzRJXYH8ykkgIQR6AagXFwRgsnMChLLRQYCkYEwRIFMtDguQT9n4VoD8iqfSAhRQOvUVoKDJWWEBCimZbRCgsJLZWQGK2JHvOwGK0qrU0zRQTIX+TAPF7ShYAw2UUPX+QQOlPK2B0h43qH8Zn1vtTryFcQacQ2iQEpCUyBDIdMocyKdBLkN+VfIbFFDn1ISCzqQTFFLjmYspTNgFRVLn7i4U1ap5UEyz3oPiqfNYGErocG9CKX8rC6U9rcsvKCO/kQYSqRuJuDwMtTW1taAjjw3t2SxoSmBoS0r4bCaGejLMk5AUVORhLKV0NTS4CPNkDHWmRKTxbV7zrA2Hg5iIED5xz1gixkpA9eTYMhodPRv3toVTwYZzfnC4qGIhd21cYi8zNAv28+xKENSUf4kTUDV3CeiKHtmuZ5iVq4VkIvBLjqMuL1k33vskVyY0SJAOHQrQWp1NH+/QrQwsLOn0E/yJTk3V+ls1HV21l+kE5fySUAsfcTET7pUUkoghMeVgReYSIGkgqh/yvZMCCzNqeIwJLkmIVKFwqAij/piFK0VURf4O8Tvu43iIgnSaBx0kHOxCkGz4FBoi5j/kigfE2TnGwkg58w7pYGap20if9/p69PIrmKo/EcXZ83nb778FJJpPOBNISDCEbOjCr5wnOFjXszsvzLpQFCxKMUkkxZQ6hYTqEpKrYWNsCYyvHC4RuAopKPidh9FUe5gtE2nhvsDPoN1DZrBablAY/vtRWGYtRDj3dii4i2KFX/5YnaLQ3Jx/M14CbaSX7fC6iCkQYjcpW2xHgXndGplBaGzmiD8/n4udCiLBhyIarSIVSpzlI9gYQiEnAv1uYLpLNP0kBmgE4daZBPYHprlOLAA=);
+}
+
+@font-face {
+ /* red-hat-text-medium-italic */
+ unicode-range: U+0030;
+ font-family: RedHatText;
+ font-style: italic;
+ font-weight: 700;
+ src: url(data:font/woff2;base64,d09GMgABAAAAAASEAAsAAAAADlwAAAQ2AAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAABmAAgkIRCAqBFIEsCwwAATYCJAMUBCAFlSEHIBukDRGVnM0Bfh7YtpMt1pMMwRCUu0ZjuTUEq7QbeQVD9K0cz/Ot8btvZnf/YKZJNFnHE01D/RnzSKWKhUyUChVONznY33vvR2dGwQ2aI5GoNmehLcEOiWwABJeyW3OVmVcm8ohUq47RZ7FbwPRAePVny2WpHmHbKIFFSYTiZgv0eNbhCK663oXEgZAouis1niAM8d0RrIJ/++6lfvsW4tlZz+XCcgddeYS86ClXUGEcuZ9pztoaHSVD7Uom1PoYXN4HzmM1HDvo8UJstrl+/duh/ooIdyFfyySwWFBRMV/ALlUwX2cxOp1g8////ez/7wTIYNOHFBRPpuM3UT2dduT/fwTQg7HosBaHAOMBUJSJ41s7f/UIRt8fohuPkYqTk0DjIYx+wDSdIBXHF4dHTScnx41XjWvbCeXg09bl7Kq2spLoZndx7KxpLys+bE+xPzlSvtEU9Kdaw+XrzSFfCjx7S7xYdVVkdxPPNt0V6Tn3pXNM35O5+5Lj8TVY8f+1pJQVQcrNGpprY9Ik7deGqZ/h49BxJ+DT3euFXl45vWgKCHjCmw1FF5wzw5Or//U/Ro5RHsGeErZC0JurgIII9gFYngkI4ITnAmJa5AKKIccFVEvTCwGdufkT0JtQdggYzCr5AkacqAYBzaT6BgLGmFS/CBjrdk8VMM7s/k7AeDMHpoAJbo+kCZhoXuMJmGx828MUE9q1+VPNbbuqdGPwlM8QK4OHYnwOQ3UoSdDZnlXozS1EGGwsFhjxtDRBs7B2yceo12BsltZPGGdzPw7jre+/YEKWDgaYaOfIK0w2u7lgirlNLucvmGpSt7lM7qfkWAGGRgZGhtBDQEM3moI2CEMXpUxIkxiaqrFAplRBDQHGchVPX5/lsEBN6JEyib5sun/VmWE4kkNiintWMinWBKkq1j5mPQKNN61wQ1xoZ4PJlTQWfjfEOg5qkYlI9NKWD4LKvsIBWNs3wdfAg/b9JD/GK04tASqxsZdO7i0AsMW2oxcemJdGiAQY6v2hj4/5jub2DnznuBqjbimBQcMNRmM/O3D5FqNuVZcqy4v1JSg8sy8eXD/7opUqTiaFNzdUr4ltdQWPsnLjOwlH+QU1zSZWiSQ6EEejweUpky0p6SA48swT/xBwKuiT9El+l5vYD1LSkIqGHupwVyFgqz9rKPLSzaGjvds6iLOcPkV66UGKvJMJY/hKQzuzbm7RfuSNODEixDT0CVekwI2Yun7uzTyIAlGRSAvNsUrPEhHr4Wac1Xe2cgCFYQwS3sbkcQPzJRV0vfftYZIvhIHN7j0prlgOThTI+EtwCbhkFD29IrGHWGFLMysqF5fCO59XB/DG+Xle3pOQIjH2k9PYLoHSvy+RUVwIR/xlEFc+BxEXVNQ/eHIo4f1HsH/OR5zIVm52mVCpjKZqyJHY+j1zNf8f/y+v5iopAAAAAA==);
+}
+
+// Fix for https://github.com/patternfly/patternfly/issues/5855
+.pf-v5-u-font-weight-light {
+ font-weight: 300;
+}
+
+// Workaround for https://github.com/patternfly/patternfly/issues/5865
+.pf-v5-c-card.pf-m-selectable, .pf-v5-c-card.pf-m-clickable {
+ isolation: auto;
+}
diff --git a/pkg/lib/plot.js b/pkg/lib/plot.js
new file mode 100644
index 0000000..19c8d7f
--- /dev/null
+++ b/pkg/lib/plot.js
@@ -0,0 +1,574 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from 'cockpit';
+
+/* The public API here is not general and a bit weird. It can only do
+ * what Cockpit itself needs right now, and what can be easily
+ * implemented without touching the older Metrics_series classes.
+ *
+ * The basic idea is that you create a global PlotState object and
+ * keep it alive for the lifetime of the page. Then you instantiate
+ * ZoomControl and SvgPlot components as needed.
+ *
+ * To control what to plot, you call some methods on the PlotState
+ * object:
+ *
+ * - plot_state.plot_single(id, metric_options)
+ *
+ * - plot_state.plot_instances(id, metric_options, instances, reset)
+ *
+ * You need to figure out the rest of the details from the existing
+ * users, unfortunately.
+ */
+
+class Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ cockpit.event_target(this);
+ this.desc = desc;
+ this.options = opts;
+ this.grid = grid;
+ this.plot_data = plot_data;
+ this.interval = interval;
+ this.channel = null;
+ this.chanopts_list = [];
+ }
+
+ stop() {
+ if (this.channel)
+ this.channel.close();
+ }
+
+ remove_series() {
+ const pos = this.plot_data.indexOf(this.options);
+ if (pos >= 0)
+ this.plot_data.splice(pos, 1);
+ }
+
+ remove() {
+ this.stop();
+ this.remove_series();
+ this.dispatchEvent("removed");
+ }
+
+ build_metric(n) {
+ return { name: n, units: this.desc.units, derive: this.desc.derive };
+ }
+
+ check_archives() {
+ if (this.channel.archives)
+ this.dispatchEvent("changed");
+ }
+}
+
+class Metrics_sum_series extends Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ super(desc, opts, grid, plot_data, interval);
+ if (this.desc.direct) {
+ this.chanopts_list.push({
+ source: 'direct',
+ archive_source: 'pcp-archive',
+ metrics: this.desc.direct.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.pmcd) {
+ this.chanopts_list.push({
+ source: 'pmcd',
+ metrics: this.desc.pmcd.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.internal) {
+ this.chanopts_list.push({
+ source: 'internal',
+ metrics: this.desc.internal.map(this.build_metric, this),
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ }
+
+ flat_sum(val) {
+ if (!val)
+ return 0;
+ if (val.length !== undefined) {
+ let sum = 0;
+ for (let i = 0; i < val.length; i++)
+ sum += this.flat_sum(val[i]);
+ return sum;
+ }
+ return val;
+ }
+
+ reset_series() {
+ if (this.channel)
+ this.channel.close();
+
+ this.channel = cockpit.metrics(this.interval, this.chanopts_list);
+
+ const metrics_row = this.grid.add(this.channel, []);
+ const factor = this.desc.factor || 1;
+ const threshold = this.desc.threshold || null;
+ const offset = this.desc.offset || 0;
+ this.options.data = this.grid.add((row, x, n) => {
+ for (let i = 0; i < n; i++) {
+ const value = offset + this.flat_sum(metrics_row[x + i]) * factor;
+ if (threshold !== null)
+ row[x + i] = [(this.grid.beg + x + i) * this.interval, Math.abs(value) > threshold ? value : null, threshold];
+ else
+ row[x + i] = [(this.grid.beg + x + i) * this.interval, value];
+ }
+ });
+
+ this.channel.addEventListener("changed", this.check_archives.bind(this));
+ this.check_archives();
+ }
+}
+
+class Metrics_stacked_instances_series extends Metrics_series {
+ constructor(desc, opts, grid, plot_data, interval) {
+ super(desc, opts, grid, plot_data, interval);
+ this.instances = { };
+ this.last_instance = null;
+ if (this.desc.direct) {
+ this.chanopts_list.push({
+ source: 'direct',
+ archive_source: 'pcp-archive',
+ metrics: [this.build_metric(this.desc.direct)],
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ if (this.desc.pmcd) {
+ this.chanopts_list.push({
+ source: 'pmcd',
+ metrics: this.desc.pmcd.map(this.build_metric, this),
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+
+ if (this.desc.internal) {
+ this.chanopts_list.push({
+ source: 'internal',
+ metrics: [this.build_metric(this.desc.internal)],
+ metrics_path_names: ['a'],
+ instances: this.desc.instances,
+ 'omit-instances': this.desc['omit-instances'],
+ host: this.desc.host
+ });
+ }
+ }
+
+ reset_series() {
+ if (this.channel)
+ this.channel.close();
+ this.channel = cockpit.metrics(this.interval, this.chanopts_list);
+ this.channel.addEventListener("changed", this.check_archives.bind(this));
+ this.check_archives();
+ for (const name in this.instances)
+ this.instances[name].reset();
+ }
+
+ add_instance(name, selector) {
+ if (this.instances[name])
+ return;
+
+ const instance_data = Object.assign({ selector }, this.options);
+ const factor = this.desc.factor || 1;
+ const threshold = this.desc.threshold || 0;
+ const last = this.last_instance;
+ let metrics_row;
+
+ function reset() {
+ metrics_row = this.grid.add(this.channel, ['a', name]);
+ instance_data.data = this.grid.add((row, x, n) => {
+ for (let i = 0; i < n; i++) {
+ const value = (metrics_row[x + i] || 0) * factor;
+ const ts = (this.grid.beg + x + i) * this.interval;
+ let floor = 0;
+
+ if (last) {
+ if (last.data[x + i][1])
+ floor = last.data[x + i][1];
+ else
+ floor = last.data[x + i][2];
+ }
+
+ if (Math.abs(value) > threshold) {
+ row[x + i] = [ts, floor + value, floor];
+ if (row[x + i - 1] && row[x + i - 1][1] === null)
+ row[x + i - 1][1] = row[x + i - 1][2];
+ } else {
+ row[x + i] = [ts, null, floor];
+ if (row[x + i - 1] && row[x + i - 1][1] !== null)
+ row[x + i - 1][1] = row[x + i - 1][2];
+ }
+ }
+ });
+ }
+
+ function remove() {
+ this.grid.remove(metrics_row);
+ this.grid.remove(instance_data.data);
+ const pos = this.plot_data.indexOf(instance_data);
+ if (pos >= 0)
+ this.plot_data.splice(pos, 1);
+ }
+
+ instance_data.reset = reset.bind(this);
+ instance_data.remove = remove.bind(this);
+ instance_data.name = name;
+ this.last_instance = instance_data;
+ this.instances[name] = instance_data;
+ instance_data.reset();
+ this.plot_data.push(instance_data);
+ this.grid.sync();
+ }
+
+ clear_instances() {
+ for (const i in this.instances)
+ this.instances[i].remove();
+ this.instances = { };
+ this.last_instance = null;
+ }
+}
+
+class Plot {
+ constructor(element, x_range_seconds, x_stop_seconds) {
+ cockpit.event_target(this);
+
+ this.series = [];
+ this.plot_data = [];
+
+ this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
+ this.grid = null;
+
+ this.sync_suppressed = 0;
+ this.archives = false;
+
+ this.reset(x_range_seconds, x_stop_seconds);
+ }
+
+ refresh() {
+ this.dispatchEvent("plot", this.plot_data);
+ }
+
+ start_walking() {
+ this.grid.walk();
+ }
+
+ stop_walking() {
+ this.grid.move(this.grid.beg, this.grid.end);
+ }
+
+ reset(x_range_seconds, x_stop_seconds) {
+ // Fill the plot with about 1000 samples, but don't sample
+ // faster than once per second.
+ //
+ // TODO - do this based on the actual size of the plot.
+ this.interval = Math.ceil(x_range_seconds / 1000) * 1000;
+
+ const x_offset = (x_stop_seconds !== undefined)
+ ? (new Date().getTime()) - x_stop_seconds * 1000
+ : 0;
+
+ const beg = -Math.ceil((x_range_seconds * 1000 + x_offset) / this.interval);
+ const end = -Math.floor(x_offset / this.interval);
+
+ if (this.grid && this.grid.interval == this.interval) {
+ this.grid.move(beg, end);
+ } else {
+ if (this.grid)
+ this.grid.close();
+ this.grid = cockpit.grid(this.interval, beg, end);
+ this.sync_suppressed++;
+ for (let i = 0; i < this.series.length; i++) {
+ this.series[i].stop();
+ this.series[i].interval = this.interval;
+ this.series[i].grid = this.grid;
+ this.series[i].reset_series();
+ }
+ this.sync_suppressed--;
+ this.sync();
+
+ this.grid.addEventListener("notify", (event, index, count) => {
+ this.refresh();
+ });
+ }
+ }
+
+ sync() {
+ if (this.sync_suppressed === 0)
+ this.grid.sync();
+ }
+
+ destroy() {
+ this.grid.close();
+ for (let i = 0; i < this.series.length; i++)
+ this.series[i].stop();
+
+ this.options = { };
+ this.series = [];
+ this.plot_data = [];
+ }
+
+ check_archives() {
+ if (!this.archives) {
+ this.archives = true;
+ this.dispatchEvent('changed');
+ }
+ }
+
+ add_metrics_sum_series(desc, opts) {
+ const sum_series = new Metrics_sum_series(desc, opts, this.grid, this.plot_data, this.interval);
+
+ sum_series.addEventListener("removed", this.refresh.bind(this));
+ sum_series.addEventListener("changed", this.check_archives.bind(this));
+ sum_series.reset_series();
+ sum_series.check_archives();
+
+ this.series.push(sum_series);
+ this.sync();
+ this.plot_data.push(opts);
+
+ return sum_series;
+ }
+
+ add_metrics_stacked_instances_series(desc, opts) {
+ const stacked_series = new Metrics_stacked_instances_series(desc, opts, this.grid, this.plot_data, this.interval);
+
+ stacked_series.addEventListener("removed", this.refresh.bind(this));
+ stacked_series.addEventListener("changed", this.check_archives.bind(this));
+ stacked_series.reset_series();
+ stacked_series.check_archives();
+
+ this.series.push(stacked_series);
+ this.sync_suppressed++;
+ for (const name in stacked_series.instances)
+ stacked_series.instances[name].reset();
+ this.sync_suppressed--;
+ this.sync();
+
+ return stacked_series;
+ }
+}
+
+class ZoomState {
+ constructor(reset_callback) {
+ cockpit.event_target(this);
+
+ this.reset_callback = reset_callback;
+
+ this.x_range = 5 * 60;
+ this.x_stop = undefined;
+ this.history = [];
+
+ this.enable_zoom_in = false;
+ this.enable_zoom_out = true;
+ this.enable_scroll_left = true;
+ this.enable_scroll_right = false;
+ }
+
+ reset() {
+ const plot_min_x_range = 5 * 60;
+
+ if (this.x_range < plot_min_x_range) {
+ this.x_stop += (plot_min_x_range - this.x_range) / 2;
+ this.x_range = plot_min_x_range;
+ }
+ if (this.x_stop >= (new Date()).getTime() / 1000 - 10)
+ this.x_stop = undefined;
+
+ this.reset_callback(this.x_range, this.x_stop);
+
+ this.enable_zoom_in = (this.x_range > plot_min_x_range);
+ this.enable_scroll_right = (this.x_stop !== undefined);
+
+ this.dispatchEvent("changed");
+ }
+
+ set_range(x_range) {
+ this.history = [];
+ this.x_range = x_range;
+ this.reset();
+ }
+
+ zoom_in(x_range, x_stop) {
+ this.history.push(this.x_range);
+ this.x_range = x_range;
+ this.x_stop = x_stop;
+ this.reset();
+ }
+
+ zoom_out() {
+ const plot_zoom_steps = [
+ 5 * 60,
+ 60 * 60,
+ 6 * 60 * 60,
+ 24 * 60 * 60,
+ 7 * 24 * 60 * 60,
+ 30 * 24 * 60 * 60,
+ 365 * 24 * 60 * 60
+ ];
+
+ let r = this.history.pop();
+ if (r === undefined) {
+ let i;
+ for (i = 0; i < plot_zoom_steps.length - 1; i++) {
+ if (plot_zoom_steps[i] > this.x_range)
+ break;
+ }
+ r = plot_zoom_steps[i];
+ }
+ if (this.x_stop !== undefined)
+ this.x_stop += (r - this.x_range) / 2;
+ this.x_range = r;
+ this.reset();
+ }
+
+ goto_now() {
+ this.x_stop = undefined;
+ this.reset();
+ }
+
+ scroll_left() {
+ const step = this.x_range / 10;
+ if (this.x_stop === undefined)
+ this.x_stop = (new Date()).getTime() / 1000;
+ this.x_stop -= step;
+ this.reset();
+ }
+
+ scroll_right() {
+ const step = this.x_range / 10;
+ if (this.x_stop !== undefined) {
+ this.x_stop += step;
+ this.reset();
+ }
+ }
+}
+
+class SinglePlotState {
+ constructor() {
+ this._plot = new Plot(null, 300);
+ this._plot.start_walking();
+ }
+
+ plot_single(metric) {
+ if (this._stacked_instances_series) {
+ this._stacked_instances_series.clear_instances();
+ this._stacked_instances_series.remove();
+ this._stacked_instances_series = null;
+ }
+ if (!this._sum_series) {
+ this._sum_series = this._plot.add_metrics_sum_series(metric, { });
+ }
+ }
+
+ plot_instances(metric, insts, reset) {
+ if (this._sum_series) {
+ this._sum_series.remove();
+ this._sum_series = null;
+ }
+ if (!this._stacked_instances_series) {
+ this._stacked_instances_series = this._plot.add_metrics_stacked_instances_series(metric, { });
+ } else if (reset) {
+ // We can't remove individual instances, only clear the
+ // whole thing (because of limitations of Metrics_stacked_instances_series above).
+ // So we do that, but only when there is at least one instance
+ // that needs to be removed. That avoids a lot of events and React warnings.
+ if (Object.keys(this._stacked_instances_series.instances).some(old => insts.indexOf(old) == -1))
+ this._stacked_instances_series.clear_instances();
+ }
+
+ for (let i = 0; i < insts.length; i++) {
+ this._stacked_instances_series.add_instance(insts[i]);
+ }
+ }
+
+ destroy() {
+ this._plot.destroy();
+ }
+}
+
+export class PlotState {
+ constructor() {
+ cockpit.event_target(this);
+ this.plots = { };
+ this.zoom_state = null;
+ }
+
+ _reset_plots(x_range, x_stop) {
+ for (const id in this.plots) {
+ const p = this.plots[id]._plot;
+ p.stop_walking();
+ p.reset(x_range, x_stop);
+ p.refresh();
+ if (x_stop === undefined)
+ p.start_walking();
+ }
+ }
+
+ _check_archives(plot) {
+ if (!this.zoom_state && plot.archives) {
+ this.zoom_state = new ZoomState(this._reset_plots.bind(this));
+ this.dispatchEvent("changed");
+ }
+ }
+
+ _get(id) {
+ if (this.plots[id])
+ return this.plots[id];
+
+ const ps = new SinglePlotState();
+ ps._plot.addEventListener("changed", () => this._check_archives(ps._plot));
+ this._check_archives(ps._plot);
+ ps._plot.addEventListener("plot", (event, data) => {
+ ps.data = data;
+ this.dispatchEvent("plot:" + id);
+ });
+
+ this.plots[id] = ps;
+ return ps;
+ }
+
+ plot_single(id, metric) {
+ const ps = this._get(id);
+ ps.plot_single(metric);
+ }
+
+ plot_instances(id, metric, insts, reset) {
+ this._get(id).plot_instances(metric, insts, reset);
+ }
+
+ data(id) {
+ return this.plots[id] && this.plots[id].data;
+ }
+}
diff --git a/pkg/lib/polyfills.js b/pkg/lib/polyfills.js
new file mode 100644
index 0000000..4c00bea
--- /dev/null
+++ b/pkg/lib/polyfills.js
@@ -0,0 +1,25 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2016 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ This file contains various polyfills and other compatibility hacks
+ */
+
+// Don't complain about extending native data types -- that's what polyfills do
+/* eslint-disable no-extend-native */
diff --git a/pkg/lib/python.js b/pkg/lib/python.js
new file mode 100644
index 0000000..c229065
--- /dev/null
+++ b/pkg/lib/python.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+const pyinvoke = ["sh", "-ec", "exec $(command -v /usr/libexec/platform-python || command -v python3) -c \"$@\"", "--"];
+
+export function spawn (script_pieces, args, options) {
+ const script = (typeof script_pieces == "string")
+ ? script_pieces
+ : script_pieces.join("\n");
+
+ return cockpit.spawn(pyinvoke.concat([script]).concat(args), options);
+}
diff --git a/pkg/lib/qunit-tests.js b/pkg/lib/qunit-tests.js
new file mode 100644
index 0000000..6f6e69d
--- /dev/null
+++ b/pkg/lib/qunit-tests.js
@@ -0,0 +1,90 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2014 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+"use strict";
+
+import QUnit from "qunit/qunit/qunit.js";
+import qunitTap from "qunit-tap/lib/qunit-tap.js";
+import "qunit/qunit/qunit.css";
+
+QUnit.mock_info = async key => {
+ const response = await fetch(`http://${window.location.hostname}:${window.location.port}/mock/info`);
+ return (await response.json())[key];
+};
+
+// Convenience for skipping tests that the python bridge can't yet
+// handle.
+
+let is_pybridge = null;
+
+QUnit.test.skipWithPybridge = async (name, callback) => {
+ if (is_pybridge === null)
+ is_pybridge = await QUnit.mock_info("pybridge");
+
+ if (is_pybridge)
+ QUnit.test.skip(name, callback);
+ else
+ QUnit.test(name, callback);
+};
+
+/* Always use explicit start */
+QUnit.config.autostart = false;
+
+let qunit_started = false;
+
+QUnit.moduleStart(() => {
+ qunit_started = true;
+});
+
+window.setTimeout(() => {
+ if (!qunit_started) {
+ console.log("QUnit not started by test");
+ console.log("cockpittest-tap-error");
+ }
+}, 20000);
+
+/* QUnit-Tap writes the summary line right after this function returns.
+* Delay printing the end marker until after that summary is out.
+*/
+QUnit.done(() => window.setTimeout(() => console.log("cockpittest-tap-done"), 0));
+
+/* Now initialize qunit-tap
+ *
+ * When not running under a tap driver this stuff will just show up in
+ * the console. We print out a special canary at the end of the tests
+ * so that the tap driver can know when the testing is done.
+ *
+ * In addition double check for a test file that doesn't properly call
+ * QUnit.start() after its done setting up its tests.
+ *
+ * We also want to insert the current test name into all tap lines.
+ */
+const tap_regex = /^((not )?ok [0-9]+ (- )?)(.*)$/;
+qunitTap(QUnit, function() {
+ if (arguments.length == 1 && QUnit.config.current) {
+ const match = tap_regex.exec(arguments[0]);
+ if (match) {
+ console.log(match[1] + QUnit.config.current.testName + ": " + match[4]);
+ return;
+ }
+ }
+ console.log.apply(console, arguments);
+});
+
+export default QUnit;
diff --git a/pkg/lib/serverTime.js b/pkg/lib/serverTime.js
new file mode 100644
index 0000000..ae3023b
--- /dev/null
+++ b/pkg/lib/serverTime.js
@@ -0,0 +1,768 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import cockpit from "cockpit";
+import React, { useState } from "react";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button/index.js";
+import { DatePicker } from "@patternfly/react-core/dist/esm/components/DatePicker/index.js";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex/index.js";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form/index.js";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover/index.js";
+import { Select, SelectOption } from "@patternfly/react-core/dist/esm/deprecated/components/Select/index.js";
+import { Spinner } from "@patternfly/react-core/dist/esm/components/Spinner/index.js";
+import { TimePicker } from "@patternfly/react-core/dist/esm/components/TimePicker/index.js";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput/index.js";
+import { CloseIcon, ExclamationCircleIcon, InfoCircleIcon, PlusIcon } from "@patternfly/react-icons";
+import { show_modal_dialog } from "cockpit-components-dialog.jsx";
+import { useObject, useEvent } from "hooks.js";
+
+import * as service from "service.js";
+import * as timeformat from "timeformat.js";
+import * as python from "python.js";
+import get_timesync_backend_py from "./get-timesync-backend.py";
+
+import { superuser } from "superuser.js";
+
+import "serverTime.scss";
+
+const _ = cockpit.gettext;
+
+export function ServerTime() {
+ const self = this;
+ cockpit.event_target(self);
+
+ function emit_changed() {
+ self.dispatchEvent("changed");
+ }
+
+ let time_offset = null;
+ let remote_offset = null;
+
+ let client = null;
+ let timedate = null;
+
+ function connect() {
+ if (client) {
+ timedate.removeEventListener("changed", emit_changed);
+ client.close();
+ }
+ client = cockpit.dbus('org.freedesktop.timedate1', { superuser: "try" });
+ timedate = client.proxy();
+ timedate.addEventListener("changed", emit_changed);
+ client.subscribe({
+ interface: "org.freedesktop.DBus.Properties",
+ member: "PropertiesChanged"
+ }, ntp_updated);
+ }
+
+ const timedate1_service = service.proxy("dbus-org.freedesktop.timedate1.service");
+ const timesyncd_service = service.proxy("systemd-timesyncd.service");
+ const chronyd_service = service.proxy("chronyd.service");
+
+ timesyncd_service.addEventListener("changed", emit_changed);
+ chronyd_service.addEventListener("changed", emit_changed);
+
+ /*
+ * The time we return from here as its UTC time set to the
+ * server time. This is the only way to get predictable
+ * behavior.
+ */
+ Object.defineProperty(self, 'utc_fake_now', {
+ enumerable: true,
+ get: function get() {
+ const offset = time_offset + remote_offset;
+ return new Date(offset + (new Date()).valueOf());
+ }
+ });
+
+ Object.defineProperty(self, 'now', {
+ enumerable: true,
+ get: function get() {
+ return new Date(time_offset + (new Date()).valueOf());
+ }
+ });
+
+ self.format = function format(and_time) {
+ const options = { dateStyle: "medium", timeStyle: and_time ? "short" : undefined, timeZone: "UTC" };
+ return timeformat.formatter(options).format(self.utc_fake_now);
+ };
+
+ const updateInterval = window.setInterval(emit_changed, 30000);
+
+ self.wait = function wait() {
+ if (remote_offset === null)
+ return self.update();
+ return Promise.resolve();
+ };
+
+ self.update = function update() {
+ return cockpit.spawn(["date", "+%s:%z"], { err: "message" })
+ .then(data => {
+ const parts = data.trim().split(":");
+ const timems = parseInt(parts[0], 10) * 1000;
+ let tzmin = parseInt(parts[1].slice(-2), 10);
+ const tzhour = parseInt(parts[1].slice(0, -2));
+ if (tzhour < 0)
+ tzmin = -tzmin;
+ const offsetms = (tzhour * 3600000) + tzmin * 60000;
+ const now = new Date();
+ time_offset = (timems - now.valueOf());
+ remote_offset = offsetms;
+ emit_changed();
+ })
+ .catch(ex => console.log("Couldn't calculate server time offset: " + cockpit.message(ex)));
+ };
+
+ /* There is no way to make sense of this date without a round trip to the
+ * server, as the timezone is really server specific. */
+ self.change_time = (datestr, timestr) => cockpit.spawn(["date", "--date=" + datestr + " " + timestr, "+%s"])
+ .then(data => {
+ const seconds = parseInt(data.trim(), 10);
+ return timedate.call('SetTime', [seconds * 1000 * 1000, false, true])
+ .then(self.update);
+ });
+
+ self.bump_time = function (millis) {
+ return timedate.call('SetTime', [millis, true, true]);
+ };
+
+ self.get_time_zone = function () {
+ return timedate.Timezone;
+ };
+
+ self.set_time_zone = function (tz) {
+ return timedate.call('SetTimezone', [tz, true]);
+ };
+
+ self.poll_ntp_synchronized = () => client.call(
+ timedate.path, "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTPSynchronized"])
+ .then(result => {
+ const ifaces = { "org.freedesktop.timedate1": { NTPSynchronized: result[0].v } };
+ const data = { };
+ data[timedate.path] = ifaces;
+ client.notify(data);
+ })
+ .catch(error => {
+ if (error.name != "org.freedesktop.DBus.Error.UnknownProperty" &&
+ error.problem != "not-found")
+ console.log("can't get NTPSynchronized property", error);
+ });
+
+ let ntp_waiting_value = null;
+ let ntp_waiting_resolve = null;
+
+ function ntp_updated(path, iface, member, args) {
+ if (!ntp_waiting_resolve || !args[1].NTP)
+ return;
+ if (ntp_waiting_value !== args[1].NTP.v)
+ console.warn("Unexpected value of NTP");
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ }
+
+ self.set_ntp = function set_ntp(val) {
+ const promise = new Promise((resolve, reject) => {
+ ntp_waiting_resolve = resolve;
+ });
+ ntp_waiting_value = val;
+ client.call(timedate.path,
+ "org.freedesktop.DBus.Properties", "Get", ["org.freedesktop.timedate1", "NTP"])
+ .then(result => {
+ // Check if don't want to enable enabled or disable disabled
+ if (result[0].v === val) {
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ return;
+ }
+ timedate.call('SetNTP', [val, true])
+ .catch(e => {
+ ntp_waiting_resolve();
+ ntp_waiting_resolve = null;
+ console.error("Failed to call SetNTP:", e.message); // not-covered: OS error
+ });
+ });
+ return promise;
+ };
+
+ self.get_ntp_active = function () {
+ return timedate.NTP;
+ };
+
+ self.get_ntp_supported = function () {
+ return timedate.CanNTP;
+ };
+
+ self.get_ntp_status = function () {
+ const status = {
+ initialized: false,
+ active: false,
+ synch: false,
+ service: null,
+ server: null,
+ sub_status: null
+ };
+
+ // flag for tests that timedated/timesyncd proxies got initialized
+ if (timedate.CanNTP !== undefined &&
+ timedate1_service.unit && timedate1_service.unit.Id &&
+ timesyncd_service.exists !== null &&
+ chronyd_service.exists !== null)
+ status.initialized = true;
+
+ status.active = timedate.NTP;
+ status.synch = timedate.NTPSynchronized;
+
+ const timesyncd_server_regex = /.*time server (.*)\./i;
+
+ const timesyncd_status = (timesyncd_service.state == "running" &&
+ timesyncd_service.service?.StatusText);
+
+ if (timesyncd_service.state == "running")
+ status.service = "systemd-timesyncd.service";
+ else if (chronyd_service.state == "running")
+ status.service = "chronyd.service";
+
+ if (timesyncd_status) {
+ const match = timesyncd_status.match(timesyncd_server_regex);
+ if (match)
+ status.server = match[1];
+ else if (timesyncd_status != "Idle." && timesyncd_status !== "")
+ status.sub_status = timesyncd_status;
+ }
+
+ return status;
+ };
+
+ function get_timesync_backend() {
+ return python.spawn(get_timesync_backend_py, [], { superuser: "try", err: "message" })
+ .then(data => {
+ const unit = data.trim();
+ if (unit == "systemd-timesyncd.service")
+ return "timesyncd";
+ else if (unit == "chrony.service" || unit == "chronyd.service")
+ return "chronyd";
+ else
+ return null;
+ });
+ }
+
+ function get_custom_ntp_timesyncd() {
+ const custom_ntp_config_file = cockpit.file("/etc/systemd/timesyncd.conf.d/50-cockpit.conf",
+ { superuser: "try" });
+
+ const result = {
+ backend: "timesyncd",
+ enabled: false,
+ servers: []
+ };
+
+ return custom_ntp_config_file.read()
+ .then(function(text) {
+ let ntp_line = "";
+ if (text) {
+ result.enabled = true;
+ text.split("\n").forEach(function(line) {
+ if (line.indexOf("NTP=") === 0) {
+ ntp_line = line.slice(4);
+ result.enabled = true;
+ } else if (line.indexOf("#NTP=") === 0) {
+ ntp_line = line.slice(5);
+ result.enabled = false;
+ }
+ });
+
+ result.servers = ntp_line.split(" ").filter(function(val) {
+ return val !== "";
+ });
+ if (result.servers.length === 0)
+ result.enabled = false;
+ }
+ return result;
+ })
+ .catch(function(error) {
+ console.warn("failed to load time servers", error);
+ return result;
+ });
+ }
+
+ function set_custom_ntp_timesyncd(config) {
+ const custom_ntp_config_file = cockpit.file("/etc/systemd/timesyncd.conf.d/50-cockpit.conf",
+ { superuser: true });
+
+ const text = `# This file is automatically generated by Cockpit\n\n[Time]\n${config.enabled ? "" : "#"}NTP=${config.servers.join(" ")}\n`;
+
+ return cockpit.spawn(["mkdir", "-p", "/etc/systemd/timesyncd.conf.d"], { superuser: true })
+ .then(() => custom_ntp_config_file.replace(text));
+ }
+
+ const chronyd_sourcedir = "/etc/chrony/sources.d";
+ const chronyd_sources_enabled = chronyd_sourcedir + "/cockpit.sources";
+ const chronyd_sources_disabled = chronyd_sourcedir + "/cockpit.disabled";
+
+ function get_custom_ntp_chronyd() {
+ const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: "try" });
+ const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: "try" });
+
+ function parse_servers(data) {
+ if (!data)
+ return [];
+ const servers = [];
+ data.split("\n").forEach(function(line) {
+ const parts = line.split(" ");
+ if (parts[0] == "server")
+ servers.push(parts[1]);
+ });
+ return servers;
+ }
+
+ return enabled_file.read()
+ .then(data => {
+ if (data) {
+ return {
+ backend: "chronyd",
+ enabled: true,
+ servers: parse_servers(data)
+ };
+ } else {
+ return disabled_file.read()
+ .then(data => {
+ return {
+ backend: "chronyd",
+ enabled: false,
+ servers: parse_servers(data)
+ };
+ });
+ }
+ });
+ }
+
+ function set_custom_ntp_chronyd(config) {
+ const enabled_file = cockpit.file(chronyd_sources_enabled, { superuser: true });
+ const disabled_file = cockpit.file(chronyd_sources_disabled, { superuser: true });
+
+ const text = "# This file is automatically generated by Cockpit\n\n" + config.servers.map(s => `server ${s}\n`).join("");
+
+ // HACK - https://bugzilla.redhat.com/show_bug.cgi?id=2168863
+ function ensure_sourcedir() {
+ function add_sourcedir(data) {
+ const line = "sourcedir " + chronyd_sourcedir;
+ if (data && data.indexOf(line) == -1)
+ data += "\n# Added by Cockpit\n" + line + "\n";
+ return data;
+ }
+ return cockpit.file("/etc/chrony.conf", { superuser: true }).modify(add_sourcedir);
+ }
+
+ return cockpit.spawn(["mkdir", "-p", chronyd_sourcedir], { superuser: true })
+ .then(() => {
+ if (config.enabled)
+ return enabled_file.replace(text).then(() => disabled_file.replace(null)).then(ensure_sourcedir);
+ else
+ return disabled_file.replace(text).then(() => enabled_file.replace(null));
+ });
+ }
+
+ self.get_custom_ntp = function () {
+ return get_timesync_backend().then(backend => {
+ if (backend == "timesyncd") {
+ return get_custom_ntp_timesyncd();
+ } else if (backend == "chronyd") {
+ return get_custom_ntp_chronyd();
+ } else {
+ return Promise.resolve({ backend: null, servers: [], enabled: false });
+ }
+ });
+ };
+
+ self.set_custom_ntp = function (config) {
+ if (config.backend == "timesyncd") {
+ return set_custom_ntp_timesyncd(config);
+ } else if (config.backend == "chronyd") {
+ return set_custom_ntp_chronyd(config);
+ } else {
+ return Promise.resolve();
+ }
+ };
+
+ self.get_timezones = function() {
+ return cockpit.spawn(["/usr/bin/timedatectl", "list-timezones"])
+ .then(content => content.split('\n').filter(tz => tz != ""));
+ };
+
+ /* NTPSynchronized needs to be polled so we just do that
+ * always.
+ */
+
+ const ntp_poll_interval = window.setInterval(function() {
+ self.poll_ntp_synchronized();
+ }, 5000);
+
+ self.close = function close() {
+ window.clearInterval(updateInterval);
+ window.clearInterval(ntp_poll_interval);
+ client.close();
+ };
+
+ connect();
+ superuser.addEventListener("reconnect", connect);
+ self.update();
+}
+
+export function ServerTimeConfig() {
+ const server_time = useObject(() => new ServerTime(),
+ st => st.close(),
+ []);
+ useEvent(server_time, "changed");
+
+ const ntp = server_time.get_ntp_status();
+
+ const tz = server_time.get_time_zone();
+ const systime_button = (
+ <Button variant="link" id="system_information_systime_button"
+ onClick={ () => change_systime_dialog(server_time, tz) }
+ data-timedated-initialized={ntp?.initialized}
+ isInline isDisabled={!superuser.allowed || !tz}>
+ { server_time.format(true) }
+ </Button>);
+
+ let ntp_status = null;
+ if (ntp?.active) {
+ let icon; let header; let body = ""; let footer = null;
+ if (ntp.synch) {
+ icon = <InfoCircleIcon className="ct-info-circle" />;
+ header = _("Synchronized");
+ if (ntp.server)
+ body = <div>{cockpit.format(_("Synchronized with $0"), ntp.server)}</div>;
+ } else {
+ if (ntp.server) {
+ icon = <Spinner size="md" />;
+ header = _("Synchronizing");
+ body = <div>{cockpit.format(_("Trying to synchronize with $0"), ntp.server)}</div>;
+ } else {
+ icon = <ExclamationCircleIcon className="ct-exclamation-circle" />;
+ header = _("Not synchronized");
+ if (ntp.service) {
+ footer = (
+ <Button variant="link"
+ onClick={() => cockpit.jump("/system/services#/" +
+ encodeURIComponent(ntp.service))}>
+ {_("Log messages")}
+ </Button>);
+ }
+ }
+ }
+
+ if (ntp.sub_status) {
+ body = <>{body}<div>{ntp.sub_status}</div></>;
+ }
+
+ ntp_status = (
+ <Popover headerContent={header} bodyContent={body} footerContent={footer}>
+ {icon}
+ </Popover>);
+ }
+
+ return (
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {systime_button}
+ {ntp_status}
+ </Flex>
+ );
+}
+
+function Validated({ errors, error_key, children }) {
+ const error = errors?.[error_key];
+ // We need to always render the <div> for the has-error
+ // class so that the input field keeps the focus when
+ // errors are cleared. Otherwise the DOM changes enough
+ // for the Browser to remove focus.
+ return (
+ <div className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
+ { children }
+ { error ? <span className="help-block dialog-error">{error}</span> : null }
+ </div>
+ );
+}
+
+function ValidatedInput({ errors, error_key, children }) {
+ const error = errors?.[error_key];
+ return (
+ <span className={error ? "ct-validation-wrapper has-error" : "ct-validation-wrapper"}>
+ { children }
+ </span>
+ );
+}
+
+function ChangeSystimeBody({ state, errors, change }) {
+ const [zonesOpen, setZonesOpen] = useState(false);
+ const [modeOpen, setModeOpen] = useState(false);
+
+ const {
+ time_zone, time_zones,
+ mode,
+ manual_date, manual_time,
+ ntp_supported, custom_ntp
+ } = state;
+
+ function add_server(event, index) {
+ custom_ntp.servers.splice(index + 1, 0, "");
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ function remove_server(event, index) {
+ custom_ntp.servers.splice(index, 1);
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ function change_server(event, index, value) {
+ custom_ntp.servers[index] = value;
+ change("custom_ntp", custom_ntp);
+ event.stopPropagation();
+ event.preventDefault();
+ return false;
+ }
+
+ const ntp_servers = (
+ custom_ntp.servers.map((s, i) => (
+ <Flex className="ntp-server-input-group" spaceItems={{ default: 'spaceItemsSm' }} key={i}>
+ <FlexItem grow={{ default: 'grow' }}>
+ <TextInput value={s} placeholder={_("NTP server")} aria-label={_("NTP server")}
+ onChange={(event, value) => change_server(event, i, value)} />
+ </FlexItem>
+ <Button variant="secondary" onClick={event => add_server(event, i)}
+ icon={ <PlusIcon /> } />
+ <Button variant="secondary" onClick={event => remove_server(event, i)}
+ icon={ <CloseIcon /> } isDisabled={i === custom_ntp.servers.length - 1} />
+ </Flex>
+ ))
+ );
+
+ const mode_options = [
+ <SelectOption key="manual_time" value="manual_time">{_("Manually")}</SelectOption>,
+ <SelectOption key="ntp_time" value="ntp_time" isDisabled={!ntp_supported}>{_("Automatically using NTP")}</SelectOption>
+ ];
+
+ if (custom_ntp.backend)
+ mode_options.push(
+ <SelectOption key="ntp_time_custom" value="ntp_time_custom" isDisabled={!ntp_supported}>
+ { custom_ntp.backend == "chronyd"
+ ? _("Automatically using additional NTP servers")
+ : _("Automatically using specific NTP servers")
+ }
+ </SelectOption>);
+
+ return (
+ <Form isHorizontal>
+ <FormGroup fieldId="systime-timezones" label={_("Time zone")}>
+ <Validated errors={errors} error_key="time_zone">
+ <Select id="systime-timezones" variant="typeahead"
+ isOpen={zonesOpen} onToggle={(_, isOpen) => setZonesOpen(isOpen)}
+ selections={time_zone}
+ onSelect={(event, value) => { setZonesOpen(false); change("time_zone", value) }}
+ menuAppendTo="parent">
+ { time_zones.map(tz => <SelectOption key={tz} value={tz}>{tz.replaceAll("_", " ")}</SelectOption>) }
+ </Select>
+ </Validated>
+ </FormGroup>
+ <FormGroup fieldId="change_systime" label={_("Set time")} isStack>
+ <Select id="change_systime"
+ isOpen={modeOpen} onToggle={(_, isOpen) => setModeOpen(isOpen)}
+ selections={mode} onSelect={(event, value) => { setModeOpen(false); change("mode", value) }}
+ menuAppendTo="parent">
+ { mode_options }
+ </Select>
+ { mode == "manual_time" &&
+ <Flex spaceItems={{ default: 'spaceItemsSm' }} id="systime-manual-row">
+ <ValidatedInput errors={errors} error_key="manual_date">
+ <DatePicker id="systime-date-input"
+ aria-label={_("Pick date")}
+ buttonAriaLabel={_("Toggle date picker")}
+ dateFormat={timeformat.dateShort}
+ dateParse={timeformat.parseShortDate}
+ invalidFormatText=""
+ locale={timeformat.dateFormatLang()}
+ weekStart={timeformat.firstDayOfWeek()}
+ placeholder={timeformat.dateShortFormat()}
+ onChange={(_, d) => change("manual_date", d)}
+ value={manual_date}
+ appendTo={() => document.body} />
+ </ValidatedInput>
+ <ValidatedInput errors={errors} error_key="manual_time">
+ <TimePicker id="systime-time-input"
+ className="ct-serverTime-time-picker"
+ time={manual_time}
+ is24Hour
+ menuAppendTo={() => document.body}
+ invalidFormatErrorMessage=""
+ onChange={(e, time, h, m, s, valid) => change("manual_time", time, valid) } />
+ </ValidatedInput>
+ <Validated errors={errors} error_key="manual_date" />
+ <Validated errors={errors} error_key="manual_time" />
+ </Flex>
+ }
+ { mode == "ntp_time_custom" &&
+ <Validated errors={errors} error_key="ntp_servers">
+ <div id="systime-ntp-servers">
+ { ntp_servers }
+ </div>
+ </Validated>
+ }
+ </FormGroup>
+ </Form>
+ );
+}
+
+function has_errors(errors) {
+ for (const field in errors) {
+ if (errors[field])
+ return true;
+ }
+ return false;
+}
+
+function change_systime_dialog(server_time, timezone) {
+ let dlg = null;
+ const state = {
+ time_zone: timezone,
+ time_zones: null,
+ mode: null,
+ ntp_supported: server_time.get_ntp_supported(),
+ custom_ntp: null,
+ manual_time_valid: true,
+ };
+ let errors = { };
+
+ function get_current_time() {
+ state.manual_date = server_time.format();
+
+ const minutes = server_time.utc_fake_now.getUTCMinutes();
+ // normalize to two digits
+ const minutes_str = (minutes < 10) ? "0" + minutes.toString() : minutes.toString();
+ state.manual_time = `${server_time.utc_fake_now.getUTCHours()}:${minutes_str}`;
+ }
+
+ function change(field, value, isValid) {
+ state[field] = value;
+ errors = { };
+
+ if (field == "mode" && value == "manual_time")
+ get_current_time();
+
+ if (field == "manual_time")
+ state.manual_time_valid = value && isValid;
+
+ update();
+ }
+
+ function validate() {
+ errors = { };
+
+ if (state.time_zone == "")
+ errors.time_zone = _("Invalid timezone");
+
+ if (state.mode == "manual_time") {
+ const new_date = new Date(state.manual_date);
+ if (isNaN(new_date.getTime()) || new_date.getTime() < 0)
+ errors.manual_date = _("Invalid date format");
+
+ if (!state.manual_time_valid)
+ errors.manual_time = _("Invalid time format");
+ }
+
+ if (state.mode == "ntp_time_custom") {
+ if (state.custom_ntp.servers.filter(s => !!s).length == 0)
+ errors.ntp_servers = _("Need at least one NTP server");
+ }
+
+ return !has_errors(errors);
+ }
+
+ function apply() {
+ return server_time.set_time_zone(state.time_zone)
+ .then(() => {
+ if (state.mode == "manual_time") {
+ return server_time.set_ntp(false)
+ .then(() => server_time.change_time(state.manual_date,
+ state.manual_time));
+ } else {
+ // Switch off NTP, write the config file, and switch NTP back on
+ state.custom_ntp.enabled = (state.mode == "ntp_time_custom");
+ state.custom_ntp.servers = state.custom_ntp.servers.filter(s => !!s);
+ return server_time.set_ntp(false)
+ .then(() => server_time.set_custom_ntp(state.custom_ntp))
+ .then(() => server_time.set_ntp(true));
+ }
+ });
+ }
+
+ function update() {
+ const props = {
+ id: "system_information_change_systime",
+ title: _("Change system time"),
+ body: <ChangeSystimeBody state={state} errors={errors} change={change} />
+ };
+
+ const footer = {
+ actions: [
+ {
+ caption: _("Change"),
+ style: "primary",
+ clicked: () => {
+ if (validate()) {
+ return apply();
+ } else {
+ update();
+ return Promise.reject();
+ }
+ }
+ }
+ ]
+ };
+
+ if (!dlg)
+ dlg = show_modal_dialog(props, footer);
+ else {
+ dlg.setProps(props);
+ dlg.setFooterProps(footer);
+ }
+ }
+
+ Promise.all([server_time.get_custom_ntp(), server_time.get_timezones()])
+ .then(([custom_ntp, time_zones]) => {
+ if (custom_ntp.servers.length == 0)
+ custom_ntp.servers = [""];
+ state.custom_ntp = custom_ntp;
+ state.time_zones = time_zones;
+ if (server_time.get_ntp_active()) {
+ if (custom_ntp.enabled)
+ state.mode = "ntp_time_custom";
+ else
+ state.mode = "ntp_time";
+ } else {
+ state.mode = "manual_time";
+ get_current_time();
+ }
+ update();
+ });
+}
diff --git a/pkg/lib/serverTime.scss b/pkg/lib/serverTime.scss
new file mode 100644
index 0000000..454ca7c
--- /dev/null
+++ b/pkg/lib/serverTime.scss
@@ -0,0 +1,7 @@
+.ct-serverTime-time-picker {
+ max-inline-size: 7rem;
+}
+
+.ntp-server-input-group:not(:last-child) {
+ margin-block-end: var(--pf-v5-global--spacer--sm);
+}
diff --git a/pkg/lib/service.js b/pkg/lib/service.js
new file mode 100644
index 0000000..3ef1878
--- /dev/null
+++ b/pkg/lib/service.js
@@ -0,0 +1,344 @@
+import cockpit from "cockpit";
+
+/* SERVICE MANAGEMENT API
+ *
+ * The "service" module lets you monitor and manage a
+ * system service on localhost in a simple way.
+ *
+ * It mainly exists because talking to the systemd D-Bus API is
+ * not trivial enough to do it directly.
+ *
+ * - proxy = service.proxy(name)
+ *
+ * Create a proxy that represents the service named NAME.
+ *
+ * The proxy has properties and methods (described below) that
+ * allow you to monitor the state of the service, and perform
+ * simple actions on it.
+ *
+ * Initially, any of the properties can be "null" until their
+ * actual values have been retrieved in the background.
+ *
+ * - proxy.addEventListener('changed', event => { ... })
+ *
+ * The 'changed' event is emitted whenever one of the properties
+ * of the proxy changes.
+ *
+ * - proxy.exists
+ *
+ * A boolean that tells whether the service is known or not. A
+ * proxy with 'exists == false' will have 'state == undefined' and
+ * 'enabled == undefined'.
+ *
+ * - proxy.state
+ *
+ * Either 'undefined' when the state can't be retrieved, or a
+ * string that has one of the values "starting", "running",
+ * "stopping", "stopped", or "failed".
+ *
+ * - proxy.enabled
+ *
+ * Either 'undefined' when the value can't be retrieved, or a
+ * boolean that tells whether the service is started 'enabled'.
+ * What it means exactly for a service to be enabled depends on
+ * the service, but an enabled service is usually started on boot,
+ * no matter whether other services need it or not. A disabled
+ * service is usually only started when it is needed by some other
+ * service.
+ *
+ * - proxy.unit
+ * - proxy.details
+ *
+ * The raw org.freedesktop.systemd1.Unit and type-specific D-Bus
+ * interface proxies for the service.
+ *
+ * - proxy.service
+ *
+ * The deprecated name for proxy.details
+ *
+ * - promise = proxy.start()
+ *
+ * Start the service. The return value is a standard jQuery
+ * promise as returned from DBusClient.call.
+ *
+ * - promise = proxy.restart()
+ *
+ * Restart the service.
+ *
+ * - promise = proxy.tryRestart()
+ *
+ * Try to restart the service if it's running or starting
+ *
+ * - promise = proxy.stop()
+ *
+ * Stop the service.
+ *
+ * - promise = proxy.enable()
+ *
+ * Enable the service.
+ *
+ * - promise = proxy.disable()
+ *
+ * Disable the service.
+ *
+ * - journal = proxy.getRunJournal(options)
+ *
+ * Return the journal of the current (if running) or recent (if failed/stopped) service run,
+ * similar to `systemctl status`. `options` is an optional array that gets appended to the `journalctl` call.
+ */
+
+let systemd_client;
+let systemd_manager;
+
+function wait_valid(proxy, callback) {
+ proxy.wait(() => {
+ if (proxy.valid)
+ callback();
+ });
+}
+
+function with_systemd_manager(done) {
+ if (!systemd_manager) {
+ // cached forever, only used for reading/watching; no superuser
+ systemd_client = cockpit.dbus("org.freedesktop.systemd1");
+ systemd_manager = systemd_client.proxy("org.freedesktop.systemd1.Manager",
+ "/org/freedesktop/systemd1");
+ wait_valid(systemd_manager, () => {
+ systemd_manager.Subscribe()
+ .catch(error => {
+ if (error.name != "org.freedesktop.systemd1.AlreadySubscribed" &&
+ error.name != "org.freedesktop.DBus.Error.FileExists")
+ console.warn("Subscribing to systemd signals failed", error);
+ });
+ });
+ }
+ wait_valid(systemd_manager, done);
+}
+
+export function proxy(name, kind) {
+ const self = {
+ exists: null,
+ state: null,
+ enabled: null,
+
+ wait,
+
+ start,
+ stop,
+ restart,
+ tryRestart,
+
+ enable,
+ disable,
+
+ getRunJournal,
+ };
+
+ cockpit.event_target(self);
+
+ let unit, details;
+ let wait_promise_resolve;
+ const wait_promise = new Promise(resolve => { wait_promise_resolve = resolve });
+
+ if (name.indexOf(".") == -1)
+ name = name + ".service";
+ if (kind === undefined)
+ kind = "Service";
+
+ function update_from_unit() {
+ self.exists = (unit.LoadState != "not-found" || unit.ActiveState != "inactive");
+
+ if (unit.ActiveState == "activating")
+ self.state = "starting";
+ else if (unit.ActiveState == "deactivating")
+ self.state = "stopping";
+ else if (unit.ActiveState == "active" || unit.ActiveState == "reloading")
+ self.state = "running";
+ else if (unit.ActiveState == "failed")
+ self.state = "failed";
+ else if (unit.ActiveState == "inactive" && self.exists)
+ self.state = "stopped";
+ else
+ self.state = undefined;
+
+ if (unit.UnitFileState == "enabled" || unit.UnitFileState == "linked")
+ self.enabled = true;
+ else if (unit.UnitFileState == "disabled" || unit.UnitFileState == "masked")
+ self.enabled = false;
+ else
+ self.enabled = undefined;
+
+ self.unit = unit;
+
+ self.dispatchEvent("changed");
+ wait_promise_resolve();
+ }
+
+ function update_from_details() {
+ self.details = details;
+ self.service = details;
+ self.dispatchEvent("changed");
+ }
+
+ with_systemd_manager(function () {
+ systemd_manager.LoadUnit(name)
+ .then(path => {
+ unit = systemd_client.proxy('org.freedesktop.systemd1.Unit', path);
+ unit.addEventListener('changed', update_from_unit);
+ wait_valid(unit, update_from_unit);
+
+ details = systemd_client.proxy('org.freedesktop.systemd1.' + kind, path);
+ details.addEventListener('changed', update_from_details);
+ wait_valid(details, update_from_details);
+ })
+ .catch(() => {
+ self.exists = false;
+ self.dispatchEvent('changed');
+ });
+ });
+
+ function refresh() {
+ if (!unit || !details)
+ return Promise.resolve();
+
+ function refresh_interface(path, iface) {
+ return systemd_client.call(path, "org.freedesktop.DBus.Properties", "GetAll", [iface])
+ .then(([result]) => {
+ const props = { };
+ for (const p in result)
+ props[p] = result[p].v;
+ systemd_client.notify({ [unit.path]: { [iface]: props } });
+ })
+ .catch(error => console.log(error));
+ }
+
+ return Promise.allSettled([
+ refresh_interface(unit.path, "org.freedesktop.systemd1.Unit"),
+ refresh_interface(details.path, "org.freedesktop.systemd1." + kind),
+ ]);
+ }
+
+ function on_job_new_removed_refresh(event, number, path, unit_id, result) {
+ if (unit_id == name)
+ refresh();
+ }
+
+ /* HACK - https://bugs.freedesktop.org/show_bug.cgi?id=69575
+ *
+ * We need to explicitly get new property values when getting
+ * a UnitNew signal since UnitNew doesn't carry them.
+ * However, reacting to UnitNew with GetAll could lead to an
+ * infinite loop since systemd emits a UnitNew in reaction to
+ * GetAll for units that it doesn't want to keep loaded, such
+ * as units without unit files.
+ *
+ * So we ignore UnitNew and instead assume that the unit state
+ * only changes in interesting ways when there is a job for it
+ * or when the daemon is reloaded (or when we get a property
+ * change notification, of course).
+ */
+
+ // This is what we want to do:
+ // systemd_manager.addEventListener("UnitNew", function (event, unit_id, path) {
+ // if (unit_id == name)
+ // refresh();
+ // });
+
+ // This is what we have to do:
+ systemd_manager.addEventListener("Reloading", (event, reloading) => {
+ if (!reloading)
+ refresh();
+ });
+
+ systemd_manager.addEventListener("JobNew", on_job_new_removed_refresh);
+ systemd_manager.addEventListener("JobRemoved", on_job_new_removed_refresh);
+
+ function wait(callback) {
+ wait_promise.then(callback);
+ }
+
+ /* Actions
+ *
+ * We don't call methods on the persistent systemd_client, as that does not have superuser
+ */
+
+ function call_manager(dbus, method, args) {
+ return dbus.call("/org/freedesktop/systemd1",
+ "org.freedesktop.systemd1.Manager",
+ method, args);
+ }
+
+ function call_manager_with_job(method, args) {
+ return new Promise((resolve, reject) => {
+ const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
+ let pending_job_path;
+
+ const subscription = dbus.subscribe(
+ { interface: "org.freedesktop.systemd1.Manager", member: "JobRemoved" },
+ (_path, _iface, _signal, [_number, path, _unit_id, result]) => {
+ if (path == pending_job_path) {
+ subscription.remove();
+ dbus.close();
+ refresh().then(() => {
+ if (result === "done")
+ resolve();
+ else
+ reject(new Error(`systemd job ${method} ${JSON.stringify(args)} failed with result ${result}`));
+ });
+ }
+ });
+
+ call_manager(dbus, method, args)
+ .then(([path]) => { pending_job_path = path })
+ .catch(() => {
+ dbus.close();
+ reject();
+ });
+ });
+ }
+
+ function call_manager_with_reload(method, args) {
+ const dbus = cockpit.dbus("org.freedesktop.systemd1", { superuser: "try" });
+ return call_manager(dbus, method, args)
+ .then(() => call_manager(dbus, "Reload", []))
+ .then(refresh)
+ .finally(dbus.close);
+ }
+
+ function start() {
+ return call_manager_with_job("StartUnit", [name, "replace"]);
+ }
+
+ function stop() {
+ return call_manager_with_job("StopUnit", [name, "replace"]);
+ }
+
+ function restart() {
+ return call_manager_with_job("RestartUnit", [name, "replace"]);
+ }
+
+ function tryRestart() {
+ return call_manager_with_job("TryRestartUnit", [name, "replace"]);
+ }
+
+ function enable() {
+ return call_manager_with_reload("EnableUnitFiles", [[name], false, false]);
+ }
+
+ function disable() {
+ return call_manager_with_reload("DisableUnitFiles", [[name], false]);
+ }
+
+ function getRunJournal(options) {
+ if (!details || !details.ExecMainStartTimestamp)
+ return Promise.reject(new Error("getRunJournal(): unit is not known"));
+
+ // collect the service journal since start time; property is μs, journal wants s
+ const startTime = Math.floor(details.ExecMainStartTimestamp / 1000000);
+ return cockpit.spawn(
+ ["journalctl", "--unit", name, "--since=@" + startTime.toString()].concat(options || []),
+ { superuser: "try", error: "message" });
+ }
+
+ return self;
+}
diff --git a/pkg/lib/superuser.js b/pkg/lib/superuser.js
new file mode 100644
index 0000000..5753b2c
--- /dev/null
+++ b/pkg/lib/superuser.js
@@ -0,0 +1,126 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import cockpit from "cockpit";
+
+/* import { superuser } from "superuser";
+ *
+ * The "superuser" object indicates whether or not the current page
+ * can open superuser channels.
+ *
+ * - superuser.allowed
+ *
+ * This is true when the page can open superuser channels, and false
+ * otherwise. This field might be "null" while the page or the Cockpit
+ * session itself is still initializing.
+ *
+ * UI elements that trigger actions that need administrative access
+ * should be hidden when the "allowed" field is false or null. (If
+ * those elements also show information, such as with checkboxes or
+ * toggle buttons, disable them instead of hiding.)
+ *
+ * UI elements that alert the user that they don't have administrative
+ * access should be shown when the "allowed" field is exactly false,
+ * but not when it is null.
+ *
+ * - superuser.addEventListener("changed", () => ...)
+ *
+ * The event handler is called whenever superuser.allowed has changed.
+ * A page should update its appearance according to superuser.allowed.
+ *
+ * - superuser.addEventListener("reconnect", () => ...)
+ *
+ * The event handler is called whenever channels should be re-opened
+ * that use the "superuser" option.
+ *
+ * The difference between "reconnect" and "connect" is that the
+ * "reconnect" signal does not trigger when superuser.allowed goes
+ * from "null" to its first real value. You don't need to re-open
+ * channels in this case, and it happens on every page load, so this
+ * is important to avoid.
+ *
+ * - superuser.reload_page_on_change()
+ *
+ * Calling this function instructs the "superuser" object to reload
+ * the page whenever "superuser.allowed" changes. This is a (bad)
+ * alternative to re-initializing the page and intended to be used
+ * only to help with the transition.
+ *
+ * Even if you are using "superuser.reload_page_on_change" to avoid having
+ * to re-initialize your page dynamically, you should still use the
+ * "changed" event to update the page appearance since
+ * "superuser.allowed" might still change a couple of times right
+ * after page reload.
+ */
+
+function Superuser() {
+ const proxy = cockpit.dbus(null, { bus: "internal" }).proxy("cockpit.Superuser", "/superuser");
+ let reload_on_change = false;
+
+ const compute_allowed = () => {
+ if (!proxy.valid || proxy.Current == "init")
+ return null;
+ return proxy.Current != "none";
+ };
+
+ const self = {
+ allowed: compute_allowed(),
+ reload_page_on_change
+ };
+
+ cockpit.event_target(self);
+
+ function changed(allowed) {
+ if (self.allowed != allowed) {
+ if (self.allowed != null && reload_on_change) {
+ window.location.reload(true);
+ } else {
+ const prev = self.allowed;
+ self.allowed = allowed;
+ self.dispatchEvent("changed");
+ if (prev != null)
+ self.dispatchEvent("reconnect");
+ }
+ }
+ }
+
+ proxy.wait(() => {
+ if (!proxy.valid) {
+ // Fall back to cockpit.permissions
+ const permission = cockpit.permission({ admin: true });
+ const update = () => {
+ changed(permission.allowed);
+ };
+ permission.addEventListener("changed", update);
+ update();
+ }
+ });
+
+ proxy.addEventListener("changed", () => {
+ changed(compute_allowed());
+ });
+
+ function reload_page_on_change() {
+ reload_on_change = true;
+ }
+
+ return self;
+}
+
+export const superuser = Superuser();
diff --git a/pkg/lib/table.css b/pkg/lib/table.css
new file mode 100644
index 0000000..63277cf
--- /dev/null
+++ b/pkg/lib/table.css
@@ -0,0 +1,138 @@
+.panel .table {
+ font-size: var(--pf-v5-global--FontSize-s);
+}
+
+/* Panels don't draw borders between them */
+.panel > .table > tbody:first-child td {
+ border-block-start: 1px solid rgb(221 221 221);
+}
+
+/* Table headers should not generate a double border */
+.panel .table thead tr th {
+ border-block-end: none;
+}
+
+/* Fix panel heading alignment & mobile layout */
+
+.panel-heading {
+ align-items: center;
+ background: #f5f5f5;
+ display: flex;
+ flex-wrap: wrap;
+ /* (28px small size widget height) + (0.5rem * 2) */
+ min-block-size: calc(28px + 1rem);
+ padding-block: 0.5rem;
+ padding-inline: 1rem;
+ position: relative;
+ z-index: 100;
+}
+
+.panel-title {
+ font: inherit;
+ margin: 0;
+ padding: 0;
+}
+
+.panel-title > a {
+ color: var(--ct-color-link);
+ display: inline-block;
+}
+
+.panel-title > a:hover,
+.panel-title > a:focus {
+ color: var(--alert-info-text);
+}
+
+/* Allow children in the title to wrap */
+.panel-title > h3,
+.panel-title > a,
+.panel-title > div,
+.panel-title > span {
+ flex-shrink: 1;
+ word-break: break-all;
+}
+
+.panel-heading > :last-child:not(:first-child),
+.panel-heading > .panel-heading-actions {
+ flex: auto;
+ text-align: end;
+}
+
+@media screen and (max-width: 640px) {
+ /* Remove _most_ of the gaps on the sides of small screens */
+ /* to maximize space, but still keep the boxy panel look */
+ .col-md-12 > .panel {
+ margin-inline: -10px;
+ }
+
+ .panel {
+ /* Background fade */
+ --hi-color: #d1d1d1;
+ --hi-color2: var(--ct-global--palette--black-250);
+ --bg-color: var(--ct-color-bg);
+ --hi-width: 20px;
+ --hi-width3: calc(var(--hi-width) * 3);
+ --transparent: rgb(255 255 255 / 0%); /* WebKit needs white transparent */
+ max-inline-size: 100vw;
+ overflow-x: auto;
+ position: relative;
+ background-image:
+ linear-gradient(to left, var(--bg-color) var(--hi-width), var(--transparent) var(--hi-width3)),
+ linear-gradient(to left, var(--hi-color) 1px, var(--transparent) 2px, var(--hi-color2) 4px, var(--bg-color) var(--hi-width)),
+ linear-gradient(to right, var(--bg-color) var(--hi-width), var(--transparent) var(--hi-width3)),
+ linear-gradient(to right, var(--hi-color) 1px, var(--transparent) 2px, var(--hi-color2) 4px, var(--bg-color) var(--hi-width));
+ background-attachment: local, scroll, local, scroll;
+ background-position: right, right, left, left;
+ background-repeat: no-repeat;
+ background-size: var(--hi-width3) 100%;
+ }
+
+ .panel > .panel-heading {
+ position: sticky;
+ inset-inline-start: 0;
+ inset-block-start: 0;
+ }
+
+ .panel .table thead th {
+ white-space: nowrap;
+ }
+
+ .panel .table:not(:hover):not(:focus):not(:active) {
+ background: transparent;
+ }
+
+ .panel .table thead:not(:hover):not(:focus):not(:active) {
+ background: transparent;
+ }
+}
+
+.pf-v5-c-table__tr.pf-m-clickable:hover > td,
+.pf-v5-c-table__tr.pf-m-clickable:hover > th {
+ /* PF5 has no hover background color; we have to force the override for hover colors */
+ background-color: var(--ct-color-list-hover-bg) !important;
+ color: var(--ct-color-list-hover-text) !important;
+}
+
+/* Override patternfly to fit buttons and such */
+.table > thead > tr > th,
+.table > tbody > tr > td {
+ padding: 0.5rem;
+ vertical-align: baseline;
+}
+
+/* Override the heavy patternfly headers */
+.table > thead {
+ background-image: none;
+ background-color: var(--ct-color-bg);
+}
+
+/* Make things line up */
+.table tbody tr > :first-child,
+.table thead tr > :first-child {
+ padding-inline-start: 1rem;
+}
+
+.table tbody tr > :last-child,
+.table thead tr > :last-child {
+ padding-inline-end: 1rem;
+}
diff --git a/pkg/lib/timeformat.js b/pkg/lib/timeformat.js
new file mode 100644
index 0000000..4995e31
--- /dev/null
+++ b/pkg/lib/timeformat.js
@@ -0,0 +1,66 @@
+/* Wrappers around Intl.DateTimeFormat and date-fns which use Cockpit's current locale, and define a few standard formats.
+ * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/DateTimeFormat
+ *
+ * Time stamps are given in milliseconds since the epoch.
+ */
+import cockpit from "cockpit";
+import { parse, formatDistanceToNow } from 'date-fns';
+import * as locales from 'date-fns/locale';
+
+// this needs to be dynamic, as some pages don't initialize cockpit.language right away
+export const dateFormatLang = () => cockpit.language.replace('_', '-');
+
+const dateFormatLangDateFns = () => {
+ if (cockpit.language == "en") return "enUS";
+ else return cockpit.language.replace('_', '');
+};
+
+// general Intl.DateTimeFormat formatter object
+export const formatter = options => new Intl.DateTimeFormat(dateFormatLang(), options);
+
+// common formatters; try to use these as much as possible, for UI consistency
+// 07:41 AM
+export const time = t => formatter({ timeStyle: "short" }).format(t);
+// 7:41:26 AM
+export const timeSeconds = t => formatter({ timeStyle: "medium" }).format(t);
+// June 30, 2021
+export const date = t => formatter({ dateStyle: "long" }).format(t);
+// 06/30/2021
+export const dateShort = t => formatter().format(t);
+// Jun 30, 2021, 7:41 AM
+export const dateTime = t => formatter({ dateStyle: "medium", timeStyle: "short" }).format(t);
+// Jun 30, 2021, 7:41:23 AM
+export const dateTimeSeconds = t => formatter({ dateStyle: "medium", timeStyle: "medium" }).format(t);
+// Jun 30, 7:41 AM
+export const dateTimeNoYear = t => formatter({ month: 'short', day: '2-digit', hour: '2-digit', minute: '2-digit' }).format(t);
+// Wednesday, June 30, 2021
+export const weekdayDate = t => formatter({ dateStyle: "full" }).format(t);
+
+// The following options are helpful for placeholders
+// yyyy/mm/dd
+export const dateShortFormat = () => locales[dateFormatLangDateFns()].formatLong.date({ width: 'short' });
+// about 1 hour [ago]
+export const distanceToNow = (t, addSuffix) => formatDistanceToNow(t, { locale: locales[dateFormatLangDateFns()], addSuffix });
+
+// Parse a string localized date like 30.06.21 to a Date Object
+export function parseShortDate(dateStr) {
+ const parsed = parse(dateStr, dateShortFormat(), new Date());
+
+ // Strip time which may cause bugs in calendar
+ const timePortion = parsed.getTime() % (3600 * 1000 * 24);
+ return new Date(parsed - timePortion);
+}
+
+/***
+ * sorely missing from Intl: https://github.com/tc39/ecma402/issues/6
+ * based on https://github.com/unicode-cldr/cldr-core/blob/master/supplemental/weekData.json#L59
+ * However, we don't have translations for most locales, and cockpit.language does not even contain
+ * the country in most cases, so this is just an approximation.
+ * Most locales start the week on Monday (day 1), so default to that and enumerate the others.
+ */
+
+const first_dow_sun = ['en', 'ja', 'ko', 'pt', 'pt_BR', 'sv', 'zh_CN', 'zh_TW'];
+
+export function firstDayOfWeek() {
+ return first_dow_sun.indexOf(cockpit.language) >= 0 ? 0 : 1;
+}
diff --git a/pkg/lib/utils.jsx b/pkg/lib/utils.jsx
new file mode 100644
index 0000000..f5b5a4e
--- /dev/null
+++ b/pkg/lib/utils.jsx
@@ -0,0 +1,33 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from "react";
+
+export function fmt_to_fragments(fmt) {
+ const args = Array.prototype.slice.call(arguments, 1);
+
+ function replace(part) {
+ if (part[0] == "$") {
+ return args[parseInt(part.slice(1))];
+ } else
+ return part;
+ }
+
+ return React.createElement.apply(null, [React.Fragment, { }].concat(fmt.split(/(\$[0-9]+)/g).map(replace)));
+}
diff --git a/plans/all.fmf b/plans/all.fmf
new file mode 100644
index 0000000..11e6f46
--- /dev/null
+++ b/plans/all.fmf
@@ -0,0 +1,23 @@
+discover:
+ how: fmf
+execute:
+ how: tmt
+
+# Let's handle them upstream only, don't break Fedora/RHEL reverse dependency gating
+environment:
+ TEST_AUDIT_NO_SELINUX: 1
+
+/system:
+ summary: Run tests on system podman
+ discover+:
+ test: /test/browser/system
+
+/user:
+ summary: Run tests on user podman
+ discover+:
+ test: /test/browser/user
+
+/misc:
+ summary: Run other tests
+ discover+:
+ test: /test/browser/other
diff --git a/po/cs.po b/po/cs.po
new file mode 100644
index 0000000..3f3cde7
--- /dev/null
+++ b/po/cs.po
@@ -0,0 +1,1511 @@
+# Pavel Borecki <pavel.borecki@gmail.com>, 2019. #zanata, 2020.
+# Matej Marusak <mmarusak@redhat.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-11-29 13:31+0000\n"
+"Last-Translator: Pavel Borecki <pavel.borecki@gmail.com>\n"
+"Language-Team: Czech <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/cs/>\n"
+"Language: cs\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 kontejner"
+msgstr[1] "$0 kontejnery"
+msgstr[2] "$0 kontejnerů"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 obraz celkem, $1"
+msgstr[1] "$0 obrazy celkem, $1"
+msgstr[2] "$0 obrazů celkem, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 sekunda"
+msgstr[1] "$0 sekundy"
+msgstr[2] "$0 sekund"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 nepoužitý obraz, $1"
+msgstr[1] "$0 nepoužité obrazy, $1"
+msgstr[2] "$0 nepoužitých obrazů, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 až 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+"Akce kterou podniknout jakmile kontejner přejde do stavu, kdy nebude v "
+"pořádku."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Přidat mapování portů"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Přidat proměnnou prostředí"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Přidat svazek"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Vše"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Všechny registry"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Vždy"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Došlo k chybě"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Autor"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Podman spouštět automaticky při zavádění systému"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "Procesor"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Nápověda ke sdílení procesoru"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Sdílení procesoru"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"Sdílení procesoru určuje prioritu spuštěných kontejnerů. Výchozí priorita je "
+"1024. Vyšší číslo upřednostňuje tento kontejner. Nižší číslo jeho přednost "
+"snižuje."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Storno"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Kontroluje se stav"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Kontrolní bod"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Kontrolní bod a podpora obnovení"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Kontejner kontrolního bodu $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Kliknutím zobrazíte publikované porty"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Kliknutím zobrazíte svazky"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Součást Cockpit pro Podman kontejnery"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Příkaz"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Komentáře"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Odeslat"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Odeslat kontejner"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Nastaveno"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konzole"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Kontejner"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Kontejner se nepodařilo vytvořit"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Kontejner se nepodařilo spustit"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Kontejner není spuštěný"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Název kontejneru"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Je třeba zadat název kontejneru."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Popis umístění kontejneru"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "Je třeba vyplnit popis umístění kontejneru"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Port kontejneru"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "Je třeba vyplnit port kontejneru"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Kontejnery"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Vytvořit"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Vytvořit nový obraz založený na stávajícím stavu kontejneru $0."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Vytvořit a spustit"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Vytvořit kontejner"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Vytvořit kontejner v $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Vytvořit kontejner v podu"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Vytvořit pod"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Vytvořeno"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Vytvořil"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Snížit podíl na procesoru"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Snížit interval"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Snížit limit počtu opětovných pokusů"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Zmenšit paměť"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Snížit počet opakovaných pokusů"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Snížit periodu startu"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Snížit časový limit"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Smazat"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "Smazat obraz $0?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Smazat $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Smazat obraz"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Smazat pod $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Smazat značkou označené obrazy"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Smazat nepoužívané systémové obrazy:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Smazat nepoužívané uživatelské obrazy:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Smazání kontejneru smaže veškerá data v něm."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "Smazání spuštěného kontejneru smaže veškerá data v něm."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Smazání tohoto podu odebere následující kontejnery:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Podrobnosti"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Prostor na disku"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr "Formát Docker se hodí pro sdílení obrazu s enginem Docker nebo Moby"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Stáhnout"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Stáhnout si nový obraz"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Prázdný pod $0 bude nenávratně odebrán."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Vstupní bod"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Proměnné prostředí"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Chyba"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Chybové hlášení"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Nastala chyba při připojování ke konzoli"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Příklad, vaše jméno <vasejmeno@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Příklad: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Skončilo"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Nepodařilo se spustit kontrolu vitálnosti"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Kontejner $0 se nepodařilo opatřit kontrolním bodem"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Nepodařilo se vyčistit kontejner"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Nepodařilo se odeslat kontejner $0"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Nepodařilo se vytvořit kontejner $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Nepodařilo se stáhnout obraz $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Nepodařilo se vynutit odebrání kontejneru $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Nepodařilo se vynutit odebrání obrazu $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Nepodařilo se vynutit restart podu $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Nepodařilo se vynutit zastavení podu $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Nepodařilo se pozastavit kontejner $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Nepodařilo se pozastavit pod $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Nepodařilo se prořezat nepoužívané kontejnery"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Nepodařilo se prořezat nepoužívané obrazy"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Nepodařilo se odeslat obraz $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Nepodařilo se odebrat kontejner $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Nepodařilo se odebrat obraz $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Nepodařilo se přejmenovat kontejner $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Nepodařilo se restartovat kontejner $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Nepodařilo se restartovat pod $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Nepodařilo se obnovit kontejner $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Nepodařilo se navázat v chodu kontejneru $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Nepodařilo se navázat v chodu podu $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Nepodařilo se spustit kontejner $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Nepodařilo se zkontrolovat vitálnost kontejneru $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Nepodařilo se vyhledat obrazy."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Nepodařilo se vyhledat obrazy: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Nepodařilo se vyhledat nové obrazy"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Nepodařilo se spustit kontejner $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Nepodařilo se spustit pod $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Nepodařilo se zastavit kontejner $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Nepodařilo se zastavit pod $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Selhávající streak"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Vynutit odeslání"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Vynutit smazání"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Vynutit smazání podu $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Vynutit restart"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Vynutit zastavení"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Brána"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Kontrola vitálnosti"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Nápověda k intervalu kontroly vitálnosti"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Nápověda k opakovaným pokusům o kontrolu vitálnosti"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Nápověda k periodě startu kontroly vitálnosti"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Nápověda k časovému limitu kontroly vitálnosti"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Nápověda k akci kontroly selhání"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "V pořádku"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Skrýt obrazy"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Skrýt obrazy mezi tím"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historie"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Popis umístění hostitele"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Port hostitele"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Nápověda k portu hostitele"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "Identif."
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP adresa"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Nápověda k IP adrese"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Ideální pro vývoj"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Ideální pro provozování služeb"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Pokud je IP adresa hostitele nastavena na 0.0.0.0 nebo vůbec nenastavena, "
+"port bude navázán na všechny IP adresy hostitele."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Pokud port hostitele není nastavený, port kontejneru bude náhodně přiřazen "
+"na port hostitele."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Pokud je nastavená staticky, IP adresu ignorovat"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Pokud je nastavená staticky, MAC adresu ignorovat"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Obraz"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Název obrazu už je použit"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Je třeba zadat název obrazu"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Nápověda k výběru obrazu"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Obrazy"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Zvýšit podíl na procesoru"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Prodloužit interval"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Zvýšit počet opětovných pokusů"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Zvětšit paměť"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Zvýšit počet opakování pokusů"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Zvýšit periodu startu"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Prodloužit časový limit"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Napojení"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Interval"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Jak často spouštět kontrolu vitálnosti."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Neplatné znaky. Název může obsahovat pouze písmena, číslice a vybranou "
+"interpunkci (_.-)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Ponechat všechny dočasné soubory kontrolních bodů"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Klíč"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "Je třeba vyplnit klíč"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Minulých 5 spuštění"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Nejnovější kontrolní bod"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Po zapsání kontrolního bodu na disk ponechat spuštěné"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Načítání podrobností…"
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Načítání záznamů událostí…"
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Načítání…"
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Lokální"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Lokální obrazy"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Záznamy událostí"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC adresa"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Maximální počet opětovných pokusů"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Operační paměť"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Limit operační paměti"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Jednotka operační paměti"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Režim"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Pro tento obraz existuje vícero štítků. Vyberte oštítkované obrazy, které "
+"smazat."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "Je třeba, aby byla platná IP adresa"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Název"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Název je už používán"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Nový název pro kontejner"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Název nového obrazu"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Ne"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Žádná akce"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Žádné kontejnery"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Tento obraz není používán žádným kontejnerem"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "V tomto podu nejsou žádné kontejnery"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Stávajícímu filtru neodpovídají žádné kontejnery"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Nezadány žádné proměnné prostředí"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Žádné obrazy"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Nenalezeny žádné obrazy"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Žádné obrazy které by odpovídaly stávajícímu filtru"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Žádný štítek"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Nejsou vystavené žádné porty"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Žádné výsledky pro $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Žádné spuštěné kontejnery"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Nejsou určeny žádné svazky"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Při nezdaru"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Pouze spuštěné"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Volby"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Vlastník"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Nápověda k vlastníkovi"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Prošlo kontrolou vitálnosti"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Pokud chcete hromadně naimportovat, zadejte do libovolné kolonky jeden či "
+"více řádků s dvojicemi klíč=hodnota"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Pozastavit"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Při vytváření obrazu pozastavit kontejner"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Pozastaveno"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Pod se nepodařilo vytvořit"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Název pro pod"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman kontejnery"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Služba podman není aktivní"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Mapování portů"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Porty"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Je možné mapovat porty do čísla 1024"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Soukromé"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokol"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Prořezat"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Prořezat nepoužívané kontejnery"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Prořezat nepoužívané obrazy"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Prořezávání kontejnerů"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Prořezávají se obrazy"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Stáhnout si nejnovější obraz"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Stahování"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Přístup pouze pro čtení"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Přístup pro čtení i zápis"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Odebrat položku"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Odebere označené (a nespuštěné) kontejnery"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Odebírá se"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Přejmenovat"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Přejmenovat kontejner $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Je možné nastavit limity prostředků"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Restart"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Pravidlo restartování"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Nápověda k pravidlu restartování"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr ""
+"Které pravidlo ohledně restartování následovat když se kontejnery ukončují."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Pravidlo ohledně restartování kterým se řídit při ukončení kontejnerů. Pokud "
+"je v rámci uživatelského účtu používán např. ecryptfs, systemd-homed, NFS "
+"nebo 2FA, pak se může stávat, že automatické spouštění pomocí linger za "
+"určitých podmínek nemusí zafungovat."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Obnovit"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Obnovit kontejner $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Obnovit s navázanými TCP spojeními"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Omezeno oprávněními uživatelského účtu"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Navázat v chodu"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Opětovných pokusů"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Zkusit znovu jiný pojem."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Zkontrolovat vitálnost"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Spuštěné"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Hledat podle názvu nebo popisu"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Hledat podle registru (repozitáře)"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Hledat"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Hledat obraz"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Hledat řetězec nebo umístění kontejneru"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Vyhledávání..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Vyhledávání: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Sdílené"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Zobrazit"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Zobrazit obrazy"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Zobrazit obrazy mezi tím"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Zobrazit méně"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Zobrazit více"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Velikost"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Spustit"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Perioda startu"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Spustit podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Vyhledávání obrazů zahájíte psaním."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Spuštěno v"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Stav"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Stav"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Zastavit"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Zastaveno"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Podpora pro zachování navázaných TCP spojení"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Systém"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Je také k dispozici systémová služba Podman"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Štítek"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Štítky"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Cockpit uživatelské rozhraní pro Podman kontejnery."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Doba potřebná pro inicializaci zavádění kontejneru."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Nejdelší přijatelná doba pro dokončení kontroly vitálnosti, než bude "
+"interval považován za nezdařilý."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Umožněný počet opětovných pokusů než bude výsledek kontroly vitálnosti "
+"považován za negativní."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Časový limit"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Řešení problémů"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Filtrujte psaním…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Nepodařilo se načíst historii obrazu"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Není v pořádku"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "V chodu od $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Použít původní Docker formát"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Používáno"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Uživatel"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Je také k dispozici uživatelská služba Podman"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Uživatel:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Hodnota"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Svazky"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Pokud v nepořádku"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "S terminálem"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Zapisovatelné"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "kontejner"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "stahuje se"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "hostitel[:port]/[uživatel]/kontejner[:štítek]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "obraz"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "v"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "mezi tím"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "Obraz mezi tím"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "n/a"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "není k dispozici"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "skupina podu"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "porty"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "sekund"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "systém"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "nepoužito"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "uživatel:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "svazky"
+
+#~ msgid "Delete $0"
+#~ msgstr "Smazat $0"
+
+#~ msgid "select all"
+#~ msgstr "vybrat vše"
+
+#~ msgid "Failure action"
+#~ msgstr "Akce při selhání"
+
+#~ msgid "Restarting"
+#~ msgstr "Restartuje se"
+
+#, fuzzy
+#~| msgid "Please confirm deletion of $0"
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Potvrďte smazání $0"
+
+#, fuzzy
+#~| msgid "Please confirm deletion of pod $0"
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Potvrďte smazání podu $0"
+
+#, fuzzy
+#~| msgid "Please confirm force deletion of pod $0"
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Potvrďte vynucení smazání podu $0"
+
+#, fuzzy
+#~| msgid "Please confirm forced deletion of $0"
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Potvrďte vynucení smazání $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontejner je nyní spuštěný."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Do exportu nezahrnovat změny v kořenovém souborovém systému"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Výchozí s jedním k výběru"
+
+#~ msgid "Start after creation"
+#~ msgstr "Po vytvoření spustit"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Smazat nepoužité $0 obrazy:"
+
+#~ msgid "created"
+#~ msgstr "vytvořeno"
+
+#~ msgid "exited"
+#~ msgstr "skončilo"
+
+#~ msgid "paused"
+#~ msgstr "pozastaveno"
+
+#~ msgid "running"
+#~ msgstr "spuštěné"
+
+#~ msgid "stopped"
+#~ msgstr "zastaveno"
+
+#~ msgid "user"
+#~ msgstr "uživatel"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Přídatná proměnná prostředí"
+
+#~ msgid "Commit image"
+#~ msgstr "Odeslat obraz"
+
+#~ msgid "Format"
+#~ msgstr "Formát"
+
+#~ msgid "Message"
+#~ msgstr "Zpráva"
+
+#~ msgid "Pause the container"
+#~ msgstr "Pozastavit kontejner"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Odebrat na proměnných prostředí"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Nastavit kontejner na proměnných prostředí"
+
+#~ msgid "Add item"
+#~ msgstr "Přidat položku"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Port hostitele (volitelně)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (volitelně)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "PouzeProČtení"
+
+#~ msgid "IP prefix length"
+#~ msgstr "Délka přepony IP adresy"
+
+#~ msgid "Get new image"
+#~ msgstr "Získat nový obraz"
+
+#~ msgid "Run"
+#~ msgstr "Spustit"
+
+#~ msgid "On build"
+#~ msgstr "Při sestavení"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Opravdu chcete tento obraz smazat?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Nedaří se připojit k tomuto kontejneru: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Nedaří se otevřít kanál:$0"
+
+#~ msgid "Everything"
+#~ msgstr "Vše"
+
+#~ msgid "Security"
+#~ msgstr "Zabezpečení"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Sken z $time ($type) nenalezl žádné zranitelnosti."
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "Tato verze Web Console nepodporuje terminál."
diff --git a/po/de.po b/po/de.po
new file mode 100644
index 0000000..17a16e3
--- /dev/null
+++ b/po/de.po
@@ -0,0 +1,1506 @@
+# Martin Pitt <mpitt@redhat.com>, 2019. #zanata
+# Mike FABIAN <mfabian@redhat.com>, 2020.
+# Matej Marusak <marusak.matej@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-09-02 07:21+0000\n"
+"Last-Translator: Martin Pitt <mpitt@redhat.com>\n"
+"Language-Team: German <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/de/>\n"
+"Language: de\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 4.18.2\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 Container"
+msgstr[1] "$0 Container"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 Abbild insgesamt, $1"
+msgstr[1] "$0 Abbilder insgesamt, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 Sekunde"
+msgstr[1] "$0 Sekunden"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 ungenutztes Abbild, $1"
+msgstr[1] "$0 ungenutzte Abbilder, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+"Zu ergreifende Maßnahmen, wenn der Container in einen ungesunden Zustand "
+"übergeht."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Port-Zuordnung hinzufügen"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Variable hinzufügen"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Volumen hinzufügen"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Alle"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Alle Registries"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Immer"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Ein Fehler ist aufgetreten"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Autor"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Podman automatisch beim Hochfahren starten"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU Shares Hilfe"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU-Shares"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU Shares legt die Priorität laufender Container fest. Standardwert ist "
+"1024. Ein höherer Wert priorisiert den Container. Ein niedrigerer Wert senkt "
+"dessen Priorität."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Abbruch"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Überprüfung der Gesundheit"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Kontrollpunkt"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Unterstützung von Kontrollpunkten und Wiederherstellung"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Kontrollpunkt für Container $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Klicken um veröffentlichte Ports zu sehen"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Klicken um Volumen zu sehen"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Cockpit Bestandteil für Podman-Container"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Befehl"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Kommentare"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Committen"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Container übergeben"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Konfiguriert"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konsole"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Container"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Container konnte nicht erstellt werden"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Container konnte nicht erstellt werden"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Container läuft nicht"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Container-Name"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Containername ist erforderlich."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Container-Pfad"
+
+#: src/Volume.jsx:23
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container path must not be empty"
+msgstr "Container konnte nicht erstellt werden"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Container-Port"
+
+#: src/PublishPort.jsx:37
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container port must not be empty"
+msgstr "Container konnte nicht erstellt werden"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Container"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Erstellen"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr ""
+"Ein neues Abbild auf der Grundlage des aktuellen Zustands des Containers $0 "
+"erstellen."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Erstellen und ausführen"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Container erstellen"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Container in $0 erstellen"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Container in Pod erstellen"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Pod erstellen"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Erstellt"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Erstellt von"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "CPU-Anteile verringern"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Intervall verringern"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Maximale Wiederholungsversuche verringern"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Speicher verringern"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Wiederholungsversuche verringern"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Startzeitraum verringern"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Zeitüberschreitung verringern"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Löschen"
+
+#: src/ImageDeleteModal.jsx:92
+#, fuzzy
+#| msgid "Delete $0?"
+msgid "Delete $0 image?"
+msgstr "$0 löschen?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "$0 löschen?"
+
+#: src/ImageDeleteModal.jsx:96
+#, fuzzy
+#| msgid "Delete tagged images"
+msgid "Delete image"
+msgstr "Markierte Image löschen"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Pod $0 löschen?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Markierte Image löschen"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Nicht verwendete Systemabbilder löschen:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Nicht verwendete Benutzerabbilder löschen:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Beim Löschen des Containers werden alle Daten darin verloren gehen."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr ""
+"Das Löschen eines laufenden Containers führt dazu, dass alle darin "
+"enthaltenen Daten gelöscht werden."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Löschen des Pods wird die folgenden Container entfernen:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Details"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Speicherplatz"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Docker-Format ist hilfreich, wenn das Image mit Docker oder Moby Engine "
+"geteilt wird"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Herunterladen"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Neues Abbild herunterladen"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Leerer Pod $0 wird endgültig entfernt."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Einsprungspunkt"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Umgebungsvariablen"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Fehler"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Fehlermeldung"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Beim Verbinden der Konsole ist ein Fehler aufgetreten"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Beispiel, Ihr Name <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Beispiel: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Beendet"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Gesundheitsprüfung fehlgeschlagen"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Überprüfung von Container $0 fehlgeschlagen"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Fehler bei der Bereinigung des Containers"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Konnte Container $0 nicht committen"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Fehler bei der Erstellung des Containers $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Konnte Image $0 nicht herunterladen: $1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Konnte Container $0 nicht neu entfernen"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Fehler bei der erzwungenen Entfernung des Abbilds $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Konnte Pod $0 nicht neu starten"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Konnte Pod $0 nicht stoppen"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Konnte Container $0 nicht pausieren"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Konnte Pod $0 nicht pausieren"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Löschen nicht gebrauchter Container fehlgeschlagen"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Reduzierung nicht gebrauchter Abbilder fehlgeschlagen"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Image $0 konnte nicht gepullt werden"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Konnte Container $0 nicht neu entfernen"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Fehler bei der Entfernung von Abbild $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Fehler beim Umbenennen des Containers $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Konnte Container $0 nicht neu starten"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Konnte Pod $0 nicht neu starten"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Konnte Container $0 nicht wiederherstellen"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Konnte Container $0 nicht wieder starten"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Konnte Pod $0 nicht wieder starten"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Konnte Container $0 nicht starten"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Gesundheitsprüfung von Container $0 fehlgeschlagen"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Konnte nicht nach Images suchen."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Konnte nicht nach Images suchen: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Konnte nicht nach neuen Images suchen"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Konnte Container $0 nicht starten"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Konnte Pod $0 nicht starten"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Konnte Container $0 nicht stoppen"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Konnte Pod $0 nicht stoppen"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr ""
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Übergabe erzwingen"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Löschen erzwingen"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Löschen von Pod $0 erzwingen?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Neustart erzwingen"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Stoppen erzwingen"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Gateway"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Gesundheitsprüfung"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Hilfe zum Gesundheitsprüfungsintervall"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Hilfe zur Wiederholung der Gesundheitsprüfung"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Hilfe zur Periode der Gesundheitsprüfung"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Hilfe zum Zeitlimit der Gesundheitsprüfung"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Hilfe zur Gesundheitsprüfungs-Aktion"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Gesund"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Abbilder ausblenden"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Intermediate Images verstecken"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Verlauf"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Host-Pfad"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Host-Port"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Hilfe zum Host-Port"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP-Adresse"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Hilfe zur IP-Adresse"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Ideal zur Entwicklung"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Ideal für Dienste"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Wenn die Host-IP auf 0.0.0.0 oder gar nicht gesetzt ist, wird der Port an "
+"alle IPs des Hosts gebunden."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Wenn der Host-Port nicht gesetzt ist, wird dem Container-Port ein zufälliger "
+"Port auf dem Host zugewiesen."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "IP-Adresse ignorieren, wenn statisch festgelegt"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "MAC-Adresse ignorieren, wenn statisch festgelegt"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Image"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Abbildname ist nicht eindeutig"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Abbildname ist erforderlich"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Abbildauswahl-Hilfe"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Images"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "CPU-Anteile erhöhen"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Intervall erhöhen"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Maximale Wiederholungsversuche erhöhen"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Speicher erhöhen"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Wiederholungsversuche erhöhen"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Startzeitraum erhöhen"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Zeitüberschreitung erhöhen"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integration"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Intervall"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Intervall, in dem die Gesundheitsprüfung durchgeführt wird."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Ungültige Zeichen. Der Name darf nur Buchstaben, Zahlen und bestimmte "
+"Satzzeichen (_ . -) enthalten."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Alle temporären Kontrollpunktdateien behalten"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Schlüssel"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr ""
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Letzte 5 Durchläufe"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "LetzterKontrollpunkt"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr ""
+"Nach dem Schreiben des Kontrollpunkts auf die Festplatte weiterlaufen lassen"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Details werden geladen ..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Protokolle werden geladen ..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Wird geladen ..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Lokal"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Lokale Abbilder"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Protokolle"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC-Adresse"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Maximale Versuche"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Speicher"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Speicher-Limit"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Speichereinheit"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Modus"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Mehrere Tags für dieses Image vorhanden. Getagte Images zum Löschen "
+"auswählen."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr ""
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Name"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr ""
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Neuer Containername"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Neuer Abbildname"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Nein"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Keine Aktion"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Keine Container"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Keine Container benutzen dieses Abbild"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Keine Container in diesem Pod"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Aktueller Filter passt auf keinen Container"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Keine Umgebungsvariablen angegeben"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Keine Images"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Keine Abbilder gefunden"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Aktueller Filter passt auf kein Image"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Keine Bezeichnung"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Keine offenen Ports"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Keine Ergebnisse für $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Keine laufenden Container"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Keine Volumen angegeben"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Bei einem Ausfall"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Nur laufenlassen"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Optionen"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Eigentümer"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Eigentümer-Hilfe"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Bestandener Gesundheitstest"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Fügen Sie eine oder mehrere Zeilen mit key=value Paaren in irgendein Feld "
+"ein für multiplen Import"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Anhalten"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Container bei der Abbilderstellung pausieren"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Pausiert"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Pod konnte nicht erstellt werden"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Pod-Name"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman-Container"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman-Service ist nicht aktiv"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Portzuordnung"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Ports"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr ""
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Privat"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokoll"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Reduzieren"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Nicht benötigte Abbilder löschen"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Reduzierung nicht benötigter Abbilder"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Räume Container auf"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Reduziere Abbilder"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Neuestes Image pullen"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Herunterladen"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Nur-Lese-Zugriff"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Lese-/Schreibzugriff"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Element entfernen"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Entfernt selektierte nicht-laufende Container"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Wird entfernt"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Umbenennen"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Container $0 umbenennen"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Ressourcen-Grenzen können gesetzt werden"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Neustart"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Neustartrichtlinie"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Neustartrichtlinien-Hilfe"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr ""
+"Neustart-Richtlinie, die beim Beenden von Containern befolgt werden soll."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Wiederherstellen"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Container $0 wiederherstellen"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Wiederherstellung mit bestehenden TCP-Verbindungen"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Eingeschränkt durch Benutzerkonto-Privilegien"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Fotfahren"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Wiederholungsversuche"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Versuchen Sie einen anderen Begriff."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Gesundheitsprüfung durchführen"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Läuft"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Nach Name oder Beschreibung suchen"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Nach Registry suchen"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Suchen nach"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Nach einem Abbild suchen"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Suchbegriff oder Containerort"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Wird gesucht ..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Wird gesucht: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Freigegeben"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Anzeigen"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Abbilder anzeigen"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Zwischenabbilder anzeigen"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Weniger anzeigen"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Mehr anzeigen"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Größe"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Starten"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Startzeitraum"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Podman starten"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Zum Suchen nach Abbildern mit dem Tippen beginnen."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Gestartet am"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Zustand"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Status"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Stoppen"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Angehalten"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Unterstützung der Aufrechterhaltung bestehender TCP-Verbindungen"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "System"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "System-Podman ist auch verfügbar"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Etikett"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Etiketten"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Die Cockpit Benutzeroberfläche für Podman Container."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Die maximal zulässige Zeit für den Abschluss der Gesundheitsprüfung, bevor "
+"ein Intervall als fehlgeschlagen gilt."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Die Anzahl der zulässigen Wiederholungsversuche, bevor eine "
+"Gesundheitsprüfung als ungesund gilt."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Zeitüberschreitung"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Fehler suchen"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Tippen zum filtern…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Abbildverlauf kann nicht geladen werden"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Ungesund"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Läuft seit $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Veraltetes Docker-Format verwenden"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Benutzt von"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Benutzer"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Benutzer Podman ist auch verfügbar"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Benutzer:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Wert"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Datenträger"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Wenn ungesund"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Mit Terminal"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Beschreibbar"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "container"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "wird heruntergeladen"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "Host[:Port]/[Benutzer]/Container[:Tag]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "Image"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "in"
+
+#: src/ImageDeleteModal.jsx:79
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate"
+msgstr "Intermediate Images verstecken"
+
+#: src/ImageDeleteModal.jsx:59
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate image"
+msgstr "Intermediate Images verstecken"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "n.v."
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "nicht verfügbar"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "Pod-Gruppe"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "Ports"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "Sekunden"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "System"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "ungenutzt"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "Benutzer:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "Volumen"
+
+#~ msgid "Delete $0"
+#~ msgstr "$0 löschen"
+
+#~ msgid "select all"
+#~ msgstr "Alles auswählen"
+
+#~ msgid "Restarting"
+#~ msgstr "Wird neu gestartet"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Löschen von $0 bestätigen"
+
+#, fuzzy
+#~| msgid "Please confirm deletion of pod $0"
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Löschen von Pod $0 bestätigen"
+
+#, fuzzy
+#~| msgid "Please confirm force deletion of pod $0"
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Erzwungenes Löschen von Pod $0 bestätigen"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Erzwungenes Löschen von $0 bestätigen"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Container läuft gerade."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Keine root-Dateisystem-Änderungen beifügen, wenn exportieren"
+
+#~ msgid "Start after creation"
+#~ msgstr "Nach der Erstellung starten"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Nicht verwendete $0 Abbilder löschen:"
+
+#~ msgid "created"
+#~ msgstr "erstellt"
+
+#~ msgid "exited"
+#~ msgstr "beendet"
+
+#~ msgid "paused"
+#~ msgstr "pausiert"
+
+#~ msgid "running"
+#~ msgstr "Läuft"
+
+#~ msgid "stopped"
+#~ msgstr "Angehalten"
+
+#~ msgid "user"
+#~ msgstr "Benutzer"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Build-Variable hinzufügen"
+
+#~ msgid "Commit image"
+#~ msgstr "Image committen"
+
+#~ msgid "Format"
+#~ msgstr "Format"
+
+#~ msgid "Message"
+#~ msgstr "Nachricht"
+
+#~ msgid "Pause the container"
+#~ msgstr "Den Container anhalten"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Bei Bau-Variable entfernen"
+
+#~ msgid "Add item"
+#~ msgstr "Element hinzufügen"
+
+#, fuzzy
+#~| msgid "Host port"
+#~ msgid "Host port (optional)"
+#~ msgstr "Host-Port"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Nur lesen"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP-Präfixlänge"
+
+#~ msgid "Get new image"
+#~ msgstr "Neues Image herunterladen"
+
+#~ msgid "Run"
+#~ msgstr "Starten"
+
+#~ msgid "On build"
+#~ msgstr "Beim Bauen"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Sind Sie sicher, dass Sie dieses Image löschen wollen?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Konnte nicht mit diesem Container verbinden: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Konnte keinen Kanal öffnen: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Alles"
+
+#~ msgid "Security"
+#~ msgstr "Sicherheit"
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "Diese Version der Web-Konsole unterstützt kein Terminal."
diff --git a/po/es.po b/po/es.po
new file mode 100644
index 0000000..b2acb86
--- /dev/null
+++ b/po/es.po
@@ -0,0 +1,1490 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Álvaro Castillo <sincorchetes@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-12-15 14:41+0000\n"
+"Last-Translator: Neftali Yagua <despacho@neftaliyagua.com>\n"
+"Language-Team: Spanish <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/es/>\n"
+"Language: es\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.3\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 contenedor"
+msgstr[1] "$0 contenedores"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 imagen, $1"
+msgstr[1] "$0 imágenes en total, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 segundo"
+msgstr[1] "$0 segundos"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 imagen sin usar, $1"
+msgstr[1] "$0 imágenes sin usar, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 a 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+"Acción a tomar una vez que el contenedor pase a un mal estado de salud."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Añadir mapeo de puertos"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Añadir variable"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Añadir volumen"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Todos"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Todos los registros"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Siempre"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Ocurrió un error"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Autor"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Iniciar podman en el arranque"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Ayuda sobre shares de CPU"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Shares de CPU"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"Los shares de CPU determinan la prioridad de ejecución de contenedores. La "
+"prioridad por defecto es 1024. Un número mayor prioriza este contenedor. Un "
+"número menor reduce la prioridad."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Cancelar"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Comprobando estado de salud"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Checkpoint"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Soporte para checkpoint y restauración"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Crear checkpoint del contenedor $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Pulse para ver los puertos publicados"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Pulse para ver los volúmenes"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Componente Cockpit para contenedores Podman"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Comando"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Comentarios"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Commit"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Confirmar contenedor"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Configurado"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Consola"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Contenedores"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Falló al crear el contenedor"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "El contenedor no pudo ser iniciado"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "El contenedor no se está ejecutando"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Nombre de contenedor"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Se requiere un nombre para el contenedor."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Ruta del contenedor"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "La ruta del contenedor no debe estar vacía"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Puerto del contenedor"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "El puerto de contenedores no debe estar vacío"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Contenedores"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Crear"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Crear una imagen nueva basada en el estado actual del contenedor $0."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Crear y ejecutar"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Crear contenedor"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Crear contenedor en $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Crear un contenedor en el pod"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Crear pod"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Creado"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Creada por"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Reducir shares de CPU"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Reducir intervalo"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Reducir el número máximo de reintentos"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Reducir la cantidad de memoria"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Reducir el número de reintentos"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Reducir el periodo de arranque"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Reducir el tiempo de espera"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Eliminar"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "¿Eliminar imagen de $0?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "¿Eliminar $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Eliminar Imagen"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "¿Eliminar pod $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Borrar imágenes seleccionadas"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Borrar imágenes del sistema sin usar:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Borrar imágenes del usuario sin usar:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Eliminando un contenedor se borrará toda la información que hay en él."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr ""
+"Eliminando un contenedor en ejecución borrará toda la información que haya "
+"en él."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Eliminando este pod eliminará los siguientes contenedores:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Detalles"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Tamaño en disco"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"El formato Docker es útil para compartir la imagen con Docker o Moby Engine"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Descargar"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Descargar nueva imagen"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "El pod $0, vacío, será eliminado permanentemente."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Punto de entrada"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Variables de entorno"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Error"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Mensaje de error"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Hubo un error al conectar la consola"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Por ejemplo, Tu Nombre <tunombre@ejemplo.es>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Por ejemplo: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Finalizado"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Comprobación de estado de salud fallada"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Fallo hacer el checkpoint del contenedor $0"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Fallo al limpiar el contenedor"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Falló hacer el commit en el contenedor $0"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Fallo al crear el contenedor $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Fallo al descargar la imagen $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Falló al forzar el borrado del contenedor $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Fallo al forzar el borrado de la imagen $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Fallo al reiniciar forzadamente el pod $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Fallo al detener forzadamente el pod $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Fallo al pausar el contenedor $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Fallo al pausar el pod $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Fallo al eliminar los contenedores no utilizados"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Fallo al eliminar las imágenes no usadas"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Fallo al obtener la imagen $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Falló al eliminar el contenedor $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Fallo al eliminar la imagen $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Fallo al renombrar el contenedor $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Falló al reiniciar el contenedor $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Fallo al reiniciar el pod $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Fallo al restaurar el contenedor $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Fallo al reanudar el contenedor $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Fallo al reanudar el pod $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Fallo al iniciar el contenedor $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Fallo al comprobar el estado de salud del contenedor $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Fallo al buscar imágenes."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Fallo al buscar las imágenes: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Falló al buscar nuevas imágenes"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Falló al iniciar el contenedor $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Fallo al iniciar el pod $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Falló al parar el contenedor $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Fallo al parar el pod $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Racha de fallos"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Forzar commit"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Forzar el borrado"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "¿Forzar la eliminación del pod $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Forzar el reinicio"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Forzar la parada"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Puerta de enlace"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Comprobación del estado de salud"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Ayuda sobre el intervalo de comprobación del estado de salud"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr ""
+"Ayuda sobre el número de reintentos de comprobación del estado de salud"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Ayuda sobre el periodo de arranque de comprobación del estado de salud"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Ayuda sobre el tiempo de espera de comprobación del estado de salud"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Ayuda sobre la acción al fallo al comprobar el estado de salud"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Buen estado de salud"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Ocultar imágenes"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Ocultar imágenes intermedias"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historial"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Ruta del anfitrión"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Puerto del anfitrión"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Ayuda sobre el puerto del anfitrión"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "Dirección IP"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Ayuda sobre la dirección IP"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Idóneo para entornos de desarrollo"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Idóneo para ejecutar servicios"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Si la IP del host se asigna a 0.0.0.0 o no se asigna, el puerto se asociará "
+"a todas las IP's del host."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Si no se asigna el puerto del host, el puerto del contenedor se asignará a "
+"un puerto aleatorio del host."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ignorar la dirección IP si se ha configurado de forma estática"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ignorar la dirección MAC si se ha configurado de forma estática"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Imagen"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "El nombre de la imagen no es único"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Se requiere un nombre para la imagen"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Ayuda sobre la selección de imagen"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Imágenes"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Incrementar shares de CPU"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Incrementar el intervalo"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Incrementar el número máximo de reintentos"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Incrementar la cantidad de memoria"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Incrementar el número de reintentos"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Incrementar el periodo de arranque"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Incrementar el tiempo de espera"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integración"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Intervalo"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Cada cuánto se ejecuta la comprobación del estado de salud."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Carácteres inválidos. El nombre sólo puede contener letras, números y "
+"ciertos signos de puntuación (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Mantener todos los archivos temporales de checkpoint"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Clave"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "La clave no debe estar vacía"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Últimas 5 comprobaciones"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Último checkpoint"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Seguir ejecutando tras escribir el checkpoint en disco"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Cargando detalles..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Cargando registros..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Cargando..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Local"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Imágenes locales"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Registros"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "Dirección MAC"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Número máximo de reintentos"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Memoria"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Límite de memoria"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Unidad de medida de memoria"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Modo"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Existen múltiples etiquetas para esta imagen. Seleccione las imágenes "
+"etiquetadas a eliminar."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "Debe ser una dirección IP válida"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Nombre"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Nombre ya en uso"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Nuevo nombre del contenedor"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Nombre de la nueva imagen"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "No"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Ninguna acción"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "No hay contenedores"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "No hay contenedores que utilicen esta imagen"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "No hay contenedores en este pod"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "No hay contenedores relacionados con los criterios de búsqueda"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "No se han especificado variables de entorno"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "No hay imágenes"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "No se han encontrado imágenes"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "No hay imágenes relacionadas con el criterio de búsqueda"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Sin etiqueta"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "No hay puertos expuestos"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "No hay resultados para $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "No hay contenedores ejecutándose"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "No se han especificado volúmenes"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "En caso de fallo"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Sólo los que se están ejecutando"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Opciones"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Propietario"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Ayuda sobre el propietario"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Comprobación de estado de salud superada"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Pegue una o más líneas de pares clave=valor en cualquier campo para importar "
+"en masa"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Pausar"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Pausar el contenedor cuando se cree la imagen"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Pausado"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Fallo al crear el pod"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Nombre del pod"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Contenedores de Podman"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "El servicio Podman no está activo"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Mapeo de puertos"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Puertos"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Se pueden mapear puertos inferiores a 1024"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Privado"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protocolo"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Eliminar"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Eliminar contenedores no utilizados"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Eliminar imágenes no utilizadas"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Eliminando contenedores"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Eliminando imágenes no utilizadas"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Obtener la última imagen"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Obteniendo"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Acceso de sólo lectura"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Acceso de lectura y escritura"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Eliminar elemento"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Elimina los contenedores seleccionados que no se están ejecutando"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Eliminándose"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Renombrar"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Renombrar el contenedor $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Se pueden establecer límites sobre los recursos"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Reiniciar"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Política de reinicio"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Ayuda sobre la política de reinicio"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Política de reinicio a seguir cuando se cierren contenedores."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Política de reinicio a seguir cuando los contenedores finalicen. Utilizar "
+"linger para iniciar contenedores automáticamente puede no funcionar bajo "
+"ciertas circunstancias, como cuando se emplean ecryptfs, systemd-homed, NFS "
+"o 2FA en una cuenta de usuario."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Restaurar"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Restaurar el contenedor $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Restaurar con las conexiones TCP establecidas"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Restringido por los permisos de la cuenta de usuario"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Reanudar"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Número de reintentos"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Pruebe con otro término."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Ejecutar comprobación de estado de salud"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Ejecutándose"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Buscar por nombre o descripción"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Buscar por registro"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Buscar"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Buscar una imagen"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Buscar cadena o ubicación de contenedor"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Buscando…"
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Buscando: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Compartido"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Mostrar"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Mostrar imágenes"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Mostrar imágenes intermedias"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Mostrar menos"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Mostrar más"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Tamaño"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Iniciar"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Periodo de arranque"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Iniciar podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Comience a escribir para buscar imágenes."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Comenzó"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Estado"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Estado"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Parar"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Detenido"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Habilitar la preservación de las conexiones TCP establecidas"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Sistema"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "El servicio de sistema Podman también está disponible"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Etiqueta"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Etiquetas"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "La interfaz de usuario de Cockpit para contenedores Podman."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "El tiempo de inicialización necesario para que un contenedor arranque."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"El máximo tiempo permitido para completar una comprobación de estado de "
+"salud antes de que se considere fallada."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"El máximo número de reintentos permitidos antes de que se considere un mal "
+"estado de salud."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Tiempo de espera"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Solución de errores"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Escriba para establecer un criterio de búsqueda…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "No se pudo cargar el historial de imágenes"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Mal estado de salud"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Levantado desde $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Usar el formato heredado Docker"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Usado por"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Usuario"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "El servicio de usuario Podman también está disponible"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Usuario:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Valor"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Volúmenes"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "En caso de mal estado de salud"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Con terminal"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Puede escribirse"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "contenedor"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "descargando"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:puerto]/[usuario]/contenedor[:etiqueta]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "imagen"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "en"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "intermedia"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "imagen intermedia"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "sin/aplicar"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "no disponible"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "Grupo de pod"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "puertos"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "segundos"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "sistema"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "sin utilizar"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "usuario:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "volúmenes"
+
+#~ msgid "Delete $0"
+#~ msgstr "Eliminar $0"
+
+#~ msgid "select all"
+#~ msgstr "seleccionar todas"
+
+#~ msgid "Failure action"
+#~ msgstr "Acción al fallo"
+
+#~ msgid "Restarting"
+#~ msgstr "Reiniciándose"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Confirme el borrado de $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Confirme el borrado del pod $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Confirme el borrado forzado del pod $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Confirme forzar el borrado de $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "El contenedor se está ejecutando."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "No incluir cambios en el sistema de archivos raíz al exportar"
+
+#, fuzzy
+#~| msgid "Get new image"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Obtener nueva imagen"
+
+#, fuzzy
+#~| msgid "Created"
+#~ msgid "created"
+#~ msgstr "Creado"
+
+#, fuzzy
+#~| msgid "Exited"
+#~ msgid "exited"
+#~ msgstr "Cerrado"
+
+#, fuzzy
+#~| msgid "Pause"
+#~ msgid "paused"
+#~ msgstr "Pausar"
+
+#, fuzzy
+#~| msgid "user"
+#~ msgid "user"
+#~ msgstr "usuario"
+
+#, fuzzy
+#~| msgid "Set container on build variables"
+#~ msgid "Add on build variable"
+#~ msgstr "Establecer el contenedor en las variables de construcción"
+
+#~ msgid "Commit image"
+#~ msgstr "Commit una imagen"
+
+#~ msgid "Format"
+#~ msgstr "Formato"
+
+#~ msgid "Message"
+#~ msgstr "Mensaje"
+
+#~ msgid "Pause the container"
+#~ msgstr "Pausar el contenedor"
+
+#, fuzzy
+#~| msgid "Set container on build variables"
+#~ msgid "Remove on build variable"
+#~ msgstr "Establecer el contenedor en las variables de construcción"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Establecer el contenedor en las variables de construcción"
+
+#, fuzzy
+#~| msgid "Host port"
+#~ msgid "Host port (optional)"
+#~ msgstr "Puerto del anfitrión"
+
+#~ msgid "ReadOnly"
+#~ msgstr "SoloLectura"
+
+#~ msgid "Run"
+#~ msgstr "Ejecutar"
+
+#~ msgid "On build"
+#~ msgstr "En construcción"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "¿Está seguro de que quiere eliminar esta imagen?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "No se pudo asociar a este contenedor: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "No se pudo abrir un canal: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Todo"
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "Esta versión de consola Web no soporta un terminal."
diff --git a/po/fi.po b/po/fi.po
new file mode 100644
index 0000000..41e85e3
--- /dev/null
+++ b/po/fi.po
@@ -0,0 +1,1485 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2024-02-20 20:36+0000\n"
+"Last-Translator: Jan Kuparinen <copper_fin@hotmail.com>\n"
+"Language-Team: Finnish <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/fi/>\n"
+"Language: fi\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.4\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 kontti"
+msgstr[1] "$0 konttia"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 levykuva yhteensä, $1"
+msgstr[1] "$0 levykuvaa yhteensä, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 sekunti"
+msgstr[1] "$0 sekuntia"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 käyttämätön levykuva, $1"
+msgstr[1] "$0 käyttämätöntä levykuvaa, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "Toimenpiteet, kun kontti siirtyy epäterveelliseen tilaan."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Lisää porttiassosiaatio"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Lisää muuttuja"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Lisää taltio"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Kaikki"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Kaikki rekisterit"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Aina"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Tapahtui virhe"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Tekijä"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Käynnistä podman automaattisesti käynnistyksen yhteydessä"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "Suoritin"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Ohjeita CPU-osuuksista"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Suoritinosuus"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU-osuudet määrittävät käynnissä olevien konttien prioriteetin. "
+"Oletusprioriteetti on 1024. Suurempi numero priorisoi tätä konttia. Pienempi "
+"numero laskee prioriteettia."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Peru"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Tarkistetaan tilaa"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Tarkistuspiste"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Tarkistuspisteen ja palautuksen tuki"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Luo tarkistuspiste kontista $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Napsauta nähdäksesi julkaistut portit"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Napsauta nähdäksesi taltiot"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Cockpit-komponentti Podman-konteille"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Komento"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Kommentit"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Kommitoi"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Kommitoi kontti"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Asetukset asetettu"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konsoli"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Kontti"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Kontin luonti epäonnistui"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Kontin käynnistys epäonnistui"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Kontti ei ole käynnissä"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Kontin nimi"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Kontin nimi vaaditaan."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Kontin polku"
+
+#: src/Volume.jsx:23
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container path must not be empty"
+msgstr "Kontin luonti epäonnistui"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Kontin portti"
+
+#: src/PublishPort.jsx:37
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container port must not be empty"
+msgstr "Kontin luonti epäonnistui"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Kontit"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Luo"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Luo uusi levykuva $0-kontin nykyisen tilan perusteella."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Luo ja suorita"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Luo kontti"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Luo kontti kohteeseen $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Luo kontti podiin"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Luo podi"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Luotu"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Tekijä"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Vähennä suoritinosuuksia"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Pienennä väliä"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Pienennä uudelleenyritysten enimmäismäärää"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Vähennä muistia"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Vähennä uudelleenyrityksiä"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Pienennä aloitusaikaa"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Pienennä aikakatkaisua"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Poista"
+
+#: src/ImageDeleteModal.jsx:92
+#, fuzzy
+#| msgid "Delete $0?"
+msgid "Delete $0 image?"
+msgstr "Poista $0?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Poista $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Poista levykuva"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Poista podi $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Poista merkityt kuvat"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Poista käyttämättömät järjestelmälevykuvat:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Poista käyttämättömät käyttäjän levykuvat:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Kontin poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "Käynnissä olevan kontin poistaminen tuhoaa kaiken sillä olevan datan."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Tämän podin poisto poistaa seuraavat kontit:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Yksityiskohdat"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Levytila"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Docker-muoto on hyödyllinen, kun levykuvaa jaetaan Dockerin tai Moby Enginen "
+"kanssa"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Lataa"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Lataa uusi levykuva"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Tyhjä podi $0 poistetaan pysyvästi."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Sisääntulokohta"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Ympäristömuuttujat"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Virhe"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Virheviesti"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Virhe konsolia liitettäessä"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Esimerkki: Nimesi <nimesi@esimerkki.fi>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Esimerkki: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Päättyi"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Epäonnistunut eheysajo"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Kontin $0 tarkistuspisteen luominen epäonnistui"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Kontin poistaminen epäonnistui"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Kontin $0 kommitointi epäonnistui"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Kontin $0 luominen epäonnistui"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Levykuvan lataaminen epäonnistui $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Kontin $0 pakotettu poistaminen epäonnistui"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Levykuvan $0 pakotettu poistaminen epäonnistui"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Podin $0 pakotettu uudelleenkäynnistäminen epäonnistui"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Podin $0 pakotettu pysäyttäminen epäonnistui"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Kontin $0 keskeyttäminen epäonnistui"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Podin $0 keskeyttäminen epäonnistui"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Käyttämättömien konttien poisto epäonnistui"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Käyttämättömien levykuvien karsiminen epäonnistui"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Levykuvan $0 hakeminen epäonnistui"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Kontin $0 poistaminen epäonnistui"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Levykuvan $0 poistaminen epäonnistui"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Kontin $0 uudelleennimeäminen epäonnistui"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Kontin $0 uudelleenkäynnistäminen epäonnistui"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Podin $0 uudelleenkäynnistäminen epäonnistui"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Kontin $0 palauttaminen epäonnistui"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Kontin $0 palauttaminen epäonnistui"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Podin $0 käytön jatkaminen epäonnistui"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Kontin $0 suorittaminen epäonnistui"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Kontin $0 eheystarkastus epäonnistui"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Levykuvien etsintä epäonnistui."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Levykuvien etsintä epäonnistui: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Uusien levykuvien etsintä epäonnistui"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Kontin $0 käynnistäminen epäonnistui"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Podin $0 käynnistäminen epäonnistui"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Kontin $0 pysäyttäminen epäonnistui"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Podin $0 pysäyttäminen epäonnistui"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Epäonnistunut sarja"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Pakota kommitti"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Pakota poisto"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Pakota podin $0 poisto?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Pakota uudelleenkäynnistys"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Pakota pysäytys"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "Gt"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Yhdyskäytävä"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Eheystarkistus"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Tietoa eheystarkistuksen aikavälistä"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Tietoa eheystarkistuksen uudelleenyrityksistä"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Tietoa eheystarkistuksen aloitusjaksosta"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Tietoa eheystarkistuksen aikakatkaisusta"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Tietoa terveystarkistuksen virhetilan toimenpiteistä"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Ehjä"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Piilota levykuvat"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Piilota välivaiheen levykuvat"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historia"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Koneen polku"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Koneen portti"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Ohjeita koneen portista"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "TUNNISTE"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP-osoite"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Ohjeita IP-osoitteesta"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Ihanteellinen kehittämiseen"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Ihanteellinen palvelujen suorittamiseen"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Jos isäntäkoneen IP-osoitteeksi on asetettu 0.0.0.0 tai sitä ei ole "
+"määritetty lainkaan, portti sidotaan kaikkiin isäntäkoneen IP-osoitteisiin."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Jos isäntäporttia ei ole asetettu, konttiportille määritetään satunnainen "
+"isäntäportti."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ohita IP-osoite, jos se on asetettu staattisesti"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ohita MAC-osoite, jos se on asetettu staattisesti"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Levykuva"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Kevykuvan nimi ei ole ainutlaatuinen"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Levykuvan nimi vaaditaan"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Ohjeita levykuvan valintaan"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Levykuvat"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Lisää suoritinosuuksia"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Suurenna väliä"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Lisää uudelleenyritysten enimmäismäärää"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Lisää muistia"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Lisää uudelleenyrityksiä"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Pidennä aloitusjaksoa"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Nosta aikakatkaisua"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integraatio"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Aikaväli"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Eheystarkistusten aikaväli."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Virheellisiä merkkejä. Nimi voi sisältää vain kirjaimia, numeroita ja "
+"tiettyjä välimerkkejä (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "Kt"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Säilytä kaikki väliaikaiset tarkistuspistetiedostot"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Avain"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "Avain ei saa olla tyhjä"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Viimeiset 5 ajoa"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Viimeisin tarkistuspiste"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Jätä käyntiin tarkistuspisteen levylle kirjoittamisen jälkeen"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Ladataan tietoja..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Ladataan lokeja..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Ladataan..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Paikallinen"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Paikallisia levykuvia"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Lokit"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC-osoite"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "Mt"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Uudelleenyritysten enimmäismäärä"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Muisti"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Muistiraja"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Muistiyksikkö"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Tila"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Tälle levykuvalle on useita tunnisteita. Valitse tunnisteelliset levykuvat "
+"jotka poistetaan."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr ""
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Nimi"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Nimi on jo käytössä"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Kontin uusi nimi"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Uusi levykuvan nimi"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Ei"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Ei toimintoa"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Ei kontteja"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Yksikään kontti ei käytä tätä levykuvaa"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Tässä podissa ei ole kontteja"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Ei nykyistä suodatusta vastaavia kontteja"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Ympäristömuuttujia ei ole määritetty"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Ei levykuvia"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Levykuvia ei löytynyt"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Ei nykyistä suodatusta vastaavia levykuvia"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Ei nimiötä"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Ei paljastettuja portteja"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Ei tuloksia hakusanalle $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Ei käynnissä olevia kontteja"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Taltiota ei ole määritelty"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Epäonnistumisesta"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Vain käynnissä olevat"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Valinnat"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "omistaja"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Omistajan ohjeet"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Onnistunut eheysajo"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Liitä yksi tai useampi rivi avain=arvo-pareja mihin tahansa kenttään "
+"joukkotuontia varten"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Keskeytä"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Pysäytä kontti levykuvan luomisen ajaksi"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Keskeytetty"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Podin luonti epäonnistui"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Podin nimi"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman-kontit"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman-palvelu ei ole aktiivinen"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Porttiassosiaatio"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Portit"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Portit alle 1024 voidaan kartoittaa"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Yksityinen"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokolla"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Karsi"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Poista käyttämättömiä kontteja"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Karsi käyttämättömät levykuvat"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Siivotaan kontteja"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Karsitaan levykuvia"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Hae viimeisin levykuva"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Haetaan"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Vain luku-käyttöoikeus"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Luku-kirjoitusoikeus"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Poista kohde"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Poistaa valitut ei käynnissä olevat kontit"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Poistetaan"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Nimeä uudelleen"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Nimeä kontti $0 uudelleen"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Resurssirajoja voidaan asettaa"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Käynnistä uudelleen"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Uudelleenkäynnistyksen käytäntö"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Ohjeita uudelleenkäynnistyksen käytännöstä"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Noudata uudelleenkäynnistyskäytäntöä, kun kontit poistuvat."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Käytettävä uudelleenkäynnistyskäytäntö, kun kontit poistuvat. Viivyttämisen "
+"käyttäminen konttien automaattiseen käynnistykseen ei välttämättä toimi "
+"joissain olosuhteissa, kuten kun käyttäjätilillä on käytössä ecryptfs, "
+"systemd-homed, NFS tai 2FA."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Palauta"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Palauta kontti $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Palauta käyttäen luotuja TCP-yhteyksiä"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Rajoitettu käyttäjätilin käyttöoikeuksilla"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Jatka"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Uudelleenyritykset"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Yritä toisella termillä uudelleen."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Aja eheystarkistus"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Käynnissä"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Hae nimen tai kuvauksen perusteella"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Haku rekisterin perusteella"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Etsittävä"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Etsi levykuvaa"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Hae merkkijonon tai kontin sijaintia"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Etsitään..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Etsitään: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Jaettu"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Näytä"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Näytä levykuvat"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Näytä välivaiheen levykuvat"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Näytä vähemmän"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Näytä lisää"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Koko"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Käynnistä"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Aloitusjakso"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Käynnistä podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Aloita kirjoittaminen etsiäksesi levykuvia."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Käynnistetty klo"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Tila"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Tila"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Pysäytä"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Pysäytetty"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Tue jo luotujen TCP -yhteyksien säilyttämistä"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Järjestelmä"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Järjestelmän Podman-palvelu on myös saatavilla"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Tunniste"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Tunnisteet"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Cockpit-käyttöliittymä Podman-konteille."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Alustusaika, joka tarvitaan kontin käynnistymiseen."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Enimmäisaika, joka on sallittu eheystarkastuksen suorittamiseen ennen kuin "
+"se katsotaan epäonnistuneeksi."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Yritysten enimmäismäärä, joka on sallittu eheystarkastuksen suorittamiseen "
+"ennen kuin se katsotaan epäonnistuneeksi."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Aikakatkaisu"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Vianetsintä"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Kirjoita suodattaaksesi…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Levykuvan historian lataaminen epäonnistui"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Rikki"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Käynnissä $0 lähtien"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Käytä vanhaa Docker-muotoa"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Käyttää"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Käyttäjä"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Käyttäjän Podman-palvelu on myös saatavilla"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Käyttäjä:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Arvo"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Taltiot"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Kun rikki"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Päätteen kanssa"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Kirjoitettavissa"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "kontti"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "ladataan"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:port]/[user]/container[:tag]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "levykuva"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "sijainnissa"
+
+#: src/ImageDeleteModal.jsx:79
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate"
+msgstr "Piilota välivaiheen levykuvat"
+
+#: src/ImageDeleteModal.jsx:59
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate image"
+msgstr "Piilota välivaiheen levykuvat"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "ei sovellu"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "ei käytettävissä"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "podiryhmä"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "portit"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "sekuntia"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "järjestelmä"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "ei käytössä"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "käyttäjä:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "taltiot"
+
+#~ msgid "Delete $0"
+#~ msgstr "Poista $0"
+
+#~ msgid "select all"
+#~ msgstr "valitse kaikki"
+
+#~ msgid "Failure action"
+#~ msgstr "virhetilan toimenpiteet"
+
+#~ msgid "Restarting"
+#~ msgstr "Käynnistyy uudelleen"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Vahvista kohteen $0 poistaminen"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Vahvista podin $0 poistaminen"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Vahvista podin $0 pakotettu poistaminen"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Vahvista kohteen $0 pakotettu poistaminen"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontti on parhaillaan käynnissä."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Älä sisällytä juuritiedostojärjestelmän muutoksia vientiä varten"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Oletusarvo kun yksi valittavissa"
+
+#~ msgid "Start after creation"
+#~ msgstr "Käynnistä luomisen jälkeen"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Poista käyttämättömät $0 levykuvat:"
+
+#~ msgid "created"
+#~ msgstr "luotu"
+
+#~ msgid "exited"
+#~ msgstr "päättyi"
+
+#~ msgid "paused"
+#~ msgstr "pysäytetty"
+
+#~ msgid "running"
+#~ msgstr "suoritetaan"
+
+#~ msgid "stopped"
+#~ msgstr "pysäytetty"
+
+#~ msgid "user"
+#~ msgstr "käyttäjä"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Lisää koontimuuttuja"
+
+#~ msgid "Commit image"
+#~ msgstr "Commit-levykuva"
+
+#~ msgid "Format"
+#~ msgstr "Alusta"
+
+#~ msgid "Message"
+#~ msgstr "Viesti"
+
+#~ msgid "Pause the container"
+#~ msgstr "Keskeytä kontin ajo"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Poista koontimuuttujalla"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Aseta kontti koontimuuttujiin"
+
+#~ msgid "Add item"
+#~ msgstr "Lisää kohta"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Koneen portti (valinnainen)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (valinnainen)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Vain-luku"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP Prefix Pituus"
+
+#~ msgid "Run"
+#~ msgstr "Suorita"
diff --git a/po/fr.po b/po/fr.po
new file mode 100644
index 0000000..dd12eba
--- /dev/null
+++ b/po/fr.po
@@ -0,0 +1,1530 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Julien Humbert <julroy67@gmail.com>, 2020.
+# Timothée Ravier <tim@siosm.fr>, 2020.
+# Sébastien Pascal-Poher <spoher@pm.me>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-05-30 13:20+0000\n"
+"Last-Translator: Ludek Janda <ljanda@redhat.com>\n"
+"Language-Team: French <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/fr/>\n"
+"Language: fr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n > 1\n"
+"X-Generator: Weblate 4.17\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 conteneur"
+msgstr[1] "$0 conteneurs"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 image total, $1"
+msgstr[1] "$0 images total, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 seconde"
+msgstr[1] "$0 secondes"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 image non utilisée, $1"
+msgstr[1] "$0 images non utilisées, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "Action à entreprendre lorsque le conteneur passe à un état insalubre."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Ajouter un mappage de port"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Ajouter une variable"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Ajouter volume"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Tous"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Tous les registres"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Toujours"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Une erreur s’est produite"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Auteur"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Démarrer automatiquement podman au démarrage"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Assistance CPU Shares"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Parts de CPU"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU Shares déterminent la priorité des conteneurs en cours d'exécution. La "
+"priorité par défaut est de 1024. Un nombre plus élevé donne la priorité à ce "
+"conteneur. Un nombre inférieur diminue la priorité."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Annuler"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Contrôle de fonctionnement"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Point de contrôle"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Prise en charge des points de contrôle et des restaurations"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Point de contrôle du conteneur $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Cliquez pour voir les ports publiés"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Cliquez pour voir les volumes"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Composant Cockpit pour les conteneurs Podman"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Commande"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Commentaires"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Valider"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Valider conteneur"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Configuré"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Console"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Conteneur"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Le conteneur n’a pas pu être créé"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Le conteneur n’a pas pu être démarré"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Le conteneur n’est pas en cours d’exécution"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Nom du conteneur"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Le nom du conteneur est obligatoire."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Chemin du conteneur"
+
+#: src/Volume.jsx:23
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container path must not be empty"
+msgstr "Le conteneur n’a pas pu être créé"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Port du conteneur"
+
+#: src/PublishPort.jsx:37
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container port must not be empty"
+msgstr "Le conteneur n’a pas pu être créé"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Conteneurs"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Créer"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Créer une nouvelle image basée sur l'état actuel du conteneur $0."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Créer et démarrer"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Créer un conteneur"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Créer un conteneur dans $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Créer un conteneur dans un pod"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Créer un pod"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Créé"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Créé par"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Diminution des parts de CPU"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Diminution de l'intervalle"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Diminuer le nombre maximum de tentatives"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Diminution de la mémoire"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Diminuer les tentatives"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Diminution de la période de démarrage"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Diminuer le délai d'attente"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Supprimer"
+
+#: src/ImageDeleteModal.jsx:92
+#, fuzzy
+#| msgid "Delete $0?"
+msgid "Delete $0 image?"
+msgstr "Supprimer $0 ?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Supprimer $0 ?"
+
+#: src/ImageDeleteModal.jsx:96
+#, fuzzy
+#| msgid "Delete tagged images"
+msgid "Delete image"
+msgstr "Supprimer les images taguées"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Supprimer le pod $0 ?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Supprimer les images taguées"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Supprimer les images système non utilisées :"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Supprimer les images utilisateur non utilisées :"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Supprimer un conteneur va effacer toutes les données qu’il contient."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr ""
+"La suppression d'un conteneur en cours d'exécution efface toutes les données "
+"qu'il contient."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "La suppression de ce pod entraînera celle des conteneurs suivants :"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Détails"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Espace disque"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Le format Docker est utile pour partager l'image avec Docker ou Moby Engine"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Télécharger"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Télécharger la nouvelle image"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Le pod vide $0 sera définitivement supprimé."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Point d’entrée"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Variables d’environnement"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Erreur"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Message d’erreur"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Une erreur s’est produite lors de la connexion à la console"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Exemple, votre nom <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Exemple : $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Quittés"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Échec de l’exécution du bilan de fonctionnement"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Échec de la création du point de contrôle du conteneur $0"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Échec du nettoyage du conteneur"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Échec de la validation du conteneur $0"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Échec de la création du conteneur $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Échec du téléchargement de l’image $0 : $1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Échec de la suppression forcée du conteneur $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Échec de la suppression forcée de l’image $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Échec du redémarrage forcé du pod $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Échec de l'arrêt forcé du pod $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Échec de la mise en pause du conteneur $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Échec de la mise en pause du pod $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+#, fuzzy
+#| msgid "Failed to prune unused images"
+msgid "Failed to prune unused containers"
+msgstr "Échec de la suppression des images non utilisées"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Échec de la suppression des images non utilisées"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Échec d’extraction de l’image $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Échec de la suppression du conteneur $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Échec de la suppression de l’image $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Échec du renommage du conteneur $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Échec du redémarrage du conteneur $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Échec du redémarrage du pod $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Échec de la restauration du conteneur $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Échec de la reprise conteneur $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Échec de la reprise pod $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Échec du démarrage du conteneur $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Échec de l'exécution du contrôle de fonctionnement du conteneur $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Échec de la recherche d'images."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Échec de la recherche de l'image : $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Échec de la recherche de nouvelles images"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Échec du démarrage du conteneur $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Échec du démarrage du pod $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Échec de l’arrêt du conteneur $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Échec de l’arrêt du pod $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Une série d'échecs"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Validation forcée"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Suppression forcée"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Forcer la suppression du pod $0 ?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Redémarrage forcé"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Arrêt forcé"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "Go"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Passerelle"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Bilan de fonctionnement"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Aide à l'intervalle entre les bilans de fonctionnement"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Aide pour les tentatives de bilans de fonctionnement"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Aide pour la période de démarrage du bilan de fonctionnement"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Aide sur le délai d'exécution du bilan de fonctionnement"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Aide à l’action pour les bilans de fonctionnement"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Fonctions intactes"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Cacher les images"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Cacher les images intermédiaires"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historique"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Chemin vers l’hôte"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Port de l’hôte"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Assistance Port d’hôte"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "Adresse IP"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Assistance Adresse IP"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Idéal pour le développement"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Idéal pour le fonctionnement des services"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Si l’IP de l’hôte est définie ainsi 0.0.0.0 ou si elle n’est pas dutout "
+"définie, le port sera relié à tous les IP de l’hôte."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Si le port de l’hôte n’est pas défini, le port du conteneur sera assigné au "
+"hasard sur l’hôte."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ignorer l'adresse IP si définie statiquement"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ignorer l'adresse MAC si définie statiquement"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Image"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Le nom de l'image n'est pas unique"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Le nom de l'image est obligatoire"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Aide à la sélection d'images"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Images"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Augmenter les parts de CPU"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Augmenter l'intervalle"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Augmenter le nombre maximum de tentatives"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Augmenter la mémoire"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Augmenter les tentatives"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Augmenter la période de démarrage"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Augmenter le délai d'attente"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Intégration"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Intervalle"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Intervalle à partir duquel le contrôle de fonctionnement est exécuté."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Caractères non valides. Le nom ne peut contenir que des lettres, des "
+"chiffres et certains signes de ponctuation (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "Ko"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Conserver tous les fichiers de points de contrôle temporaires"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Clé"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr ""
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Les 5 dernières exécutions"
+
+#: src/ContainerDetails.jsx:71
+#, fuzzy
+#| msgid "Checkpoint"
+msgid "Latest checkpoint"
+msgstr "Point de contrôle"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Conserver actif après la création du point de contrôle sur le disque"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Chargement des détails..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Chargement des logs..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Chargement..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Local"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Aucune image"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Journaux"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "Adresse MAC"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "Mo"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Nombre max de nouvellles tentatives"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Mémoire"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Limite mémoire"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Unité de mémoire"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Mode"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Plusieurs tags existent pour cette image. Sélectionnez les images marquées à "
+"supprimer."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr ""
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Nom"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr ""
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Nouveau nom du conteneur"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Nom de la nouvelle image"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Non"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Pas d'action"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Aucun conteneur"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Aucun conteneur n’utilise cette image"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Aucun conteneur dans ce pod"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Aucun conteneur ne correspond au filtre actuel"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Aucune variables d’environnement spécifiée"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Aucune image"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Aucune image trouvée"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Aucune image ne correspond au filtre actuel"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Pas de label"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Aucuns ports exposés"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Aucun résultat pour $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Aucun conteneur en cours d’exécution"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Aucuns volumes spécifiés"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "En cas d’échec"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "En cours d’exécution seulement"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Options"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Propriétaire"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Aide au propriétaire"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Bilan de fonctionnement réussi"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Collez une ou plusieurs lignes de paires clé=valeur dans n'importe quel "
+"champ pour une importation en masse"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Pause"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Mettre le conteneur en pause lors de la création de l’image"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "En pause"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Échec de la création du pod"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Nom du pod"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Conteneurs Podman"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Le service Podman n’est pas actif"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Mappage de port"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Ports"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Les ports inférieurs à 1024 peuvent être mappés"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Privé"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protocole"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Suppression de certaines images"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+#, fuzzy
+#| msgid "Prune unused images"
+msgid "Prune unused containers"
+msgstr "Suppression d’images non utilisées"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Suppression d’images non utilisées"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+#, fuzzy
+#| msgid "No running containers"
+msgid "Pruning containers"
+msgstr "Aucun conteneur en cours d’exécution"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Suppression de certaines images"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Extraire la dernière image"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Extraction"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Accès en lecture seule"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Accès en lecture-écriture"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Supprimer l'élément"
+
+#: src/PruneUnusedContainersModal.jsx:99
+#, fuzzy
+#| msgid "Images and running containers"
+msgid "Removes selected non-running containers"
+msgstr "Images et conteneurs en cours d’exécution"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "En cours de suppression"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Renommer"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Renommer le conteneur $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Des limites de ressources peuvent être fixées"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Redémarrer"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Redémarrer la stratégie"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Assistance pour la politique de redémarrage"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Politique de redémarrage à suivre lors de la sortie des conteneurs."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Politique de redémarrage à suivre lorsque les conteneurs se terminent. "
+"L'utilisation de linger pour le démarrage automatique des conteneurs peut ne "
+"pas fonctionner dans certaines circonstances, comme lorsque ecryptfs, "
+"systemd-homed, NFS ou 2FA sont utilisés sur un compte utilisateur."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Restaurer"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Restaurer le conteneur $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Restaurer avec les connexions TCP établies"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Limité par les autorisations du compte de l'utilisateur"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Reprendre"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Tentatives"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Nouvelle tentative sur autre term."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Exécuter le bilan de fonctionnement"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "En fonctionnement"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Rechercher par nom ou description"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Recherche par registre"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Rechercher"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Chercher une image"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Chaîne de recherche ou emplacement du conteneur"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Recherche en cours..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Recherche en cours : $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Partagé"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Afficher"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Afficher les images"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Afficher les images intermédiaires"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Afficher moins de détails"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Montrer plus"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Taille"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Démarrer"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Période de démarrage"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Démarrer podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Commencez à saisir les données pour rechercher des images."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Commencé à"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "État"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Statut"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Arrêter"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Arrêté"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Prendre en charge le maintien des connexions TCP établies"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Système"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Le service Podman est également disponible"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Tag"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Étiquettes"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "L’interface utilisateur Cockpit pour les conteneur Podman."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Le temps d'initialisation nécessaire à l'amorçage d'un conteneur."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Le temps maximum autorisé pour compléter le contrôle de fonctionnement avant "
+"qu'un intervalle soit considéré comme ayant échoué."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Le nombre de tentatives autorisées avant qu'un contrôle de fonctionnement ne "
+"soit considéré non acceptable."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Délai d'attente"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Dépannage"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Entrez pour filtrer…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Impossible de charger l'historique des images"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Non acceptable"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "En cours d’exécution depuis $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Utiliser l'ancien format Docker"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Utilisé par"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Utilisateur"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Le service utilisateur Podman est également disponible"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Utilisateur :"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Valeur"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Volumes"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "En cas d'insalubrité"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Avec le terminal"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Accessible en écriture"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "conteneur"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "En cours de téléchargement"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:port]/[user]/container[:tag]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "image"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "dans"
+
+#: src/ImageDeleteModal.jsx:79
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate"
+msgstr "Cacher les images intermédiaires"
+
+#: src/ImageDeleteModal.jsx:59
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate image"
+msgstr "Cacher les images intermédiaires"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "n. d."
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "non disponible"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "groupe pod"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "ports"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "secondes"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "système"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "non utilisé"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "utilisateur :"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "volumes"
+
+#~ msgid "Delete $0"
+#~ msgstr "Supprimer $0"
+
+#~ msgid "select all"
+#~ msgstr "tout sélectionner"
+
+#~ msgid "Restarting"
+#~ msgstr "Redémarrage"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Confirmer la suppression de $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Confirmer la suppression du pod $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Confirmer la suppression forcée du pod $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Confirmer la suppression forcée de $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Le conteneur est actuellement en cours d’exécution."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr ""
+#~ "Ne pas inclure les changements effectués sur la racine du système de "
+#~ "fichier lors de l'export"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Par défaut avec sélection simple"
+
+#~ msgid "Start after creation"
+#~ msgstr "Démarrer après création"
+
+#, fuzzy
+#~| msgid "Delete tagged images"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Supprimer les images taguées"
+
+#~ msgid "created"
+#~ msgstr "créé"
+
+#~ msgid "exited"
+#~ msgstr "Quittés"
+
+#~ msgid "paused"
+#~ msgstr "Mis en pause"
+
+#~ msgid "running"
+#~ msgstr "en cours d'exécution"
+
+#~ msgid "stopped"
+#~ msgstr "arrêté"
+
+#, fuzzy
+#~| msgid "user:"
+#~ msgid "user"
+#~ msgstr "utilisateur :"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Ajouter une variable de build"
+
+#~ msgid "Commit image"
+#~ msgstr "Valider image"
+
+#~ msgid "Format"
+#~ msgstr "Format"
+
+#~ msgid "Message"
+#~ msgstr "Message"
+
+#~ msgid "Pause the container"
+#~ msgstr "Mettre le conteneur en pause"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Supprimer la variable de construction"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Définir le conteneur sur les variables de construction"
+
+#~ msgid "Add item"
+#~ msgstr "Ajouter un élément"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Port de l’hôte (optionnel)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (optionnel)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "LectureSeule"
+
+#~ msgid "IP prefix length"
+#~ msgstr "Taille du préfixe IP"
+
+#~ msgid "Get new image"
+#~ msgstr "Obtenir une nouvelle image"
+
+#~ msgid "Run"
+#~ msgstr "Exécuter"
+
+#~ msgid "On build"
+#~ msgstr "En construction"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Voulez-vous vraiment supprimer cette image ?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Impossible de s’attacher à ce conteneur : $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Impossible d’ouvrir le canal : $0"
+
+#~ msgid "Everything"
+#~ msgstr "Tout"
+
+#~ msgid "Security"
+#~ msgstr "Sécurité"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "L’analyse de $time ($type) n’a trouvé aucune vulnérabilité."
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr ""
+#~ "Cette version de la console web n’est compatible avec les terminaux."
diff --git a/po/ja.po b/po/ja.po
new file mode 100644
index 0000000..5adaf59
--- /dev/null
+++ b/po/ja.po
@@ -0,0 +1,1468 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2024-02-19 03:30+0000\n"
+"Last-Translator: Mie Yamamoto <myamamot@redhat.com>\n"
+"Language-Team: Japanese <https://translate.fedoraproject.org/projects/"
+"cockpit-podman/main/ja/>\n"
+"Language: ja\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.4\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 コンテナー"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 イメージ合計、$1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 2 番目"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 の未使用イメージ、$1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 - 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "コンテナーが異常な状態に移行したときに実行するアクション。"
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "ポートマッピングの追加"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "変数の追加"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "ボリュームの追加"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "すべて"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "すべてのレジストリー"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "常時"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "エラーが発生しました"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "作成者"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "起動時に podman を自動的に起動する"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU share のヘルプ"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU 共有"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU 共有は実行中のコンテナーの優先度を決定します。デフォルトの優先度は 1024 "
+"です。数値が高いほど、このコンテナーの優先度が高くなります。数値が低いほど優"
+"先度が低くなります。"
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "取り消し"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "ヘルスチェック"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "チェックポイント"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "チェックポイントおよび復元のサポート"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "チェックポイントコンテナー $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "クリックして公開されたポートを表示します"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "クリックしてボリュームを表示します"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Podman コンテナーの Cockpit コンポーネント"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "コマンド"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "コメント"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "コミット"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "コンテナーのコミット"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "設定済み"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "コンソール"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "コンテナー"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "コンテナーの作成に失敗しました"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "コンテナーの開始に失敗しました"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "コンテナーが実行されていません"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "コンテナー名"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "コンテナー名が必要です。"
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "コンテナーパス"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "コンテナーパスは空欄にできません"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "コンテナーポート"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "コンテナーポートは空欄にできません"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "コンテナー"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "作成"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "$0 コンテナーの現在の状態に基づいて、新しいイメージを作成します。"
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "作成して実行する"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "コンテナーの作成"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "$0 でのコンテナーの作成"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Pod でのコンテナーの作成"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Pod の作成"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "作成済み"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "作成元"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "CPU 共有を減らす"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "間隔を縮小する"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "最大再試行回数を減らす"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "メモリーを減らす"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "再試行回数を減らす"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "開始期間を縮小する"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "タイムアウトを縮小する"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "削除"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "$0 イメージを削除しますか?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "$0 を削除しますか?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "イメージの削除"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Pod $0 を削除しますか?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "タグ付けされたイメージの削除"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "使用されていないシステムイメージの削除:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "使用されていないユーザーイメージの削除:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "コンテナーを削除すると、コンテナー内のすべてのデータが削除されます。"
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "実行中のコンテナーを削除すると、その中のすべてのデータが削除されます。"
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "この Pod を削除すると、以下のコンテナーが削除されます:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "詳細"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "ディスクの空き容量"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Dockerフォーマットは、イメージをDockerまたはMoby Engineで共有する時に便利です"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "ダウンロード"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "新規イメージのダウンロード"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "空の Pod $0 は完全に削除されます。"
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "エントリーポイント"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "環境変数"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "エラー"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "エラーメッセージ"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "コンソールへの接続中にエラーが発生しました"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "例: 名前 <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "例: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "終了"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "失敗したヘルスの実行"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "コンテナー $0 のチェックポイントに失敗しました"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "コンテナーのクリーンアップに失敗しました"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "コンテナー $0 のコミットに失敗しました"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "コンテナー $0 の作成に失敗しました"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "イメージ $0 のダウンロードに失敗しました: $1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "コンテナー $0 を強制的に削除できませんでした"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "イメージ $0 の強制削除に失敗しました"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Pod $0 の強制再起動に失敗しました"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Pod $0 の強制停止に失敗しました"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "コンテナー $0 の一時停止に失敗しました"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Pod $0 の一時停止に失敗しました"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "使用されていないイメージの prune に失敗しました"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "使用されていないイメージのpruneに失敗しました"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "イメージ $0 のpullに失敗しました"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "コンテナー $0 の削除に失敗しました"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "イメージ $0 の削除に失敗しました"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "コンテナーの名前変更に失敗しました $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "コンテナー $0 の再起動に失敗しました"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Pod $0 の再起動に失敗しました"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "コンテナー $0 の復元に失敗しました"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "コンテナー $0 のレジュームに失敗しました"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Pod $0 の再開に失敗しました"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "コンテナー $0 の実行に失敗しました"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "コンテナーでのヘルスチェックの実行に失敗しました $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "イメージの検索に失敗しました。"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "イメージの検索に失敗しました: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "新規イメージの検索に失敗しました"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "コンテナー $0 の起動に失敗しました"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Pod $0 の起動に失敗しました"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "コンテナー $0 の停止に失敗しました"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Pod $0 の停止に失敗しました"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "失敗の連続"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "強制コミット"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "削除の強制"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Pod $0 を強制的に削除しますか?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "再起動の強制"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "停止の強制"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "ゲートウェイ"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "ヘルスチェック"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "ヘルスチェックの間隔のヘルプ"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "ヘルスチェックの再試行のヘルプ"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "ヘルスチェックの開始期間のヘルプ"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "ヘルスチェックのタイムアウトのヘルプ"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "ヘルスチェック失敗時のアクションのヘルプ"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "健全"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "イメージを非表示"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "中間イメージを非表示"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "履歴"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "ホストパス"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "ホストポート"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "ホストポートヘルプ"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP アドレス"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "IP アドレスヘルプ"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "開発に最適"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "実行中のサービスに最適"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"ホスト IP を 0.0.0.0 に設定した場合、またはまったく設定しなかった場合、ポート"
+"はホスト上のすべての IP にバインドされます。"
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"ホストポートが設定されていない場合、コンテナーポートにはホストのポートがラン"
+"ダムに割り当てられます。"
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "静的に設定された場合は IP アドレスを無視"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "静的に設定された場合は MAC アドレスを無視"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "イメージ"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "イメージ名は一意ではありません"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "イメージ名が必要です"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "イメージ選択のヘルプ"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "イメージ"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "CPU 共有を増やす"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "間隔を増やす"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "最大再試行回数を増やす"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "メモリーを増やす"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "再試行を増やす"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "開始期間を拡大する"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "タイムアウトを拡大する"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "インテグレーション"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "間隔"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "ヘルスチェックの実行頻度 (間隔)。"
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"無効な文字です。名前には文字、数字、および特定の句読点 (_ . -) のみを使用でき"
+"ます。"
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "すべての一時的なチェックポイントファイルを維持"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "キー"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "キーは空欄にできません"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "過去 5 回の実行"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "最終チェックポイント"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "チェックポイントをディスクに書き込む後もそのまま実行したままにする"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "詳細をロード中..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "ログをロード中..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "ロード中..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "ローカル"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "ローカルのイメージ"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "ログ"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC アドレス"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "最大施行数"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "メモリ"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "メモリー制限"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "メモリーユニット"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "モード"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"このイメージには、複数のタグが存在します。削除するタグ付けされたイメージを選"
+"択します。"
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "有効な IP アドレスにしてください"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "名前"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "名前はすでに使用中です"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "新しいコンテナー名"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "新しいイメージ名"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "いいえ"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "アクションなし"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "コンテナーなし"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "このイメージを使用するコンテナーはありません"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "この Pod 内のコンテナーはありません"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "現在のフィルターに一致するコンテナーがありません"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "環境変数が指定されていません"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "イメージなし"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "イメージが見つかりません"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "現在のフィルターに一致するイメージがありません"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "ラベルなし"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "開放されているポートはありません"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "$0 の結果なし"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "実行中のコンテナーはありません"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "指定されているボリュームはありません"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "障害発生時"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "実行のみ"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "オプション"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "所有者"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "所有者のヘルプ"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "合格したヘルスの実行"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"一括インポートのために、キー=値ペアからなる 1 つまたは複数の行を任意のフィー"
+"ルドに貼り付けます"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "一時停止"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "イメージの作成時にコンテナーを一時停止します"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "一時停止"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Pod の作成に失敗しました"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Pod 名"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman コンテナー"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman サービスがアクティブではありません"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "ポートマッピング"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "ポート"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "1024 未満のポートをマッピングできます"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "プライベート"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "プロトコル"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "削除"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "使用していないコンテナーの削除"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "未使用イメージの削除"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "コンテナーに prune を実行中"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "イメージを削除中"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "最新イメージのプル"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "プル中"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "読み取り専用アクセス"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "読み書きアクセス"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "アイテムの削除"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "選択した実行中でないコンテナーを削除します"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "削除中"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "名前変更"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "コンテナーの名前変更 $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "リソース制限を設定できます"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "再起動"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "再起動ポリシー"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "再起動ポリシーのヘルプ"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "コンテナーの終了時に従う再起動ポリシー。"
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"コンテナーの終了時に従う再起動ポリシー。コンテナーの自動起動に linger を使用"
+"すると、ユーザーアカウントで ecryptfs、systemd-homed、NFS、または 2FA が使用"
+"されている場合など、一部の状況では機能しない場合があります。"
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "復元"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "コンテナー $0 の復元"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "確立された TCP 接続での復元"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "ユーザーアカウントのパーミッションによって制限"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "再開"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "再試行回数"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "別の用語を再試行します。"
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "ヘルスチェックを実行する"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "実行中"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "名前または説明による検索"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "レジストリーで検索"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "検索"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "イメージの検索"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "検索文字列またはコンテナーのロケーション"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "検索中..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "検索中: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "共有"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "表示"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "イメージの表示"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "中間イメージの表示"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "簡易表示"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "詳細表示"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "サイズ"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "開始"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "開始期間"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Podman を起動"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "イメージを検索するために入力を開始します。"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "開始日時"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "状態"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "ステータス"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "停止"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "停止中"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "確立された TCP 接続の保持サポート"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "システム"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "システム Podman サービスも利用できます"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "タグ"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "タグ"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Podman コンテナーの Cockpit ユーザーインターフェイス。"
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "コンテナーのブートストラップに必要な初期化時間。"
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"間隔が失敗したとみなされる前に、ヘルスチェックを完了するために許容される最大"
+"時間。"
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr "ヘルスチェックが異常であると見なされるまでに許可される再試行の回数。"
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "タイムアウト"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "トラブルシュート"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "入力してフィルタリング…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "イメージ履歴をロードできません"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "異常"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "$0 から稼働中"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "レガシーの Docker フォーマットの使用"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "使用中"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "ユーザー"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "ユーザーの Podman サービスも利用できます"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "ユーザー:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "値"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "ボリューム"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "異常な場合"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "端末の使用"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "書き込み可能"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "コンテナー"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "ダウンロード中"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:port]/[user]/container[:tag]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "イメージ"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "場所"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "中間"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "中間イメージ"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "N/A"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "利用できません"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "Pod グループ"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "Podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "ポート"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "秒"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "システム"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "未使用"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "ユーザー:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "ボリューム"
+
+#~ msgid "Delete $0"
+#~ msgstr "$0 の削除"
+
+#~ msgid "select all"
+#~ msgstr "すべて選択"
+
+#~ msgid "Restarting"
+#~ msgstr "再起動中"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "$0 の削除を確定する"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Pod $0 の削除を確定する"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Pod $0 の強制削除を確定する"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "$0 の強制削除を確定する"
+
+#~ msgid "Container is currently running."
+#~ msgstr "コンテナーは現在実行中です。"
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "エクスポート時に root ファイルシステムの変更を含めないでください"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "単一項目のみ選択可能なデフォルト"
+
+#~ msgid "Start after creation"
+#~ msgstr "作成後に開始"
+
+#, fuzzy
+#~| msgid "Delete tagged images"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "タグ付けされたイメージの削除"
+
+#~ msgid "created"
+#~ msgstr "作成済み"
+
+#~ msgid "exited"
+#~ msgstr "終了"
+
+#~ msgid "paused"
+#~ msgstr "一時停止"
+
+#~ msgid "running"
+#~ msgstr "実行中"
+
+#~ msgid "stopped"
+#~ msgstr "停止中"
+
+#, fuzzy
+#~| msgid "user:"
+#~ msgid "user"
+#~ msgstr "ユーザー:"
+
+#~ msgid "Add on build variable"
+#~ msgstr "ビルド変数の追加"
+
+#~ msgid "Commit image"
+#~ msgstr "イメージのコミット"
+
+#~ msgid "Format"
+#~ msgstr "フォーマット"
+
+#~ msgid "Message"
+#~ msgstr "メッセージ"
+
+#~ msgid "Pause the container"
+#~ msgstr "コンテナーの一時停止"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "ビルド変数の削除"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "ビルド変数にコンテナーを設定する"
+
+#~ msgid "Add item"
+#~ msgstr "アイテムの追加"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "ホストポート (オプション)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (任意)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "読み取り専用"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP プレフィックスの長さ"
+
+#~ msgid "Run"
+#~ msgstr "実行"
diff --git a/po/ka.po b/po/ka.po
new file mode 100644
index 0000000..de9484c
--- /dev/null
+++ b/po/ka.po
@@ -0,0 +1,1409 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-11-29 13:31+0000\n"
+"Last-Translator: Temuri Doghonadze <temuri.doghonadze@gmail.com>\n"
+"Language-Team: Georgian <https://translate.fedoraproject.org/projects/"
+"cockpit-podman/main/ka/>\n"
+"Language: ka\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 ცალი კონტეინერი"
+msgstr[1] "$0 კონტეინერი"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 ცალი გამოსახულება, $1"
+msgstr[1] "$0 გამოსახულება, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 წამი"
+msgstr[1] "$0 წამი"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 ცალი გამოუყენებელი გამოსახულება, $1"
+msgstr[1] "$0 გამოუყენებელი გამოსახულება, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1-დან 65535-მდე"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+"ქმედება, რომელიც კონტეინერის არაჯანმრთელ მდომარეობაში გადასვლისას შესრულდება."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "პორტის ასახვის დამატება"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "ცვლადის დამატება"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "საცავის დამატება"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "ყველა"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "ყველა რეგისტრები"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "ყველთვის"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "შეცდომა"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "ავტორი"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "podman-ის გაშვება სისტემის ჩატვირთვისას"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "პროცესორი"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "პროცესორის გაზიარების დახმარება"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU გაზიარება"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU-ის ზიარები განსაზღვრავენ გაშვებულიკონტეინერების პრიორიტეტს. ნაგულისხმები "
+"პრიორიტეტი 1024-ის ტოლია. რაც მეტია რიცხვი, მით მეტია პრიორიტეტი; რაც "
+"ნაკლები -ნაკლები."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "გაუქმება"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "ჯანმრთელობის შემოწმება"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "საკონტროლო წერტილი"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "საკონტროლო წერტილის და აღდგენის მხარდაჭერა"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "საკონტროლო წერტილის კონტეინერი ($0)"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "დააწკაპუნეთ გამოჩენილი პორტების სანახავად"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "დააწკაპუნეთ ტომების სანახავად"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Cockpit-ის კომპონენტი Podman-ის კონტეინერებისთვის"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "ბრძანება"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "კომენტარები"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "გადაცემა"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "კონტეინერის გაგზავნა"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "მორგებულია"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "კონსოლი"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "კონტეინერი"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "კონტეინერის შექმნის შეცდომა"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "კონტეინერის გაშვების შეცდომა"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "კონტეინერი გაშვებული არაა"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "კონტეინერის სახელი"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "კონტეინერის სახელი აუცილებელია."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "კონტეინერის ბილიკი"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "კონტეინერის ბილიკი არ შეიძლება ცარიელი იყოს"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "კონტეინერის პორტი"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "კონტეინერის პორტი არ შეიძლება ცარიელი იყოს"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "კონტეინერები"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "შექმნა"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "კონტეინერის ($0) მდგომარეობაზე დამყარებული ახალი გამოსახულების შექმნა."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "შექმნა და გაშვება"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "კონტეინერის შექმნა"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "$0-ში კონტეინერის შექმნა"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "pod-ში კონტეინერის შექმნა"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "pod-ის შექმნა"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "შექმნილია"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "ავტორი"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "CPU გაზიარებების შემცირება"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "ინტერვალის შემცირება"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "ცდების მაქსიმალური რაოდენობის შემცირება"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "მეხსიერების შემცირება"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "ცდების რაოდენობის შემცირება"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "გაშვების პერიოდის შემცირება"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "მოლოდინის დროის შემცირება"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "წაშლა"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "წავშალო $0 დისკის ასლი?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "წავშალო $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "დისკის ასლის წაშლა"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "წავშალო პოდი $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "ჭდეებიანი გამოსახულებების წაშლა"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "სისტემის გამოუყენებელი გამოსახულებების წაშლა:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "მომხმარებლის გამოუყენებელი გამოსახულებების წაშლა:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "კონტეინერის წაშლა მასში ყველა მონაცემს წაშლის."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "გაშვებული კონტეინერის წაშლა მასში ყველა მონაცემს წაშლის."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "ამ Pod-ის წაშლა ასევე წაშლის შემდეგ კონტეინერებს:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "დეტალები"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "ადგილი დისკზე"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Docker-ის ფორმატი ძალიან სასარგებლოა გამოსახულების Docker-თან ან Moby "
+"Engline-სთან გაზიარებისთვის"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "გადმოწერა"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "ახალი გამოსახულების გადმოწერა"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "ცარიელი პოდი $0 სამუდამოდ წაიშლება."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "შესავალი წერტილი"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "გარემოს ცვლადები"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "შეცდომა"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "შეცდომის შეტყობინება"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "კონსოლთან დაკავშირების შეცდომა"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "მაგ. თქვენი სახელი <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "მაგალითად: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "გამოსული"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "ჯანმრთელობის შემოწმების შეცდომა"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "კონტეინერის ($0) საკონტროლო წერტილის შეცდომა"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "კონტეინერის გასუფთავების შეცდომა"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "კონტეინერის ($0) გადაცემის შეცდომა"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "კონტეინერის ($0) შექმნის შეცდომა"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "გამოსახულების ($0) გადმოწერის შეცდომა: $1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "კონტეინერის ($0) წაშლის შეცდომა"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "გამოსახულების ($0) ძალით წაშლა შეუძლებელია"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Pod-ის ($0) ძალით რესტარტის შეცდომა"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Pod-ის ($0) ძალით გაჩერების შეცდომა"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "კონტეინერის ($0) შეჩერების შეცდომა"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Pod-ის ($0) შეჩერების შეცდომა"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "გამოუყენებელი კონტეინერების წაკვეთის შეცდომა"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "გამოუყენებელი გამოსახულებების წაკვეთის შეცდომა"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "გამოსახულების ($0) გამოთხოვნის შეცდომა"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "კონტეინერის ($0) წაშლის შეცდომა"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "გამოსახულების ($0) წაშლის შეცდომა"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "კონტეინერის ($0) სახელის გადარქმევის შეცდომა"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "კონტეინერის ($0) რესტარტის შეცდომა"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Pod-ის ($0) რესტარტის შეცდომა"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "კონტეინერის ($0) აღდგენის შეცდომა"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "კონტეინერის ($0) გაგრძელების შეცდომა"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Pod-ის ($0) გაგრძელების შეცდომა"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "კონტეინერის ($0) გაშვების შეცდომა"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "კონტეინერის ($0) ჯანმრთელობის შემოწმების შეცდომა"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "გამოსახულებების ძებნის შეცდომა."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "გამოსახულების ძებნის შეცდომა: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "ახალი გამოსახულებების ძებნის შეცდომა"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "კონტეინერის ($0) გაშვების შეცდომა"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "pod-ს ($0) გაშვების შეცდომა"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "კონტეინერის ($0) გაჩერების შეცდომა"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "pod-ის ($0) გაჩერების შეცდომა"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "მრავალჯერადი წარუმატებლობა"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "ძალით გადაგზავნა"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "ძალით წაშლა"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "წავშლო ძალით პოდი $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "ძალით რესტარტი"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "ძალით გაჩერება"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "გბ"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "ნაგულისხმები რაუტერი"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "ჯანმრთელობის შემოწმება"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "ჯანმრთელობის შემოწმების ინტერვალის დახმარება"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "ჯანმრთელობის შემოწმების ცდების რაოდენობის დახმარება"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "ჯანმრთელობის შემოწმების გაშვების პერიოდის დახმარება"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "ჯანმრთელობის შემოწმების ვადის დახმარება"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "ჯანმრთელობის შემოწმების ქმედების დახმარება"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "ჯანმრთელი"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "გამოსახულებების დამალვა"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "შუალედური გამოსახულებების დამალვა"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "ისტორია"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "ჰოსტის ბილიკი"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "ჰოსტის პორტი"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "დახმარება ჰოსტის პორტის შესახებ"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP მისამართი"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "დახმარება IP მისამართის შესახებ"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "იდეალურია პროგრამირებისთვის"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "იდეალურია სერვისების გასაშვებად"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"თუ ჰოსტის IP დაყენებულია 0.0.0.0-ზე ან საერთოდ არაა დაყენებული, პორტი ჰოსტზე "
+"არსებულ ყველა IP-ს მიებმება."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"თუ ჰოსტის პორტი დაყენებული არაა, კონტეინერის პორტი ჰოსტზე შემთხვევითად "
+"იქნება არჩეული."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "სტატიკურად მინიჭებული IP მისამართის იგნორი"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "სტატიკურად მინიჭებული MAC მისამართის იგნორი"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "გამოსახულება"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "ასლის სახელი უნიკალური არაა"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "ასლის სახელი აუცილებელია"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "დახმარება გამოსახულების არჩევის შესახებ"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "გამოსახულებები"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "CPU გაზიარებების გაზრდა"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "ინტერვალის გაზრდა"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "ცდების მაქსიმალური რაოდენობის გაზრდა"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "მეხსიერების გაზრდა"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "ცდების რაოდენობის გაზრდა"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "გაშვების პერიოდის გაზრდა"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "მოლოდინის დროის გაზრდა"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "ინტეგრაცია"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "ინტერვალი"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "ჯანმრთელობის შემოწმების გაშვების ინტერვალი."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"არასწორი სიმბოლოები. სახელი მხოლოდ ასოებს, ციფრებს და ზოგიერთ პუნქტუაციის "
+"ნიშანს (_,-) შეიძლება შეიცავდეს."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "კბ"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "საკონტროლო წერტილის ყველა დროებითი ფაილის შენახვა"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "გასაღები"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "გასაღები არ შეიძლება, ცარიელი იყოს"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "ბოლო 5 გაშვება"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "უახლესი საკონტროლო წერტილი"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "საკონტროლო წერტილის დისკზე ჩაწერის შემდეგ გაშვებულად დატოვება"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "დეტალების ჩატვირთვა..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "ჟურნალის ჩატვირთვა..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "ჩატვირთვა..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "ლოკალური"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "ლოკალური გამოსახულებები"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "ჟურნალი"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC მისამართი"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "მბ"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "ცდების მაქსიმალური რაოდენობა"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "მეხსიერება"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "მეხსიერების ლიმიტი"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "მეხსიერების ერთეული"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "რეჟიმი"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"ამ გამოსახულებას მრავალი ჭდე გააჩნია. წასაშლელად მონიშნეთ ჭდეებიანი "
+"გამოსახულებები."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "უნდა იყოს სწორი IP მისამართი"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "სახელი"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "სახელი უკვე გამოიყენება"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "ახალი კონტეინერის სახელი"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "ახალი გამოსახულების სახელი"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "არა"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "ქმედების გარეშე"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "კონტეიენერების გარეშე"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "ამ გამოსახულებას არცერთი კონტეინერი არ იყენებს"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "ამ Pod-ში კონტეინერები არაა"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "მიმდინარე ფილტრს არცერთი კონტეინერი არ შეესაბამება"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "გარემოს ცვლადი მითითებული არაა"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "გამოსახულებების გარეშე"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "გამოსახულებების გარეშე"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "მიმდინარე ფილტრს არცერთი გამოსახულება არ შეესაბამება"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "ჭდეების გარეშე"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "პორტები გამოტანილი არაა"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "პასუხების გარეშე $0-თვის"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "გაშვებული კონტეინერების გარეშე"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "საცავი მითითებული არაა"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "შეცდომისას"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "მხოლოდ გაშვებული"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "მორგება"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "მფლობელი"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "მფლობელის დახმარება"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "ჯანმრთელობის შემოწმება წარმატებულია"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"მრავალი შემოტანისთვის ველებში ჩასვით ერთი ან მეტი გასაღები=მნიშვნელობა-ის "
+"ტიპის წყვილები"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "პაუზა"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "კონტეინერის შეჩერება გამოსახულების შექმნისას"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "შეჩერებულია"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Pod-ის შექმნის შეცდომა"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Pod-ის სახელი"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman-ის კონტეინერები"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman-ის სერვისი აქტიური არაა"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "პორტების ასახვა"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "პორტები"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "შეგიძლიათ 1024-ზე ნაკლები ნომრის მქონე პორტის მიბმა"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "პირადი"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "პროტოკოლი"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "შეკვეცა"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "გამოუყენებელი კონტეინერების წაკვეთა"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "გამოუყენებელი გამოსახულებების წაკვეთა"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "კონტეინერების წაკვეთა"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "გამოსახულებების წაკვეთა"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "უახლესი გამოსახულების წამოღება"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "გამოწევა"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "მხოლოდ-კითხვის წვდომა"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "ჩაწერა/წაკითხვის წვდომა"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "ელემენტის წაშლა"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "მონიშნული არა-გაშვებული კონტეინერების წაშლა"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "წაშლა"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "გადარქმევა"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "კონტეინერის სახელის გადარქმევა ($0)"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "შეგიძლიათ ლიმიტების დაყენება"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "გადატვირთვა"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "რესტარტის წესები"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "დახმარება რესტარტის წესების შესახებ"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "კონტეინერების მუშაობის დასასრულისას რესტარტის წესები."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"კონტეინერების მუშაობის დასრულებისას მისაყოლი გადატვირთის პოლიტიკა. "
+"კონტეინერების ავტომატური გაშვებისთვის linger-მა ყოველთვის შეიძლება არ "
+"იმუშაოს. მაგალითად, თუ მომხმარებელზე enccryptfs, systemd-homed, NFS ან 2FA "
+"გამოიყენება."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "აღდგენა"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "კონტეინერის აღდგენა ($0)"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "დამყარებული TCP კავშირებით აღდგენა"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "შეზღუდულია მომხმარებლის ანგარიშის წვდომებით"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "გაგრძელება"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "თავიდან ცდები"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "სხვა სიტყვა სცადეთ."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "ჯანმრთელობის შემოწმების გაშვება"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "გაშვებულია"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "სახელით ან აღწერით ძებნა"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "რეგისტრის ძებნა"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "ძებნა"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "გამოსახულების ძებნა"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "სტრიქონის ან კონტეინერის მდებარეობის ძებნა"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "ძებნა..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "ძებნა: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "გაზიარებული"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "ჩვენება"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "გამოსახულებების ჩვენება"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "შუალედური გამოსახულებების ჩვენება"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "ნაკლების ჩვენება"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "მეტის ჩვენება"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "ზომა"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "დაწყება"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "გაშვების პერიოდი"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Podman-ის გაშვება"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "გამოსახულებების მოსაძებნად დაიწყეთ მისი სახელის კრეფა."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "გაშვების დრო"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "მდგომარეობა"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "სტატუსი"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "გაჩერება"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "გაჩერებულია"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "დამყარებული TCP კავშირების შენარჩუნების მხარდაჭერა"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "სისტემა"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "ასევე ხელმისაწვდომია Podman-ის სისტემური სერვისი"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "ჭდე"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "ჭდეები"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Cockpit-ის მომხმარებლის ინტერფეისი Podman-ის კონტეინერებისთვის."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "კონტეინერის მოსარგებად საჭირო ინიციალიზაციის დრო."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"მაქსიმალური დრო ჯანმრთელობის შესამოწმებლად. ამ ინტერვალის გასვლის შემდეგ "
+"შემოწმება შეცდომის მქონედ ითვლება."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"ცდების რაოდენობა, რის შემდეგაც ჯანმრთელობის შემოწმება ავარიულად ითვლება."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "დროის ამოწურვა"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "პრობლემების პოვნა"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "გაფილტვრისთვის აკრიფეთ…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "გამოსახულების ისტორიის ჩატვირთვის შეცდომა"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "ჯანმრთელი არაა"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "$0 და ზემოთ"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Docker-ის ძველი ფორმატის გამოყენება"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "გამოიყენება"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "მომხმარებელი"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "ასევე ხელმისაწვდომია Podman-ის მომხმარებლის სერვისი"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "მომხმარებელი:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "მნიშვნელობა"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "საცავები"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "როცა ჯანმრთელი არაა"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "ტერმინალით"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "ჩაწერადი"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "კონტეინერი"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "გადმოწერა"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "ჰოსტი[:პორტი]/[მომხმარებელი]/კონტეინერი[:ჭდე]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "გამოსახულება"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "-ში"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "შუალედური"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "შუალედური დისკის ასლი"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "ა/მ"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "ხელმიუწვდომელია"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "pod-ების ჯგუფი"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "პორტები"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "წამი"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "სისტემა"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "გამოუყენებელი"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "მომხმარებელი:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "ტომები"
+
+#~ msgid "Delete $0"
+#~ msgstr "$0-ის წაშლა"
+
+#~ msgid "select all"
+#~ msgstr "ყველას მონიშვნა"
+
+#~ msgid "Failure action"
+#~ msgstr "ქმედება ავარიისას"
+
+#~ msgid "Restarting"
+#~ msgstr "გადატვირთვა"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "დაადასტურეთ $0-ის წაშლა"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "დაადასტურეთ Pod-ის წაშლა: $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "დაადასტურეთ pod-ის ძალით წაშლა: $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "დაადასტურეთ ძალით წაშლა: $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "ამჟამად კონტეინერი გაშვებულია."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "გატანისას ფაილური სისტემის ცვლილებების არ-გადატანა"
diff --git a/po/ko.po b/po/ko.po
new file mode 100644
index 0000000..140b0ae
--- /dev/null
+++ b/po/ko.po
@@ -0,0 +1,1462 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# FIRST AUTHOR <EMAIL@ADDRESS>, YEAR.
+#
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-11-29 13:31+0000\n"
+"Last-Translator: 김인수 <simmon@nplob.com>\n"
+"Language-Team: Korean <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/ko/>\n"
+"Language: ko\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 컨테이너"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 이미지 총계, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 초"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 미사용 이미지, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 ~ 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "컨테이너가 비정상 상태로 전이되면 수행 할 작업."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "포트 대응을 추가합니다"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "변수를 추가합니다"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "볼륨 추가"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "모두"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "모든 레지스터리"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "항상"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "오류가 발생했습니다"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "작성자"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "부트시 자동으로 podman 시작"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU 공유 도움말"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU 공유"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU 공유는 동작 중인 콘테이너 우선 순위를 결정합니다. 기본 우선순위는 1024입"
+"니다. 숫자가 클 수록 이 컨테이너 우선순위를 갖습니다. 낮은 번호는 우선 순위"
+"가 낮아집니다."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "취소"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "상태 점검 중"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "점검점"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "점검점과 복구 지원"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "점검점 컨테이너 $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "공개된 포트를 보려면 누르세요"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "볼륨을 보려면 누르세요"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "포드맨 컨테이너를 위한 cockpit 구성요소"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "명령"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "주석"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "수행"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "컨테이너 커밋"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "구성됨"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "콘솔"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "컨테이너"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "컨테이너 생성에 실패했습니다"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "컨테이너 시작에 실패했습니다"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "컨테이너가 동작 중이 아닙니다"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "컨테이너 이름"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "컨테이너 이름이 필요합니다."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "컨테이너 경로"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "컨테이너 경로는 비워두면 안됩니다"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "컨테이너 포트"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "컨테이너 포트는 비워두면 안됩니다"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "컨테이너"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "생성"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "$0 컨테이너의 현재 상태에서 기반된 새로운 이미지를 생성합니다."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "생성과 실행"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "컨테이너 생성"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "$0에서 컨테이너 생성"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "포드에서 컨테이너 생성"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "포드 생성"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "생성일"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "생성됨"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "CPU 공유 감소"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "간격 감소"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "최대 재시도 감소"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "메모리 감소"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "재시도 감소"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "시작 기간 감소"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "시간종료 감소"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "삭제"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "$0 이미지를 삭제할까요?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "$0 삭제?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "이미지 삭제"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "pod $0 삭제?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "태그가 지정된 이미지 삭제"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "사용하지 않는 시스템 이미지 삭제:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "사용하지 않는 사용자 이미지 삭제:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "컨테이너를 삭제는 내부의 모든 자료도 제거됩니다."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "동작 중인 컨테이너 삭제는 내부의 모든 자료도 제거됩니다."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "이 pod의 삭제는 다음 컨테이너도 제거될 것입니다:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "상세정보"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "디스크 공간"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"도커 형식은 도커 또는 모비 엔진과 같은 이미지를 공유 할 때에 유용합니다"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "내려받기"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "신규 이미지 내려받기"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "빈 pod $0는 영구적으로 제거됩니다."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "시작점"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "환경 변수"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "오류"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "오류 메시지"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "오류가 콘솔 연결 중에 발생했습니다"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "예제, 당신의 이름 <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "예제: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "종료됨"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "상태 실행에 실패함"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "컨테이너 $0 점검점에 실패"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "컨테이너를 정리하는데 실패"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "컨테이너 $0 수행에 실패"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "컨테이너 $0 생성에 실패"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "이미지 $0:$1 내려받기에 실패"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "컨테이너 $0 강제 제거에 실패"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "이미지 $0 강제 제거에 실패"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "pod $0 강제로 재시작 하는데 실패"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "pod $0 강제 멈춤에 실패"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "컨테이너 $0를 일시 중지 하는 데 실패"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "pod $0 일시정지에 실패"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "사용하지 않는 컨테이너 정리 실패"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "사용하지 않는 이미지 정리 실패"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "이미지 $0를 가져오는 데 실패"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "컨테이너 $0 제거에 실패"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "이미지 $0 제거에 실패"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "컨테이너 $0의 이름을 변경하는데 실패함"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "컨테이너 $0 재시작에 실패"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "pod $0 재시작에 실패"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "컨테이너 $0 복구에 실패"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "컨테이너 $0를 다시 시작 하는 데 실패"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "pod $0 재개에 실패"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "컨테이너 $0를 실행하는 데 실패"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "컨테이너 $0에서 상태를 점검하는데 실패함"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "이미지 검색을 실패함."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "이미지를 위한 검색 실패: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "새로운 이미지 검색 실패"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "컨테이너 $0 시작에 실패"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "pod $0를 시작하는 데 실패"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "컨테이너 $0 멈춤에 실패"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "pod $0 멈춤에 실패"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "연속 실패"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "강제 커밋"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "강제 삭제"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "pod $0 강제 삭제?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "강제 재시작"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "강제 멈춤"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "게이트웨이"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "상태 점검"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "상태 점검 간격 도움말"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "상태 점검 재시도 도움말"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "상태 점검 시작 기간 도움말"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "상태 점검 시간종료 도움말"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "상태 장애 점검 작용 도움말"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "상태좋음"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "이미지 숨김"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "중간 이미지 숨기기"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "기록"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "호스트 경로"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "호스트 포트"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "호스트 포트 도움말"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP 주소"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "IP 주소 도움말"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "개발을 위해 이상적인"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "실행 중인 서비스에 이상적인"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"만약 호스트 IP가 0.0.0.0으로 설정되거나 또는 전혀 설정되지 않으면, 포트는 호"
+"스트의 모든 IP에서 묶이게 됩니다."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"만약 호스트 포트가 컨테이너에 설정되어 있지 않으면, 포트는 호스트에서 임의 지"
+"정된 포트가 됩니다."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "정적인 상태라면 IP 주소 무시"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "정적인 상태라면 맥 주소 무시"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "이미지"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "이미지 이름은 독특하지 않습니다"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "이미지 이름이 필요합니다"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "이미지 선택 도움말"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "이미지"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "CPU 공유 증가"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "간격 증가"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "최대 재시도 증가"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "메모리 증가"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "재시도 증가"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "시작 기간 증가"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "시간종료 증가"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "통합"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "간격"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "얼마나 자주 상태 점검을 실행하는 간격."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"잘못된 문자. 이름은 문자, 수자와 특정 구두점(_ . -)만 포함 될 수 있습니다."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "모든 임시 점검점 파일을 유지합니다"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "키"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "키는 비워두면 안됩니다"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "최근 5회 실행"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "최신 점검점"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "점검점을 디스크에 쓰기 후에 계속 진행"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "세부정보 적재 중..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "기록 적재 중..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "적재 중..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "로컬"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "로컬 이미지"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "기록"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "맥(mac) 주소"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "최대 재시도"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "메모리"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "메모리 제한"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "메모리 장치"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "모드"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr "다중 태그는 이 이미지에 존재합니다. 삭제에 태그된 이미지를 선택합니다."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "유효한 IP 주소이어야 합니다"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "이름"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "이미 사용 중인 이름입니다"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "신규 컨테이너 이름"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "신규 이미지 이름"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "아니오"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "반응 없음"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "컨테이너 없음"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "이 이미지에 사용 중인 컨테이너가 없습니다"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "이 포드에는 컨테이너가 없습니다"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "현재 필터에 일치하는 컨테이너가 없습니다"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "지정된 환경 변수가 없습니다"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "이미지 없음"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "이미지를 찾지 못했습니다"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "현재 필터에 일치하는 이미지가 없습니다"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "이름표가 없습니다"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "노출된 포트가 없습니다"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "$0의 결과를 찾을 수 없습니다"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "실행 중인 컨테이너가 없습니다"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "지정된 볼륨이 없습니다"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "실패한 경우"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "실행 만"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "옵션"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "소유자"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "소유자 도움말"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "상태 실행 통과됨"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"대량으로 가져오기 위해 키=값 쌍의 하나 이상의 행에 입력부분에 붙여 넣습니다"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "일시정지"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "이미지에 생성 중인 컨테이너를 일시 중지합니다"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "일시정지됨"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "포드 생성에 실패했습니다"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "포드 이름"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "포드맨"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "포드맨 컨테이너"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "포드맨 서비스가 동작하지 않습니다"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "포트 대응"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "포트"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "1024 이하 포트는 대응 될 수 있습니다"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "비공개"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "통신규약"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "프룬"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "사용하지 않은 컨테이너 정리"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "프룬 사용하지 않은 이미지"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "컨테이너 정리 중"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "프루닝 이미지"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "최신 이미지 가져오기"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "당기기"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "읽기-전용 접근"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "읽기-쓰기 접근"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "항목 제거"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "선택된 비-실행 컨테이너를 제거합니다"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "제거 중"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "이름변경"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "컨테이너 $0 이름변경"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "자원 한계가 설정 될 수 있습니다"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "재시작"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "재시작 정책"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "정책 도움말 재시작"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "컨테이너를 종료 할 경우에 따라야 할 정책을 재시작 합니다."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"컨테이너가 종료 될 때 따라야 할 정책을 재시작합니다. 자동으로 시작하는 컨테이"
+"너를 위한 linger를 사용은 ecryptfs, systemd-homed, NFS, 또는 2FA가 사용자 계"
+"정에서 사용되는 것과 같이 특정 상황에서 작동하지 않을 수 있습니다."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "복구"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "컨테이너$0에서 복구"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "설정된 TCP 연결로 복구"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "사용자 계졍 권한에 의해 제한됨"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "다시 시작"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "재시도"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "다른 용어를 재시도."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "상태 점검을 실행"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "작동중"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "이름 또는 설명으로 찾기"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "레지스터리로 검색"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "찾기"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "이미지로 찾기"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "문자열 또는 컨테이너 위치 검색"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "검색 중..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "검색 중: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "공유됨"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "보기"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "이미지 보여주기"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "중간 이미지 보기"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "덜 보기"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "더 보기"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "크기"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "시작"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "시작 주기"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "포드맨 시작"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "이미지를 찾으려면 입력을 시작하세요."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "시작 시간"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "상태"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "상태"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "중지"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "정지됨"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "설정된 TCP 연결 유지 지원"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "시스템"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "시스템 포드맨 서비스도 사용 할 수 있습니다"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "꼬리표"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "꼬리표"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "포드맨 컨테이너를 위한 cockpit 사용자 연결장치."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "부트스트랩에서 컨테이너를 위해 필요한 초기화 시간."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"간격이 실패한 것으로 간주되기 전에 상태 점검을 완료하도록 허용되는 최대 시간."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr "상태점검이 좋지 않은 것으로 고려되기 전에 허용되는 재시도 수."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "시간종료"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "문제 해결"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "필터 입력…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "이미지 기록을 적재 할 수 없음"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "상태나쁨"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "$0 이후 상승"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "레거시 도커 형식을 사용합니다"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "사용되었습니다"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "사용자"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "사용자 포드맨 서비스가 사용 할 수 없습니다"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "사용자:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "값"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "볼륨"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "상태나쁨"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "터미널 포함"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "쓰기 가능"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "컨테이너"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "내려받기 중"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:포트]/[사용자]/컨테이너[:태그]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "이미지"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "in"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "중간"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "중간 이미지"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "해당 없음"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "사용할 수 없음"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "포드 그룹"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "포드맨"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "포트"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "초"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "systemd"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "미사용"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "사용자:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "볼륨"
+
+#~ msgid "Delete $0"
+#~ msgstr "$0 삭제"
+
+#~ msgid "select all"
+#~ msgstr "모두 선택"
+
+#~ msgid "Failure action"
+#~ msgstr "장애 작용"
+
+#~ msgid "Restarting"
+#~ msgstr "재시작하기"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "$0 삭제를 확인합니다"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "pod $0의 삭제를 확인합니다"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "pod $0의 강제 삭제를 확인합니다"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "$0의 강제된 삭제를 확인합니다"
+
+#~ msgid "Container is currently running."
+#~ msgstr "컨테이너가 현재 실행 중입니다."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "내보내기 할 때에 root 파일-시스템 변경을 포함하지 마세요"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "단일 선택 가능한 기본값"
+
+#~ msgid "Start after creation"
+#~ msgstr "생성 후에 시작"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "사용하지 않는 $0 이미지 삭제:"
+
+#~ msgid "created"
+#~ msgstr "생성 됨"
+
+#~ msgid "exited"
+#~ msgstr "종료됨"
+
+#~ msgid "paused"
+#~ msgstr "일시 중지됨"
+
+#~ msgid "running"
+#~ msgstr "실행 중"
+
+#~ msgid "stopped"
+#~ msgstr "정지됨"
+
+#~ msgid "user"
+#~ msgstr "사용자"
+
+#~ msgid "Add on build variable"
+#~ msgstr "빌드 변수에 추가"
+
+#~ msgid "Commit image"
+#~ msgstr "이미지 수행"
+
+#~ msgid "Format"
+#~ msgstr "포멧"
+
+#~ msgid "Message"
+#~ msgstr "메세지"
+
+#~ msgid "Pause the container"
+#~ msgstr "컨테이너 일시중지"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "빌드 변수에서 제거"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "빌드 변수에 컨테이너 설정"
+
+#~ msgid "Add item"
+#~ msgstr "항목 추가"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "호스트 경로(선택적)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (선택적)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "읽기 전용"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP 프리픽스 길이"
+
+#~ msgid "Run"
+#~ msgstr "실행"
diff --git a/po/pl.po b/po/pl.po
new file mode 100644
index 0000000..2472cd8
--- /dev/null
+++ b/po/pl.po
@@ -0,0 +1,1509 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Piotr Drąg <piotrdrag@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2024-02-23 19:36+0000\n"
+"Last-Translator: Daviteusz <daviteusz0@gmail.com>\n"
+"Language-Team: Polish <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/pl/>\n"
+"Language: pl\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n==1 ? 0 : n%10>=2 && n%10<=4 && (n%100<10 "
+"|| n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 5.4\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 kontener"
+msgstr[1] "$0 kontenery"
+msgstr[2] "$0 kontenerów"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 obraz, $1"
+msgstr[1] "$0 obrazy razem, $1"
+msgstr[2] "$0 obrazów razem, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 sekunda"
+msgstr[1] "$0 sekundy"
+msgstr[2] "$0 sekund"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 nieużywany obraz, $1"
+msgstr[1] "$0 nieużywane obrazy, $1"
+msgstr[2] "$0 nieużywanych obrazów, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 do 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "Działanie podejmowane po przejściu kontenera do niezdrowego stanu."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Dodaj mapowanie portów"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Dodaj zmienną"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Dodaj wolumin"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Wszystko"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Wszystkie rejestry"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Zawsze"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Wystąpił błąd"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Autor"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Automatyczne włączanie usługi podman podczas uruchamiania"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "Procesor"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Pomoc udziałów procesora"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Udziały procesora"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"Udziały procesora ustalają priorytety uruchomionych kontenerów. Domyślny "
+"priorytet to 1024. Wyższa liczba zwiększa priorytet danego kontenera. "
+"Mniejsza liczba zmniejsza priorytet."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Anuluj"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Sprawdzanie zdrowia"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Punkt kontrolny"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Obsługa punktu kontrolnego i przywracania"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Kontener punktu kontrolnego $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Kliknięcie wyświetli opublikowane porty"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Kliknięcie wyświetli woluminy"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Składnik Cockpit do kontenerów Podman"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Polecenie"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Komentarze"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Zatwierdź"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Zatwierdź kontener"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Skonfigurowane"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konsola"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Kontener"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Utworzenie kontenera się nie powiodło"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Uruchomienie kontenera się nie powiodło"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Kontener nie jest uruchomiony"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Nazwa kontenera"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Wymagana jest nazwa kontenera."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Ścieżka do kontenera"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "Ścieżka do kontenera nie może być pusta"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Port kontenera"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "Port kontenera nie może być pusty"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Kontenery"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Utwórz"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Utwórz nowy obraz na podstawie obecnego stanu kontenera $0."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Utwórz i uruchom"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Utwórz kontener"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Utwórz kontener w $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Utwórz kontener w pojemniku"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Utwórz pojemnik"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Utworzono"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Utworzony przez"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Zmniejsz udziały procesora"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Zmniejsz odstęp"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Zmniejsz maksymalną liczbę ponownych prób"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Zmniejsz pamięć"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Zmniejsz liczbę ponownych prób"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Zmniejsz okres uruchamiania"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Zmniejsz czas oczekiwania"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Usuń"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "Usunąć obraz $0?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Usunąć $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Usuń obraz"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Usunąć pojemnik $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Usuń oznaczone obrazy"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Usunięcie nieużywanych obrazów systemowych:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Usunięcie nieużywanych obrazów użytkownika:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr ""
+"Usunięcie kontenera spowoduje usunięcie wszystkich zawartych na nim danych."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr ""
+"Usunięcie uruchomionego kontenera spowoduje usunięcie wszystkich zawartych "
+"na nim danych."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Usunięcie tego pojemnika spowoduje usunięcie tych kontenerów:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Szczegóły"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Miejsce na dysku"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Format Docker jest przydatny podczas udostępniania obrazu za pomocą Dockera "
+"lub Moby Engine"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Pobierz"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Pobierz nowy obraz"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Pusty pojemnik $0 zostanie trwale usunięty."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Punkt wejścia"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Zmienne środowiskowe"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Błąd"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Komunikat o błędzie"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Wystąpił błąd podczas łączenia konsoli"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Przykład, Imię Nazwisko <imięnazwisko@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Przykład: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Zakończono"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Niepomyślny wynik sprawdzania zdrowia"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Punkt kontrolny kontenera $0 się nie powiódł"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Wyczyszczenie kontenera się nie powiodło"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Zatwierdzenie kontenera $0 się nie powiodło"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Utworzenie kontenera $0 się nie powiodło"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Pobranie obrazu $0:$1 się nie powiodło"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Wymuszenie usunięcia kontenera $0 się nie powiodło"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Wymuszenie usunięcia obrazu $0 się nie powiodło"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Wymuszenie ponownego uruchomienia pojemnika $0 się nie powiodło"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Wymuszenie zatrzymania pojemnika $0 się nie powiodło"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Wstrzymanie kontenera $0 się nie powiodło"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Wstrzymanie pojemnika $0 się nie powiodło"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Wyczyszczenie nieużywanych kontenerów się nie powiodło"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Wyczyszczenie nieużywanych obrazów się nie powiodło"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Pobranie obrazu $0 się nie powiodło"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Usunięcie kontenera $0 się nie powiodło"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Usunięcie obrazu $0 się nie powiodło"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Zmiana nazwy kontenera $0 się nie powiodła"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Ponowne uruchomienie kontenera $0 się nie powiodło"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Ponowne uruchomienie pojemnika $0 się nie powiodło"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Przywrócenie kontenera $0 się nie powiodło"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Wznowienie kontenera $0 się nie powiodło"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Wznowienie pojemnika $0 się nie powiodło"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Uruchomienie kontenera $0 się nie powiodło"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Wykonanie sprawdzania zdrowia na kontenerze $0 się nie powiodło"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Wyszukanie obrazów się nie powiodło."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Wyszukanie obrazów się nie powiodło: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Wyszukanie nowych obrazów się nie powiodło"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Uruchomienie kontenera $0 się nie powiodło"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Uruchomienie pojemnika $0 się nie powiodło"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Zatrzymanie kontenera $0 się nie powiodło"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Zatrzymanie pojemnika $0 się nie powiodło"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Wielokrotne niepowodzenia"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Wymuś zatwierdzenie"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Wymuś usunięcie"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Wymusić usunięcie pojemnika $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Wymuś ponowne uruchomienie"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Wymuś zatrzymanie"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Brama"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Sprawdzanie zdrowia"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Pomoc odstępów między sprawdzaniem zdrowia"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Pomoc ponownych prób sprawdzania zdrowia"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Pomoc okresu uruchamiania sprawdzania zdrowia"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Pomoc czasu oczekiwania sprawdzania zdrowia"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Pomoc działania niepowodzenia sprawdzania zdrowia"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Zdrowy"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Ukryj obrazy"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Ukryj pośrednie obrazy"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historia"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Ścieżka do hosta"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Port hosta"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Pomoc portu hosta"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "Identyfikator"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "Adres IP"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Pomoc adresu IP"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Doskonałe dla programistów"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Doskonałe do uruchamiania usług"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Jeśli adres IP hosta jest ustawiony na 0.0.0.0 lub nie jest ustawiony, to "
+"port będzie dowiązany do wszystkich adresów IP na hoście."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Jeśli port hosta nie jest ustawiony, to do portu kontenera będzie losowo "
+"przydzielany port na hoście."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ignorowanie adresu IP, jeśli jest ustawiony statycznie"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ignorowanie adresu MAC, jeśli jest ustawiony statycznie"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Obraz"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Nazwa obrazu nie jest unikalna"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Nazwa obrazu jest wymagana"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Pomoc przy wyborze obrazu"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Obrazy"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Zwiększ udziały procesora"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Zwiększ odstęp"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Zwiększ maksymalną liczbę ponownych prób"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Zwiększ pamięć"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Zwiększ liczbę ponownych prób"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Zwiększ okres uruchamiania"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Zwiększ czas oczekiwania"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integracja"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Odstęp"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Odstęp między wykonywaniem sprawdzania zdrowia."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Nieprawidłowe znaki. Nazwa może zawierać tylko litery, cyfry i część znaków "
+"interpunkcyjnych (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Przechowywanie wszystkich tymczasowych plików punktów kontrolnych"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Klucz"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "Klucz nie może być pusty"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Ostatnie 5 razy"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Najnowszy punkt kontrolny"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Bez wyłączania po zapisaniu punktu kontrolnego na dysku"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Wczytywanie szczegółów…"
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Wczytywanie dzienników…"
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Wczytywanie…"
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Lokalne"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Lokalne obrazy"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Dzienniki"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "Adres MAC"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Maksymalna liczba ponownych prób"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Pamięć"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Ograniczenie pamięci"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Jednostka pamięci"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Tryb"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Dla tego obrazu istnieje wiele etykiet. Proszę wybrać oznaczone obrazy do "
+"usunięcia."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "Musi być prawidłowym adresem IP"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Nazwa"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Nazwa jest już używana"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Nowa nazwa kontenera"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Nazwa nowego obrazu"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Nie"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Brak działania"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Brak kontenerów"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Żadne kontenery nie używają tego obrazu"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Brak kontenerów w tym pojemniku"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Żadne kontenery nie pasują do obecnego filtru"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Nie podano zmiennych środowiskowych"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Brak obrazów"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Nie odnaleziono obrazów"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Żadne obrazy nie pasują do obecnego filtru"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Brak etykiety"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Brak eksponowanych portów"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Brak wyników dla zapytania „$0”"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Brak uruchomionych kontenerów"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Nie podano woluminów"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Przy niepowodzeniu"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Tylko uruchomione"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Opcje"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Właściciel"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Pomoc na temat właściciela"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Pomyślny wynik sprawdzania zdrowia"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Należy wkleić jeden lub więcej wierszy par klucz=wartość do dowolnego pola, "
+"aby zaimportować hurtowo"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Wstrzymaj"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Wstrzymaj kontener podczas tworzenia obrazu"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Wstrzymane"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Utworzenie pojemnika się nie powiodło"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Nazwa pojemnika"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Kontenery Podman"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Usługa Podman nie jest aktywna"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Mapowanie portów"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Porty"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Można mapować porty poniżej 1024"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Prywatne"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokół"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Wyczyść"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Wyczyść nieużywane kontenery"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Wyczyść nieużywane obrazy"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Czyszczenie kontenerów"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Czyszczenie obrazów"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Pobierz najnowszy obraz"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Pobieranie"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Dostęp tylko do odczytu"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Dostęp do odczytu i zapisu"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Usuń element"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Usuwa zaznaczone nieuruchomione kontenery"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Usuwanie"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Zmień nazwę"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Zmień nazwę kontenera $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Można ustawić ograniczenia zasobów"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Uruchom ponownie"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Zasada ponownego uruchamiania"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Pomoc zasady ponownego uruchamiania"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Zasada ponownego uruchamiania używana podczas wyłączania kontenerów."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Zasada ponownego uruchamiania używana podczas wyłączania kontenerów. "
+"Używanie zwlekania do automatycznego uruchamiania kontenerów może nie "
+"działać w pewnych warunkach, takich jak podczas używania eCryptfs, systemd-"
+"homed, NFS lub uwierzytelniania dwuskładnikowego na koncie użytkownika."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Przywróć"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Przywróć kontener $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Przywróć z nawiązanymi połączeniami TCP"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Ograniczone przez uprawnienia konta użytkownika"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Wznów"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Ponowne próby"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Spróbuj innych słów."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Wykonaj sprawdzanie zdrowia"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Uruchomione"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Szukaj według nazwy lub opisu"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Szukaj według rejestru"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Szukaj"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Znajdź obraz"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Szukaj ciągu lub położenia kontenera"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Wyszukiwanie…"
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Wyszukiwanie: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Współdzielone"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Wyświetl"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Wyświetl obrazy"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Wyświetl pośrednie obrazy"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Wyświetl mniej"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Wyświetl więcej"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Rozmiar"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Uruchom"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Okres uruchamiania"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Uruchom usługę podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Zacznij pisać, aby wyszukać obrazy."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Uruchomiono"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Stan"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Stan"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Zatrzymaj"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Zatrzymane"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Obsługa zachowywania nawiązanych połączeń TCP"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "System"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Dostępna jest także systemowa usługa Podman"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Etykieta"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Etykiety"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Interfejs Cockpit do kontenerów Podman."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Czas inicjacji wymagany do uruchomienia kontenera."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Maksymalny dozwolony czas na ukończenie sprawdzania zdrowia, zanim odstęp "
+"jest uważany za niepomyślny."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Dozwolona liczba ponownych prób, zanim wynik sprawdzania zdrowia jest "
+"uważany za niepomyślny."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Czas oczekiwania"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Rozwiąż problem"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Wyszukiwanie…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Nie można wczytać historii obrazu"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Niezdrowy"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Uruchomione od $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Przestarzały format Docker"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Używane przez"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Użytkownik"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Dostępna jest także usługa Podman użytkownika"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Użytkownik:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Wartość"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Woluminy"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Kiedy jest niezdrowy"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Z terminalem"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Zapisywalny"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "kontener"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "pobieranie"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "komputer[:port]/[użytkownik]/kontener[:etykieta]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "obraz"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "w"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "pośredni"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "pośredni obraz"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "niedostępne"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "niedostępne"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "grupa pojemników"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "porty"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "s"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "systemowa"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "nieużywane"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "użytkownik:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "woluminy"
+
+#~ msgid "Delete $0"
+#~ msgstr "Usuń $0"
+
+#~ msgid "select all"
+#~ msgstr "zaznacz wszystko"
+
+#~ msgid "Failure action"
+#~ msgstr "Działanie niepowodzenia"
+
+#~ msgid "Restarting"
+#~ msgstr "Uruchamianie ponownie"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Potwierdź usunięcie $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Potwierdź usunięcie pojemnika $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Potwierdź wymuszenie usunięcia pojemnika $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Potwierdź wymuszenie usunięcia $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontener jest obecnie uruchomiony."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Bez dołączania zmian głównego systemu plików podczas eksportowania"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Domyślne z jednym wyborem"
+
+#~ msgid "Start after creation"
+#~ msgstr "Uruchom po utworzeniu"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Usunięcie nieużywanych obrazów $0:"
+
+#~ msgid "created"
+#~ msgstr "utworzone"
+
+#~ msgid "exited"
+#~ msgstr "zakończone"
+
+#~ msgid "paused"
+#~ msgstr "wstrzymane"
+
+#~ msgid "running"
+#~ msgstr "uruchomione"
+
+#~ msgid "stopped"
+#~ msgstr "zatrzymane"
+
+#~ msgid "user"
+#~ msgstr "użytkownika"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Dodaj na zmiennej budowania"
+
+#~ msgid "Commit image"
+#~ msgstr "Zatwierdź obraz"
+
+#~ msgid "Format"
+#~ msgstr "Format"
+
+#~ msgid "Message"
+#~ msgstr "Komunikat"
+
+#~ msgid "Pause the container"
+#~ msgstr "Wstrzymaj kontener"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Usuń na zmiennej budowania"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Ustaw kontener na zmiennych budowania"
+
+#~ msgid "Add item"
+#~ msgstr "Dodaj element"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Port gospodarza (opcjonalnie)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (opcjonalnie)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Tylko do odczytu"
+
+#~ msgid "IP prefix length"
+#~ msgstr "Długość przedrostka IP"
+
+#~ msgid "Get new image"
+#~ msgstr "Pobierz nowy obraz"
+
+#~ msgid "Run"
+#~ msgstr "Uruchom"
+
+#~ msgid "On build"
+#~ msgstr "Podczas budowania"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Na pewno usunąć ten obraz?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Nie można dołączyć do tego kontenera: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Nie można otworzyć kanału: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Wszystko"
+
+#~ msgid "Security"
+#~ msgstr "Zabezpieczenia"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Wyszukiwanie z $time ($type) nie znalazło żadnych zagrożeń."
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "Ta wersja konsoli internetowej nie obsługuje terminala."
diff --git a/po/sk.po b/po/sk.po
new file mode 100644
index 0000000..c95236a
--- /dev/null
+++ b/po/sk.po
@@ -0,0 +1,1554 @@
+# Matej Marusak <mmarusak@redhat.com>, 2019. #zanata, 2020.
+# Matej Marusak <marusak.matej@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-11-13 13:37+0000\n"
+"Last-Translator: Jose Riha <jose1711@gmail.com>\n"
+"Language-Team: Slovak <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/sk/>\n"
+"Language: sk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=(n==1) ? 0 : (n>=2 && n<=4) ? 1 : 2\n"
+"X-Generator: Weblate 5.1.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 kontajner"
+msgstr[1] "$0 kontajnery"
+msgstr[2] "$0 kontajnerov"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 obrazov dohromady, $1"
+msgstr[1] "$0 obrazy dohromady, $1"
+msgstr[2] "$0 obrazov dohromady, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] ""
+msgstr[1] ""
+msgstr[2] ""
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 nepoužitých obrazov, $1"
+msgstr[1] "$0 nepoužité obrazy, $1"
+msgstr[2] "$0 nepoužitých obrazov, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Pridať mapovanie portov"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Pridať premennú"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Pridať zväzok"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Všetky"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Všetky registre"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Vždy"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Vyskytla sa chyba"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Autor"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Spúšťať podman pri zavádzaní systému"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+#, fuzzy
+#| msgid "IP address"
+msgid "CPU Shares help"
+msgstr "IP adresa"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU podiely"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Zrušiť"
+
+#: src/Containers.jsx:315
+#, fuzzy
+#| msgid "Checkpoint"
+msgid "Checking health"
+msgstr "Vytvoriť kontrolný bod"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Vytvoriť kontrolný bod"
+
+#: src/ImageRunModal.jsx:775
+#, fuzzy
+#| msgid "Checkpoint container $0"
+msgid "Checkpoint and restore support"
+msgstr "Vytvoriť kontrolný bod pre kontajner $0"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Vytvoriť kontrolný bod pre kontajner $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr ""
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr ""
+
+#: org.cockpit-project.podman.metainfo.xml:6
+#, fuzzy
+#| msgid "Podman containers"
+msgid "Cockpit component for Podman containers"
+msgstr "Podman kontajnery"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Príkaz"
+
+#: src/ImageHistory.jsx:33
+#, fuzzy
+#| msgid "Commit"
+msgid "Comments"
+msgstr "Vytvoriť"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Vytvoriť"
+
+#: src/ContainerCommitModal.jsx:136
+#, fuzzy
+#| msgid "No containers"
+msgid "Commit container"
+msgstr "Žiadne kontajnery"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Nakonfigurovaný"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konzola"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Kontajner"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Kontajner sa nepodarilo vytvoriť"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Kontajner sa nepodarilo spustiť"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Kontajner nebeží"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Názov kontajneru"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+#, fuzzy
+#| msgid "Container name"
+msgid "Container name is required."
+msgstr "Názov kontajneru"
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Cesta v kontajneri"
+
+#: src/Volume.jsx:23
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container path must not be empty"
+msgstr "Kontajner sa nepodarilo vytvoriť"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Port kontajneru"
+
+#: src/PublishPort.jsx:37
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container port must not be empty"
+msgstr "Kontajner sa nepodarilo vytvoriť"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Kontajnery"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Vytvoriť"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Vytvoriť a spustiť"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Vytvoriť kontajner"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Vytvoriť kontajner v $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Vytvoriť kontajner v skupine"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+#, fuzzy
+#| msgid "Created"
+msgid "Create pod"
+msgstr "Vytvorený"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Vytvorený"
+
+#: src/ImageHistory.jsx:33
+#, fuzzy
+#| msgid "Created"
+msgid "Created by"
+msgstr "Vytvorený"
+
+#: src/ImageRunModal.jsx:939
+#, fuzzy
+#| msgid "CPU shares"
+msgid "Decrease CPU shares"
+msgstr "CPU podiely"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr ""
+
+#: src/ImageRunModal.jsx:978
+#, fuzzy
+#| msgid "Maximum retries"
+msgid "Decrease maximum retries"
+msgstr "Maximum pokusov"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1099
+#, fuzzy
+#| msgid "Start podman"
+msgid "Decrease start period"
+msgstr "Spustiť podman"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr ""
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Odstrániť"
+
+#: src/ImageDeleteModal.jsx:92
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Delete $0 image?"
+msgstr "Odstrániť $0"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Delete $0?"
+msgstr "Odstrániť $0"
+
+#: src/ImageDeleteModal.jsx:96
+#, fuzzy
+#| msgid "Delete tagged images"
+msgid "Delete image"
+msgstr "Zmazať označené obrazy"
+
+#: src/PodActions.jsx:41
+#, fuzzy
+#| msgid "Delete $0"
+msgid "Delete pod $0?"
+msgstr "Odstrániť $0"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Zmazať označené obrazy"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Zmazať nepoužívané systémové obrazy:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Zmazať nepoužívané užívateľské obrazy:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Zmazaním kontajneru sa zmažú všetky dáta v ňom."
+
+#: src/Containers.jsx:71
+#, fuzzy
+#| msgid "Deleting a container will erase all data in it."
+msgid "Deleting a running container will erase all data in it."
+msgstr "Zmazaním kontajneru sa zmažú všetky dáta v ňom."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Zmazanie tohto podu zmaže nasledujúce kontajnery:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Podrobnosti"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr ""
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Stiahnuť"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Stiahnuť nový obraz"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr ""
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Vstupný bod"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Premenné prostredia"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Chyba"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Chybová správa"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr ""
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Príklad, Vaše Meno <vasemeno@priklad.sk>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Príklad: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Skončený"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr ""
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Nepodarilo sa vytvoriť kontrolný bod pre kontajner $0"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Nepodarilo sa zmazať kontajner"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr ""
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Nepodarilo sa vytvoriť kontajner $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Nepodarilo sa stiahnuť obraz $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Nepodarilo sa vynútiť zmazanie kontajneru $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Nepodarilo sa vynútiť zmazanie obrazu $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Nepodarilo sa nútene reštartovať pod $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Nepodarilo sa vynútiť zmazanie podu $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Nepodarilo sa zastaviť kontajner $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Nepodarilo sa zastaviť pod $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Nepodarilo sa vyčistiť nepoužívané kontajnery"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Nepodarilo sa vyčistiť nepoužívané obrazy"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Nepodarilo sa stiahnuť obraz $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Nepodarilo sa zmazať kontajner $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Nepodarilo sa zmazať obraz $0"
+
+#: src/ContainerRenameModal.jsx:54
+#, fuzzy
+#| msgid "Failed to resume container $0"
+msgid "Failed to rename container $0"
+msgstr "Nepodarilo sa spustiť kontajner $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Nepodarilo sa reštartovať kontajner $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Nepodarilo sa reštartovať pod $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Nepodarilo sa obnoviť kontajner $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Nepodarilo sa spustiť kontajner $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Nepodarilo sa znovuspustiť pod $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Nepodarilo sa spustiť kontajner $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+#, fuzzy
+#| msgid "Failed to checkpoint container $0"
+msgid "Failed to run health check on container $0"
+msgstr "Nepodarilo sa vytvoriť kontrolný bod pre kontajner $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+#, fuzzy
+#| msgid "Failed to search for images: $0"
+msgid "Failed to search for images."
+msgstr "Nepodarilo sa nájsť obrazy: $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Nepodarilo sa nájsť obrazy: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Nepodarilo sa nájsť nové obrazy"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Nepodarilo sa spustiť kontajner $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Nepodarilo sa spustiť pod $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Nepodarilo sa zastaviť kontajner $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Nepodarilo sa zastaviť pod $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr ""
+
+#: src/ContainerCommitModal.jsx:151
+#, fuzzy
+#| msgid "Force stop"
+msgid "Force commit"
+msgstr "Vynútiť zastavenie"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Vynútiť odstránenie"
+
+#: src/PodActions.jsx:40
+#, fuzzy
+#| msgid "Force delete"
+msgid "Force delete pod $0?"
+msgstr "Vynútiť odstránenie"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Vynútiť reštartovanie"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Vynútiť zastavenie"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Brána"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr ""
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr ""
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Skryť obrazy"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Skryť dočasné obrazy"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr ""
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Cesta na hostiteľovi"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Port na hostiteľovi"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Nápoveda k portu na hostiteľovi"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP adresa"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Nápoveda k IP adrese"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr ""
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr ""
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ignorovať staticky nastavené IP adresy"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ignorovať staticky nastavené MAC adresy"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Obraz"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr ""
+
+#: src/ContainerCommitModal.jsx:35
+#, fuzzy
+#| msgid "Container name"
+msgid "Image name is required"
+msgstr "Názov kontajneru"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Nápoveda k výberu obrazu"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Obrazy"
+
+#: src/ImageRunModal.jsx:940
+#, fuzzy
+#| msgid "CPU shares"
+msgid "Increase CPU shares"
+msgstr "CPU podiely"
+
+#: src/ImageRunModal.jsx:1050
+#, fuzzy
+#| msgid "Integration"
+msgid "Increase interval"
+msgstr "Integrácia"
+
+#: src/ImageRunModal.jsx:979
+#, fuzzy
+#| msgid "Maximum retries"
+msgid "Increase maximum retries"
+msgstr "Maximum pokusov"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1100
+#, fuzzy
+#| msgid "Start podman"
+msgid "Increase start period"
+msgstr "Spustiť podman"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr ""
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integrácia"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+#, fuzzy
+#| msgid "Integration"
+msgid "Interval"
+msgstr "Integrácia"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr ""
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr ""
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Kľúč"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr ""
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr ""
+
+#: src/ContainerDetails.jsx:71
+#, fuzzy
+#| msgid "Checkpoint"
+msgid "Latest checkpoint"
+msgstr "Vytvoriť kontrolný bod"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr ""
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+#, fuzzy
+#| msgid "Loading logs..."
+msgid "Loading details..."
+msgstr "Načítanie záznamov udalostí..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Načítanie záznamov udalostí..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Načítanie..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Miestne"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Miestne obrazy"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Záznamy udalostí"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC adresa"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Maximum pokusov"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Pamäť"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Pamäťový limit"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Pamäťová jednotka"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Mód"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr ""
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Názov"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr ""
+
+#: src/ContainerRenameModal.jsx:68
+#, fuzzy
+#| msgid "Container name"
+msgid "New container name"
+msgstr "Názov kontajneru"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Názov nového obrazu"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Nie"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr ""
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Žiadne kontajnery"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Tento obraz nepoužívajú žiadne kontajnery"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "V tomto pode nie sú žiadne kontajnery"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Žiadne kontajneri nevyhovujú danému filtru"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Neboli definované žiadne premenné prostredia"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Žiadne obrazy"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Žiadne obrazy neboli nájdené"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Žiadne obrazy nevyhovujú danému filtru"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr ""
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Žiaden výsledok pre $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Žiadne spustené kontajnery"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Žiadne špecifikované zväzky"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Pri chybe"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Iba bežiace"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Možnosti"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Vlastník"
+
+#: src/ImageRunModal.jsx:761
+#, fuzzy
+#| msgid "Owner"
+msgid "Owner help"
+msgstr "Vlastník"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Pozastaviť"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Zastaviť kontajner keď sa vytvára obraz"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Pozastavený"
+
+#: src/PodCreateModal.jsx:89
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Pod failed to be created"
+msgstr "Kontajner sa nepodarilo vytvoriť"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr ""
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman kontajnery"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Služba Podman nie je aktívna"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Mapovanie portov"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Porty"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr ""
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Súkromný"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokol"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Vyčistiť"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Vyčistiť nepoužívané kontajnery"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Vyčistiť nepoužívané obrazy"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+#, fuzzy
+#| msgid "No running containers"
+msgid "Pruning containers"
+msgstr "Žiadne spustené kontajnery"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Prerezávanie obrazov"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Stiahnúť najnovší obraz"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Sťahuje sa"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr ""
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr ""
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Odstrániť položku"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Odstráni vybrané nespustené kontajnery"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Odstraňuje sa"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr ""
+
+#: src/ContainerRenameModal.jsx:85
+#, fuzzy
+#| msgid "Restore container $0"
+msgid "Rename container $0"
+msgstr "Obnovenie kontajneru $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr ""
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Reštartovať"
+
+#: src/ImageRunModal.jsx:948
+#, fuzzy
+#| msgid "Restart"
+msgid "Restart policy"
+msgstr "Reštartovať"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+#, fuzzy
+#| msgid "Host path"
+msgid "Restart policy help"
+msgstr "Cesta na hostiteľovi"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr ""
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Obnoviť"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Obnovenie kontajneru $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr ""
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr ""
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:190
+#, fuzzy
+#| msgid "Please retry another term."
+msgid "Retry another term."
+msgstr "Skúste iný pojem."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr ""
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr ""
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Hľadať podľa mena alebo popisu"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Hľadanie obrazu"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Hľadá sa..."
+
+#: src/ImageRunModal.jsx:822
+#, fuzzy
+#| msgid "Loading..."
+msgid "Searching: $0"
+msgstr "Načítanie..."
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Zdielaný"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr ""
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Zobraziť obrazy"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr ""
+
+#: src/ContainerIntegration.jsx:82
+#, fuzzy
+#| msgid "Show images"
+msgid "Show less"
+msgstr "Zobraziť obrazy"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Zobraziť viac"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Veľkosť"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Spustiť"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+#, fuzzy
+#| msgid "Start podman"
+msgid "Start period"
+msgstr "Spustiť podman"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Spustiť podman"
+
+#: src/ImageSearchModal.jsx:185
+#, fuzzy
+#| msgid "Please start typing to look for images."
+msgid "Start typing to look for images."
+msgstr "Začnite písať na vyhľadanie obrazov."
+
+#: src/ContainerHealthLogs.jsx:105
+#, fuzzy
+#| msgid "Start"
+msgid "Started at"
+msgstr "Spustiť"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Stav"
+
+#: src/ContainerHealthLogs.jsx:56
+#, fuzzy
+#| msgid "State"
+msgid "Status"
+msgstr "Stav"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Zastaviť"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Zastavený"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr ""
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Systém"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Systémový podman je tiež dostupný"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Tag"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Tagy"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr ""
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Riešiť problém"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr ""
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+#, fuzzy
+#| msgid "Failed to download image $0:$1"
+msgid "Unable to load image history"
+msgstr "Nepodarilo sa stiahnuť obraz $0:$1"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr ""
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Aktívny od $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr ""
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Využívaný s"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr ""
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Užívateľský podman je tiež dostupný"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Používateľ:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Hodnota"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Zväzky"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+#, fuzzy
+#| msgid "Checkpoint"
+msgid "When unhealthy"
+msgstr "Vytvoriť kontrolný bod"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "S terminálom"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr ""
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "kontajner"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "sťahuje sa"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr ""
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "obraz"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr ""
+
+#: src/ImageDeleteModal.jsx:79
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate"
+msgstr "Skryť dočasné obrazy"
+
+#: src/ImageDeleteModal.jsx:59
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate image"
+msgstr "Skryť dočasné obrazy"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "Nedostupné"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "nedostupné"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr ""
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+#, fuzzy
+#| msgid "Ports"
+msgid "ports"
+msgstr "Porty"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr ""
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "systém"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "nepoužitý"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "používateľ:"
+
+#: src/Containers.jsx:585
+#, fuzzy
+#| msgid "Volumes"
+msgid "volumes"
+msgstr "Zväzky"
+
+#~ msgid "Delete $0"
+#~ msgstr "Odstrániť $0"
+
+#, fuzzy
+#~| msgid "Restart"
+#~ msgid "Restarting"
+#~ msgstr "Reštartovať"
+
+#, fuzzy
+#~| msgid "Please confirm deletion of $0"
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Potvrďte zmazanie $0"
+
+#, fuzzy
+#~| msgid "Please confirm deletion of pod $0"
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Potvrďte zmazanie podu $0"
+
+#, fuzzy
+#~| msgid "Please confirm force deletion of pod $0"
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Potvrďte vynútené zmazanie podu $0"
+
+#, fuzzy
+#~| msgid "Please confirm forced deletion of $0"
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Potvrďte nútené zmazanie $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kontajner momentálne beží."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Do exportu nezahŕnať zmeny v koreňovom súborovovom systéme"
+
+#, fuzzy
+#~| msgid "Delete tagged images"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Zmazať označené obrazy"
+
+#~ msgid "created"
+#~ msgstr "vytvorený"
+
+#~ msgid "exited"
+#~ msgstr "skončený"
+
+#~ msgid "paused"
+#~ msgstr "pozastavený"
+
+#, fuzzy
+#~| msgid "user"
+#~ msgid "user"
+#~ msgstr "používateľ"
+
+#~ msgid "Commit image"
+#~ msgstr "Vytvoriť obraz"
+
+#~ msgid "Format"
+#~ msgstr "Formát"
+
+#~ msgid "Message"
+#~ msgstr "Správa"
+
+#~ msgid "Pause the container"
+#~ msgstr "Pozastaviť kontajner"
+
+#~ msgid "Add item"
+#~ msgstr "Pridať položku"
+
+#, fuzzy
+#~| msgid "Host port"
+#~ msgid "Host port (optional)"
+#~ msgstr "Port na hostiteľovi"
+
+#~ msgid "IP prefix length"
+#~ msgstr "Dĺžka predpony IP adresy"
+
+#~ msgid "Run"
+#~ msgstr "Spustiť"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Ste si istý/istá, že chcete odstrániť tento obraz?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Nepodarilo sa pripojiť k tomuto kontajneru: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Nepodarilo sa otvoriť kanál: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Všetko"
+
+#~ msgid "Security"
+#~ msgstr "Bezpečnosť"
diff --git a/po/sv.po b/po/sv.po
new file mode 100644
index 0000000..e72a8f9
--- /dev/null
+++ b/po/sv.po
@@ -0,0 +1,1485 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Gleb Vassiljev <gleb@netkom.se>, 2020.
+# Göran Uddeborg <goeran@uddeborg.se>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-11-29 13:31+0000\n"
+"Last-Translator: Luna Jernberg <bittin@reimu.nl>\n"
+"Language-Team: Swedish <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/sv/>\n"
+"Language: sv\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 behållare"
+msgstr[1] "$0 behållare"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 avbild, total, $1"
+msgstr[1] "$0 avbilder, totalt, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 sekund"
+msgstr[1] "$0 sekunder"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 oanvänd avbild, $1"
+msgstr[1] "$0 oanvända avbilder, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 till 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "Åtgärd att ta när behållaren övergår till ett ohälsosamt tillstånd."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Lägg till portmappning"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Lägg till variabel"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Lägg till volym"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Alla"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Alla register"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Alltid"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Ett fel uppstod"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Upphovsman"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Kör igång podman automatiskt vid uppstart"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU-andelar hjälp"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU-andelar"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU-andelar avgör prioriteten på körande behållare. Standardprioriteten är "
+"1024. Ett högre tal prioriterar denna behållare. Ett lägre tal sänker "
+"prioriteten."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Avbryt"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Kontrollerar hälsa"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Checkpunkt"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "checkpunkt och återställnings stöd"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Ta checkpunkt på behållare $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Klicka för att se publicerade portar"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Klicka för att se volymer"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Cockpit komponent för Podman-behållare"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Kommando"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Kommentarer"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Fastställ"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Commit behållare"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Konfigurerad"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konsol"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Behållare"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Behållaren misslyckades att skapas"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Behållaren misslyckades att startas"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Behållaren kör inte"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Behållarnamn"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Behållarnamn krävs."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Behållarsökväg"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "Behållarens sökväg får inte vara tom"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Behållarport"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "Behållarporten får inte vara tom"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Behållare"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Skapa"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Skapa en ny avbild baserad på det aktuella läget för $0 behållaren."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Skapa och kör"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Skapa behållare"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Skapa behållare i $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Skapa behållare i kapsel"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Skapa pod"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Skapad"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Skapad av"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Minska CPU-andelar"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Minska intervallet"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Minska maximalt antal försök igen"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Minska minne"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Minska återförsök"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Minska startintervallet"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Minska timeout"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Radera"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "Radera $0 avbild?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Radera $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Radera avbild"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Radera podd $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Radera taggade avbilder"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Ta bort oanvända systemavbilder:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Ta bort oanvända användaravbilder:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "När en behållare raderas försvinner även all data i den."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "När en behållare som körs raderas försvinner även all data i den."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Att ta bort denna kapsel kommer ta bort följande behållare:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Detaljer"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Disk utrymme"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Docker-format är användbart när du delar avbilden med Docker eller Moby "
+"Engine"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Ladda ner"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Ladda ner ny avbild"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Tom podd $0 kommer att tas bort permanent."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Ingångspunkt"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Miljövariabler"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Fel"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Felmeddelande"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Ett fel uppstod vid anslutning till konsolen"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Exempel, Ditt Namn <dittnamn@exempel.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Exempel: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Avslutats"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Misslyckad hälsokörning"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Kunde inte ta checkpunkt av behållare $0"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Misslyckades med att rensa upp behållaren"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Kunde inte fastställa behållaren $0"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Misslyckades att skapa behållaren $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Kunde inte ladda ner avbild $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Misslyckades att tvingande ta bort behållare $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Misslyckades att tvingande ta bort avbilden $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Misslyckades med tvingad omstart av kapseln $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Misslyckades att tvinga stopp av kapsel $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Misslyckades med att pausa behållaren $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Misslyckades att pausa kapseln $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Misslyckades att rensa oanvända behållare"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Misslyckades att rensa oanvända avbilder"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Misslyckades med att hämta avbilden $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Misslyckades att ta bort behållaren $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Misslyckades att ta bort avbilden $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Misslyckades att byta namn på behållaren $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Misslyckades att starta om behållaren $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Misslyckades att starta om kapseln $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Misslyckades att återställa behållaren $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Misslyckades att återuppta behållaren $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Misslyckades att återuppta kapseln $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Misslyckades att köra behållaren $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Misslyckades att köra hälsokontrollen på behållare $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Misslyckades att söka efter avbilder."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Misslyckades att söka efter avbilder: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Misslyckades att söka efter nya avbilder"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Misslyckades att starta behållaren $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Misslyckades att starta kapseln $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Misslyckades att stoppa behållaren $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Misslyckades att stoppa kapseln $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Misslyckanden i följd"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Framtvinga commit"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Framtvinga borttagande"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Framtvinga borttagande av podd $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Framtvinga omstart"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Framtvinga stopp"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Gateway"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Hälsokontroll"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Hälsokontrollens intervallhjälp"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Hälsokontrollens återförsökshjälp"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Hälsokontrollens startintervallshjälp"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Hälsokontrollens tidsgränshjälp"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Hälsokontrollens checkåtgärd hjälp"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Hälsosam"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Dölj avbilder"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Dölj mellanliggande avbilder"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Historik"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Värdsökväg"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Värdport"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Värdport hjälp"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP-adress"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "IP-adress hjälp"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Ideal för utveckling"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Ideal för att köra tjänster"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Om värd-IP är inställd på 0.0.0.0 eller inte alls, kommer porten att bindas "
+"till alla IP-adresser på värden."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Om värdporten inte är inställd kommer behållareporten att slumpmässigt "
+"tilldelas en port på värden."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ignorera IP-adressen om den är statiskt satt"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ignorera MAC-adressen om den är statiskt satt"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Avbild"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Avbildsnamn är inte unikt"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Avbildsnamn krävs"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Avbildsval hjälp"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Avbilder"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Öka CPU-andelar"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Öka intervallet"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Öka maximalt antal försök igen"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Öka minne"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Öka återförsöken"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Öka startintervallet"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Öka timeout"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Integration"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Intervall"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Intervallet hur ofta hälsokontroller körs."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Ogiltiga tecken. Namn kan bara innehålla bokstäver, siffror och vissa "
+"skiljetecken (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Behåll alla tillfälliga checkpunktsfiler"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Nyckel"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "Nyckel får inte vara tom"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Senaste 5 körningarna"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Senaste checkpunkt"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Låt fortsätta köra efter att skriva checkpunkt till disk"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Läser in detaljer …"
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Läser in loggar …"
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Läser in …"
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Lokal"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Lokala avbilder"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Loggar"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC-adress"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Maximalt antal försök igen"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Minne"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Minnesgräns"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Minnesenhet"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Läge"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Flera taggar finns för denna avbild. Välj vilka taggade avbilder som skall "
+"raderas."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "Måste vara en giltig IP adress"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Namn"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Namn används redan"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Nytt behållarnamn"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Nytt Avbildsnamn"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Nej"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Ingen åtgärd"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Inga behållare"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Inga behållare använder denna avbild"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Inga behållare i denna kapsel"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Inga behållare som stämmer med det aktuella filtret"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Inga miljövariabler specificerade"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Inga avbilder"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Inga avbilder funna"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Inga avbilder som stämmer med det aktuella filtret"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Ingen Etikett"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Inga portar exponerade"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Inga resultat för $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Inga körande behållare"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Inga volymer specificerade"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Vid misslyckande"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Endast körande"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Alternativ"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Ägare"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Ägare hjälp"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Lyckad hälsokontrollskörning"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Klistra in en eller flera rader med nyckel=värdepar i valfritt fält för "
+"massimport"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Pausa"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Pausa behållaren när du skapar en avbild"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Pausad"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Podden kunde inte skapas"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Pod namn"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman-behållare"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman-tjänsten är inte aktiv"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Portkartering"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Portar"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Portar under 1024 kan inte mappas"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Privat"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokoll"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Rensa"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Rensa oanvända behållare"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Rensa oanvända avbilder"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Rensar behållare"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Rensar avbilder"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Hämta senaste avbild"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Hämtar"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Skrivskyddad åtkomst"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Läs och skrivåtkomst"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Ta bort post"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Tar bort valda icke-körande behållare"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Tar bort"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Byt namn"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Byt namn på behållaren $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Resursgränser kan ställas in"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Starta om"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Starta om policy"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Starta om policy hjälp"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Starta om policy att följa när behållare avslutas."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Starta om policyn till att följa när behållare avslutar. Att använda "
+"fortlevande (linger) för automatstartande behållare fungerar kanske inte i "
+"vissa situationer, som när ecryptfs, systemd-homed, NFS eller 2FA används "
+"för ett användarkonto."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Återställ"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Återställ behållaren $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Återställ med etablerade TCP-förbindelser"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Begränsat av användarkontobehörigheter"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Återuppta"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Omförsök"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Försök igen med en annan term."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Kör hälsokontroll"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Kör"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Sök efter namn eller beskrivning"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Sök efter register"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Sök efter"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Sök efter en avbild"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Söksträng eller behållareplats"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Söker …"
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Söker: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Delad"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Visa"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Visa avbilder"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Visa mellanliggande avbilder"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Visa mindre"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Visa mer"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Storlek"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Starta"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Startperiod"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Starta podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Börja skriva för att leta efter avbilder."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Startad vid"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Tillstånd"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Status"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Stoppa"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Stoppad"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Stöd att bevara etablerade TCP-förbindelser"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "System"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "System-podman-tjänsten är också tillgänglig"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Tagg"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Taggar"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Cockpit-användargränssnittet för Podman-behållare."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Initieringstiden sombehövs för att en behållare skall komma igång."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Den maximala tiden som tillåts för att klara av hälsokontrollen före ett "
+"intervall betraktas som misslyckat."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Antalet återförsök som tillåts före en hälsokontroll betraktas som ohälsosam."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Tidsgräns"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Felsök"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Skriv för att filtrera …"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Kunde inte ladda avbildshistoriken"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Ohälsosam"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Uppe sedan $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Använd gammalt Docker-format"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Används av"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Användare"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Användar-podman-tjänsten är också tillgänglig"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Användare:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Värde"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Volymer"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "När ohälsosam"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Med terminal"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Skrivbar"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "behållare"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "hämtar"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "värd[:port]/[användare]/behållare[:tagg]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "avbild"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "i"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "mellanliggande"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "mellanliggande avbild"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "ej tillämpligt"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "inte tillgängligt"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "kapselgrupp"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "portar"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "sekunder"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "system"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "oanvänd"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "användare:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "volymer"
+
+#~ msgid "Delete $0"
+#~ msgstr "Radera $0"
+
+#~ msgid "select all"
+#~ msgstr "välj alla"
+
+#~ msgid "Failure action"
+#~ msgstr "Misslyckad åtgärd"
+
+#~ msgid "Restarting"
+#~ msgstr "Startar om"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Bekräfta borttagning av $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Bekräfta borttagning av kapsel $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Bekräfta tvångsradering av kapsel $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Bekräfta tvångsradering av $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Behållaren kör för närvarande."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Inkludera inte rotfilsystemsändringar vid export"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Standard med enkel valbar"
+
+#~ msgid "Start after creation"
+#~ msgstr "Starta efter skapande"
+
+#, fuzzy
+#~| msgid "Delete tagged images"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Radera taggade avbilder"
+
+#~ msgid "created"
+#~ msgstr "skapad"
+
+#~ msgid "exited"
+#~ msgstr "avslutad"
+
+#~ msgid "paused"
+#~ msgstr "pausad"
+
+#~ msgid "running"
+#~ msgstr "kör"
+
+#~ msgid "stopped"
+#~ msgstr "stoppad"
+
+#~ msgid "user"
+#~ msgstr "användare"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Lägg till byggvariabel"
+
+#~ msgid "Commit image"
+#~ msgstr "Fastställningsavbild"
+
+#~ msgid "Format"
+#~ msgstr "Format"
+
+#~ msgid "Message"
+#~ msgstr "Meddelande"
+
+#~ msgid "Pause the container"
+#~ msgstr "Pausa behållaren"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Ta bort byggvariabel"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Sätt behållare på byggvariabler"
+
+#~ msgid "Add item"
+#~ msgstr "Lägg till post"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Värdport (valfri)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (valfri)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "LäsEndast"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP-prefixlängd"
+
+#~ msgid "Get new image"
+#~ msgstr "Hämta ny avbild"
+
+#~ msgid "Run"
+#~ msgstr "Kör"
+
+#~ msgid "On build"
+#~ msgstr "Vid bygge"
+
+#~ msgid "Everything"
+#~ msgstr "Allting"
diff --git a/po/tr.po b/po/tr.po
new file mode 100644
index 0000000..d9ea1ee
--- /dev/null
+++ b/po/tr.po
@@ -0,0 +1,1502 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Oğuz Ersen <oguzersen@protonmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-12-01 18:43+0000\n"
+"Last-Translator: Weblate Translation Memory <noreply-mt-weblate-translation-"
+"memory@weblate.org>\n"
+"Language-Team: Turkish <https://translate.fedoraproject.org/projects/cockpit-"
+"podman/main/tr/>\n"
+"Language: tr\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=2; plural=n != 1\n"
+"X-Generator: Weblate 5.2.1\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 kapsayıcı"
+msgstr[1] "$0 kapsayıcı"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "Toplam $0 kalıp, $1"
+msgstr[1] "Toplam $0 kalıp, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 saniye"
+msgstr[1] "$0 saniye"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 kullanılmayan kalıp, $1"
+msgstr[1] "$0 kullanılmayan kalıp, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1'den 65535'e"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr ""
+"Kapsayıcı sağlıksız bir duruma geçtikten sonra gerçekleştirilecek eylem."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Bağlantı noktası eşlemesi ekle"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Değişken ekle"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Birim ekle"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Tümü"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Tüm kayıtlar"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Her zaman"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Bir hata meydana geldi"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Hazırlayan"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Podman'ı önyüklemede otomatik olarak başlat"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU Paylaşımları yardımı"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU paylaşımları"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU paylaşımları, kapsayıcıları çalıştırmanın önceliğini belirler. "
+"Varsayılan öncelik 1024'dür. Daha yüksek bir sayı bu kapsayıcıya öncelik "
+"verir. Daha düşük bir sayı önceliği azaltır."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "İptal"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Sağlık denetleniyor"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Denetim noktası"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Denetim noktası ve geri yükleme desteği"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "$0 kapsayıcısı denetim noktası"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Yayınlanan bağlantı noktalarını görmek için tıklayın"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Birimleri görmek için tıklayın"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Podman kapsayıcıları için Cockpit bileşeni"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Komut"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Açıklamalar"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "İşle"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Kapsayıcı işle"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Yapılandırıldı"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Konsol"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Kapsayıcı"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Kapsayıcının oluşturulması başarısız oldu"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Kapsayıcının başlatılması başarısız oldu"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Kapsayıcı çalışmıyor"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Kapsayıcı adı"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Kapsayıcı adı gerekli."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Kapsayıcı yolu"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "Kapsayıcı yolu boş olmamak zorundadır"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Kapsayıcı bağlantı noktası"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "Kapsayıcı bağlantı noktası boş olmamak zorundadır"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Kapsayıcılar"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Oluştur"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "$0 kapsayıcısının şu anki durumuna göre yeni bir kalıp oluşturun."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Oluştur ve çalıştır"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Kapsayıcı oluştur"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "$0 içinde kapsayıcı oluştur"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Bölme içinde kapsayıcı oluştur"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Bölme oluştur"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Oluşturuldu"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Oluşturan"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "CPU paylaşımlarını azalt"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Aralığı azalt"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "En fazla yeniden denemeyi azalt"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Belleği azalt"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Yeniden denemeleri azalt"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Başlangıç süresini azalt"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Zaman aşımını azalt"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Sil"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "$0 kalıbı silinsin mi?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "$0 silinsin mi?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "Kalıbı sil"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "$0 bölmesi silinsin mi?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Etiketli kalıpları sil"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Kullanılmayan sistem kalıplarını sil:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Kullanılmayan kullanıcı kalıplarını sil:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "Bir kapsayıcıyı silmek içindeki tüm verileri silecek."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "Çalışan bir kapsayıcıyı silmek içindeki tüm verileri silecek."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Bu bölmeyi silmek aşağıdaki kapsayıcıları kaldıracak:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Ayrıntılar"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Disk alanı"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Docker biçimi, kalıbı Docker veya Moby Engine ile paylaşırken kullanışlıdır"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "İndir"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Yeni kalıbı indir"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Boş bölme $0 kalıcı olarak kaldırılacaktır."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Giriş noktası"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Ortam değişkenleri"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Hata"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Hata iletisi"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Konsola bağlanırken hata meydana geldi"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Örnek, Adınız <adiniz@ornek.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Örnek: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Çıkıldı"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Sağlık işlemini çalıştırma başarısız oldu"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "$0 kapsayıcısını denetleme noktası başarısız oldu"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Kapsayıcıyı temizleme başarısız oldu"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "$0 kapsayıcısını işleme başarısız oldu"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "$0 kapsayıcısını oluşturma başarısız oldu"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "$0:$1 kalıbını indirme başarısız oldu"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "$0 kapsayıcısını zorla kaldırma başarısız oldu"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "$0 kalıbını zorla kaldırma başarısız oldu"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "$0 bölmesini yeniden başlatmaya zorlama başarısız oldu"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "$0 bölmesini zorla durdurma başarısız oldu"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "$0 kapsayıcısını duraklatma başarısız oldu"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "$0 bölmesini duraklatma başarısız oldu"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Kullanılmayan kapsayıcıları ayıklama başarısız oldu"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Kullanılmayan kalıpları ayıklama başarısız oldu"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "$0 kalıbını çekme başarısız oldu"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "$0 kapsayıcısını kaldırma başarısız oldu"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "$0 kalıbını kaldırma başarısız oldu"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "$0 kapsayıcısını yeniden adlandırma başarısız oldu"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "$0 kapsayıcısını yeniden başlatma başarısız oldu"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "$0 bölmesini yeniden başlatma başarısız oldu"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "$0 kapsayıcısını geri yükleme başarısız oldu"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "$0 kapsayıcısını sürdürme başarısız oldu"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "$0 bölmesini sürdürme başarısız oldu"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "$0 kapsayıcısını çalıştırma başarısız oldu"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "$0 kapsayıcısında sağlık denetimi çalıştırma başarısız oldu"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Kalıpları arama başarısız oldu."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Kalıpları arama başarısız oldu: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Yeni kalıpları arama başarısız oldu"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "$0 kapsayıcısını başlatma başarısız oldu"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "$0 bölmesini başlatma başarısız oldu"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "$0 kapsayıcısını durdurma başarısız oldu"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "$0 bölmesini durdurma başarısız oldu"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Başarısız olan seri"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "İşlemeye zorla"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Silmeye zorla"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "$0 bölmesi silmeye zorlansın mı?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Yeniden başlatmaya zorla"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Durdurmaya zorla"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Ağ geçidi"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Sağlık denetimi"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Sağlık denetimi aralığı yardımı"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Sağlık denetimi yeniden denemeleri yardımı"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Sağlık denetimi başlangıç dönemi yardımı"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Sağlık denetimi zaman aşımı yardımı"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Sağlık denetimi hatası eylem yardımı"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Sağlıklı"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Kalıpları gizle"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Ara kalıpları gizle"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Geçmiş"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Anamakine yolu"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Anamakine bağlantı noktası"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Anamakine b.noktası yardım"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "Kimlik"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP adresi"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "IP adresi yardım"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Geliştirme için ideal"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Çalışan hizmetler için ideal"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Eğer anamakine IP'si 0.0.0.0 olarak ayarlanırsa veya hiç ayarlanmazsa, "
+"bağlantı noktası anamakinedeki tüm IP'lere bağlanır."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Eğer anamakine bağlantı noktası ayarlanmazsa, kapsayıcı bağlantı noktası "
+"anamakinede rastgele bir bağlantı noktasına atanacaktır."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Sabit olarak ayarlanmışsa IP adresini yoksay"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Sabit olarak ayarlanmışsa MAC adresini yoksay"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Kalıp"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Kalıp adı benzersiz değil"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Kalıp adı gerekli"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Kalıp seçim yardımı"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Kalıplar"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "CPU paylaşımlarını artır"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Aralığı artır"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "En fazla yeniden denemeyi artır"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Belleği artır"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Yeniden denemeleri artır"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Başlangıç süresini artır"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Zaman aşımını artır"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Bütünleştirme"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Aralık"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Sağlık denetiminin ne sıklıkta çalıştırıldığı aralığı."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Geçersiz karakterler. Ad sadece harf, sayı ve belirli noktalama işaretlerini "
+"(_ . -) içerebilir."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Tüm geçici denetim noktası dosyalarını sakla"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Anahtar"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "Anahtar boş olmamak zorundadır"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Son 5 çalıştırma"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Son denetim noktası"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Denetim noktasını diske yazdıktan sonra çalışır durumda bırak"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Ayrıntılar yükleniyor..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Günlükler yükleniyor..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Yükleniyor..."
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Yerel"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Yerel kalıplar"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Günlükler"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC adresi"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "En fazla yeniden deneme"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Bellek"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Bellek sınırı"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Bellek birimi"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Mod"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Bu kalıp için birden fazla etiket var. Silinecek etiketlenmiş kalıpları "
+"seçin."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "Geçerli bir IP adresi olmak zorundadır"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Ad"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "Ad zaten kullanımda"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Yeni kapsayıcı adı"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Yeni kalıp adı"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Hayır"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Eylem yok"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Kapsayıcılar yok"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Bu kalıbı hiçbir kapsayıcı kullanmıyor"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "Bu bölmede kapsayıcılar yok"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Şu anki süzgeçle eşleşen kapsayıcılar yok"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Belirtilen ortam değişkenleri yok"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Kalıplar yok"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Bulunan kalıplar yok"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Şu anki süzgeçle eşleşen kalıplar yok"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Etiket yok"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Açığa çıkan bağlantı noktaları yok"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "$0 için sonuçlar yok"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Çalışan kapsayıcılar yok"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Belirtilen birimler yok"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "Başarısızlıkla"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Sadece çalışanlar"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Seçenekler"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Sahibi"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Sahibi yardımı"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Sağlık çalıştırması geçti"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Toplu içe aktarma için herhangi bir alana bir veya daha fazla anahtar=değer "
+"çifti satırı yapıştırın"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Duraklat"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Kalıp oluştururken kapsayıcıyı duraklatın"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Duraklatıldı"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Bölmenin oluşturulması başarısız oldu"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Bölme adı"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman kapsayıcıları"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman hizmeti etkin değil"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Bağlantı noktası eşleme"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Bağlantı noktaları"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "1024 altındaki bağlantı noktaları eşlenebilir"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Özel"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Protokol"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Ayıkla"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Kullanılmayan kapsayıcıları ayıkla"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Kullanılmayan kalıpları ayıkla"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Kapsayıcılar ayıklanıyor"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Kalıplar ayıklanıyor"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Son kalıbı çek"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Çekiliyor"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Salt-okunur erişim"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Okuma-yazma erişimi"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Öğeyi kaldır"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Seçilen çalışmayan kapsayıcıları kaldırır"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Kaldırılıyor"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Yeniden adlandır"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "$0 kapsayıcısını yeniden adlandır"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Kaynak sınırları ayarlanabilir"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Yeniden başlat"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Yeniden başlatma ilkesi"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Yeniden başlatma ilkesi yardımı"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Kapsayıcılardan çıkıldığında izlenecek ilkeyi yeniden başlatın."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Kapsayıcılar çıktığında izlenecek ilkeyi yeniden başlatın. Otomatik başlatma "
+"kapsayıcıları için geciktirme kullanılması, bir kullanıcı hesabında "
+"ecryptfs, systemd-homed, NFS veya 2FA kullanıldığında olduğu gibi bazı "
+"durumlarda çalışmayabilir."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Geri yükle"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "$0 kapsayıcısını geri yükle"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Kurulu TCP bağlantıları ile geri yükle"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Kullanıcı hesabı izinleri tarafından kısıtlanmış"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Sürdür"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Yeniden denemeler"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Başka bir terimi yeniden deneyin."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Sağlık denetimini çalıştır"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Çalışıyor"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Ada veya açıklamaya göre ara"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Kayıt defterine göre ara"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Ara"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Kalıp ara"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Arama dizgisi veya kapsayıcı konumu"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Aranıyor..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Aranan: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Paylaşılan"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Göster"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Kalıpları göster"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Ara kalıpları göster"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Daha az göster"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Daha fazlasını göster"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Boyut"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Başlat"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Başlangıç süresi"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Podman'ı başlat"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Kalıpları aramak için yazmaya başlayın."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Başlama zamanı"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Durum"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Durum"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Durdur"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Durduruldu"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Kurulu TCP bağlantılarını korumayı destekle"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Sistem"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Sistem Podman hizmeti de kullanılabilir"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Etiket"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Etiketler"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Podman kapsayıcıları için Cockpit kullanıcı arayüzü."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Bir kapsayıcının önyükleme yapması için gereken başlatma süresi."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Bir aralığın başarısız olduğu kabul edilmeden önce sağlık denetimini "
+"tamamlanması için izin verilen en fazla süre."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Bir sağlık denetiminden önce izin verilen yeniden deneme sayısı sağlıksız "
+"olarak kabul edilir."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Zaman aşımı"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Sorun Giderme"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Süzmek için yazın…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Kalıp geçmişini yükleme başarısız oldu"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Sağlıksız"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "$0'dan beri aktif"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Eski Docker biçimini kullan"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Kullanan"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Kullanıcı"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Kullanıcı Podman hizmeti de kullanılabilir"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Kullanıcı:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Değer"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Birimler"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Sağlıksız olduğunda"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "Terminal ile"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Yazılabilir"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "kapsayıcı"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "indiriliyor"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "anamakine[:b.noktası]/[kullanıcı]/kapsayıcı[:etiket]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "kalıp"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "şurada"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "orta seviye"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "orta seviye kalıp"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "yok"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "kullanılabilir değil"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "bölme grubu"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "bağlantı noktaları"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "saniye"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "sistem"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "kullanılmayan"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "kullanıcı:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "birimler"
+
+#~ msgid "Delete $0"
+#~ msgstr "$0 sil"
+
+#~ msgid "select all"
+#~ msgstr "tümünü seç"
+
+#~ msgid "Failure action"
+#~ msgstr "Başarısız eylem"
+
+#~ msgid "Restarting"
+#~ msgstr "Yeniden başlatılıyor"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "$0 için silme işlemini onaylayın"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "$0 bölmesinin silme işlemini onaylayın"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "$0 bölmesinin zorla silme işlemini onaylayın"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "$0 için zorla silme işlemini onaylayın"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Kapsayıcı şu anda çalışıyor."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Dışa aktarırken kök dosya sistemi değişikliklerini dahil etme"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Tek seçilebilir ile varsayılan"
+
+#~ msgid "Start after creation"
+#~ msgstr "Oluşturmadan sonra başlat"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Kullanılmayan $0 kalıplarını sil:"
+
+#~ msgid "created"
+#~ msgstr "oluşturuldu"
+
+#~ msgid "exited"
+#~ msgstr "çıkıldı"
+
+#~ msgid "paused"
+#~ msgstr "duraklatıldı"
+
+#~ msgid "running"
+#~ msgstr "çalışıyor"
+
+#~ msgid "stopped"
+#~ msgstr "durduruldu"
+
+#~ msgid "user"
+#~ msgstr "kullanıcı"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Yapım değişkeninde ekle"
+
+#~ msgid "Commit image"
+#~ msgstr "Kalıbı işle"
+
+#~ msgid "Format"
+#~ msgstr "Biçim"
+
+#~ msgid "Message"
+#~ msgstr "İleti"
+
+#~ msgid "Pause the container"
+#~ msgstr "Konteyneri duraklat"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Yapım değişkeninde kaldır"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Yapım değişkenlerinde kapsayıcıyı ayarla"
+
+#~ msgid "Add item"
+#~ msgstr "Öğe ekle"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Anamakine bağlantı noktası (isteğe bağlı)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP (isteğe bağlı)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "SadeceOku"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP ön ek uzunluğu"
+
+#~ msgid "Get new image"
+#~ msgstr "Yeni kalıp al"
+
+#~ msgid "Run"
+#~ msgstr "Çalıştır"
+
+#~ msgid "On build"
+#~ msgstr "Yapım anında"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Bu kalıbı silmek istediğinizden emin misiniz?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Bu konteynere iliştirilemedi: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Kanal açılamadı: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Her şey"
+
+#~ msgid "Security"
+#~ msgstr "Güvenlik"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "$time ($type) taraması hiçbir güvenlik açığı bulamadı."
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "Web Konsolu'nun bu sürümü bir terminali desteklememektedir."
diff --git a/po/uk.po b/po/uk.po
new file mode 100644
index 0000000..55ac29b
--- /dev/null
+++ b/po/uk.po
@@ -0,0 +1,1523 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Yuri Chornoivan <yurchor@ukr.net>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-08-07 10:58+0000\n"
+"Last-Translator: Yuri Chornoivan <yurchor@ukr.net>\n"
+"Language-Team: Ukrainian <https://translate.fedoraproject.org/projects/"
+"cockpit-podman/main/uk/>\n"
+"Language: uk\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=3; plural=n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
+"n%10<=4 && (n%100<10 || n%100>=20) ? 1 : 2;\n"
+"X-Generator: Weblate 4.18.2\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0 контейнер"
+msgstr[1] "$0 контейнери"
+msgstr[2] "$0 контейнерів"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "$0 образ загалом, $1"
+msgstr[1] "$0 образи загалом, $1"
+msgstr[2] "$0 образів загалом, $1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 секунда"
+msgstr[1] "$0 секунди"
+msgstr[2] "$0 секунд"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0 невикористаний образ, $1"
+msgstr[1] "$0 невикористані образи, $1"
+msgstr[2] "$0 невикористаних образів, $1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr ""
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "Дія, яку слід виконати після переходу контейнера у небезпечний стан."
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "Додати прив'язку портів"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "Додати змінну"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "Додати том"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "Усі"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "Усі реєстри"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "Завжди"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "Сталася помилка"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "Автор"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "Автоматично запускати podman під час завантаження"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "Процесор"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "Довідка щодо спільних процесорів"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "Спільне використання процесора"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"Спільні процесори визначають пріоритетність запуску контейнерів. Типовою "
+"пріоритетністю є 1024. Більші числа роблять контейнер пріоритетнішим. Менші "
+"числа зменшують пріоритетність."
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "Скасувати"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "Перевіряємо працездатність"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "Контрольна точка"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "Підтримка контрольних точок і відновлення"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "Контрольна точка контейнера $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "Натисніть, щоб переглянути оприлюднені порти"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "Натисніть, щоб переглянути томи"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Компонент Cockpit для контейнерів Podman"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "Команда"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "Коментарі"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "Надіслати"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "Внести контейнер"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "Налаштовано"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "Консоль"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "Контейнер"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "Не вдалося створити контейнер"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "Не вдалося запустити контейнер"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "Контейнер не запущено"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "Назва контейнера"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "Слід вказати назву контейнера."
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "Шлях до контейнера"
+
+#: src/Volume.jsx:23
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container path must not be empty"
+msgstr "Не вдалося створити контейнер"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "Порт контейнера"
+
+#: src/PublishPort.jsx:37
+#, fuzzy
+#| msgid "Container failed to be created"
+msgid "Container port must not be empty"
+msgstr "Не вдалося створити контейнер"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "Контейнери"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "Створити"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "Створити образ на основі поточного стану контейнера $0."
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "Створити і запустити"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "Створити контейнер"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "Створити контейнер у $0"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "Створити контейнер у стручку"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "Створити стручок"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "Створено"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "Створено"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "Зменшити спільне використання процесора"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "Зменшити інтервал"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "Зменшити максимум повторних спроб"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "Зменшити пам'ять"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "Зменшити кількість повторних спроб"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "Зменшити період запуску"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "Зменшити час очікування"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "Вилучити"
+
+#: src/ImageDeleteModal.jsx:92
+#, fuzzy
+#| msgid "Delete $0?"
+msgid "Delete $0 image?"
+msgstr "Вилучити $0?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "Вилучити $0?"
+
+#: src/ImageDeleteModal.jsx:96
+#, fuzzy
+#| msgid "Delete tagged images"
+msgid "Delete image"
+msgstr "Вилучити позначені міткою образи"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "Вилучити кокон $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "Вилучити позначені міткою образи"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "Вилучити невикористані образи системи:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "Вилучити невикористані образи користувача:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr ""
+"Вилучення контейнера призведе до витирання усіх даних, що на ньому "
+"зберігаються."
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr ""
+"Вилучення запущеного контейнера призведе до витирання усіх даних, що на "
+"ньому зберігаються."
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "Вилучення цього кокону призведе до вилучення таких контейнерів:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "Подробиці"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "Місце на диску"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr ""
+"Формат Docker корисний, якщо ви захочете використовувати образ спільно у "
+"середовищах Docker або Moby Engine"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "Отримати"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "Отримати новий образ"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "Порожній кокон $0 буде остаточно вилучено."
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "Точка входження"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "Змінні середовища"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "Помилка"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "Повідомлення про помилку"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "Під час спроби встановити з'єднання з консоллю сталася помилка"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "Приклад: Ваше Ім'я <vasheimya@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "Приклад: $0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "Вийшов"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "Не вдалося запустити перевірку працездатності"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "Не вдалося створити контрольну точку контейнера $0"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "Не вдалося очистити контейнер"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "Не вдалося надіслати на обробку контейнер $0"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "Не вдалося створити контейнер $0"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "Не вдалося отримати образ $0:$1"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "Не вдалося примусово вилучити контейнер $0"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "Не вдалося примусово вилучити образ $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "Не вдалося примусово перезапустити кокон $0"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "Не вдалося примусово зупинити кокон $0"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "Не вдалося призупинити роботу контейнера $0"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "Не вдалося призупинити кокон $0"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "Не вдалося позбутися невикористаних контейнерів"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "Не вдалося позбутися невикористаних образів"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "Не вдалося отримати образ $0"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "Не вдалося вилучити контейнер $0"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "Не вдалося вилучити образ $0"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "Не вдалося перейменувати контейнер $0"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "Не вдалося перезапустити контейнер $0"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "Не вдалося перезапустити кокон $0"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "Не вдалося відновити контейнер $0"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "Не вдалося відновити роботу контейнера $0"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "Не вдалося відновити кокон $0"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "Не вдалося запустити контейнер $0"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "Не вдалося запустити перевірку працездатності контейнера $0"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "Не вдалося виконати пошук образів."
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "Не вдалося виконати пошук образів: $0"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "Не вдалося виконати пошук нових образів"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "Не вдалося запустити роботу контейнера $0"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "Не вдалося запустити кокон $0"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "Не вдалося зупинити роботу контейнера $0"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "Не вдалося зупинити кокон $0"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "Помилкова смужка"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "Примусово внести"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "Примусове вилучення"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "Примусово вилучити кокон $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "Примусовий перезапуск"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "Примусово зупинити"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "ГБ"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "Шлюз"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "Перевірка працездатності"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "Довідка з інтервалу перевірки працездатності"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "Довідка з повторних спроб перевірки працездатності"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "Довідка із початкового періоду перевірки працездатності"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "Довідка з часу очікування перевірки працездатності"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "Довідка щодо дії при перевірці небезпечності стану"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "Працездатний"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "Приховати образи"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "Приховати проміжні образи"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "Журнал"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "Шлях у основній системі"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "Порт в основній системі"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "Довідка щодо порту в основній системі"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "Ід."
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP-адреса"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "Довідка щодо IP-адреси"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "Ідеальне для розробки"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "Ідеальне для запуску служб"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr ""
+"Якщо встановлено IP-адресу основної системи 0.0.0.0 або адресу не "
+"встановлено взагалі, порт буде пов'язано із усіма IP-адресами в основній "
+"системі."
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr ""
+"Якщо порт в основній системі не встановлено, порт контейнера буде випадковим "
+"чином пов'язано із портом в основній системі."
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "Ігнорувати IP-адресу, якщо її встановлено статично"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "Ігнорувати MAC-адресу, якщо її встановлено статично"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "Образ"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "Назва образу не є унікальною"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "Слід вказати назву образу"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "Довідка щодо вибору образу"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "Образи"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "Збільшити спільне використання процесора"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "Збільшити інтервал"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "Збільшити максимум повторних спроб"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "Збільшити пам'ять"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "Збільшити кількість повторних спроб"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "Збільшити початковий період"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "Збільшити час очікування"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "Інтеграція"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "Інтервал"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "Частота запуску перевірки працездатності."
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr ""
+"Некоректні символи. Назва може складатися лише з літер, цифр та деяких "
+"символів пунктуації (_ . -)."
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "кБ"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "Зберігати усі тимчасові файли контрольних точок"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "Ключ"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr ""
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "Останні 5 запусків"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "Найсвіжіша контрольна точка"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "Лишати запущеним після запису контрольної точки на диск"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "Завантаження подробиць…"
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "Завантаження журналу…"
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "Завантаження…"
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "Локальний"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "Локальні образи"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "Журнал"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC-адреса"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "МБ"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "Максимум повторних спроб"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "Пам'ять"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "Обмеження пам’яті"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "Одиниця пам’яті"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "Режим"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr ""
+"Для цього образу існує декілька міток. Виберіть позначені міткою образи для "
+"вилучення."
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr ""
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "Назва"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr ""
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "Назва нового контейнера"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "Назва нового образу"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "Ні"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "Без дії"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "Немає контейнерів"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "Цей образ не використовує жоден контейнер"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "У цьому коконі немає контейнерів"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "Немає контейнерів, які проходять поточні умови фільтрування"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "Змінних середовища не визначено"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "Немає образів"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "Образів не знайдено"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "Немає образів, які проходять поточні умови фільтрування"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "Немає мітки"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "Не відкрито жодного порту"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "Немає результатів, що відповідають $0"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "Немає запущених контейнерів"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "Томів не визначено"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "При помилці"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "Лише запущені"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "Параметри"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "Власник"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "Довідка щодо власника"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "Перевірку працездатності пройдено"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr ""
+"Вставте один або декілька рядків у форматі парт «ключ=значення» до будь-"
+"якого поля для пакетного імпортування"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "Призупинити"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "Призупинити роботу контейнера при створенні образу"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "Призупинено"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "Не вдалося створити стручок"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Назва стручка"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Контейнери Podman"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Служба Podman є неактивною"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "Прив'язка портів"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "Порти"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "Можна прив'язувати порти нижче 1024"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "Приватний"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "Протокол"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "Позбутися"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "Позбутися невикористаних контейнерів"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "Позбутися невикористаних образів"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "Очищуємо контейнери"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "Позбуваємося образів"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "Отримати найсвіжіший образ"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "Отримання даних"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "Доступ лише до читання"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "Доступ до читання і запису"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "Вилучити запис"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "Вилучає позначені незапущені контейнери"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "Вилучення"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "Перейменувати"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "Перейменувати контейнер $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "Можна встановлювати обмеження на ресурси"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "Перезапустити"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "Правила перезапуску"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "Довідка щодо правил перезапуску"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "Правила перезапуску, які слід виконувати при виході з контейнерів."
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"Перезапускати правила після виходу з контейнерів. Використання очікування "
+"для автозапуску контейнерів може за певних обставин не працювати, зокрема "
+"воно не працює, якщо для облікового запису користувача використано ecryptfs, "
+"systemd-homed, NFS або 2FA."
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "Відновити"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "Відновити контейнер $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "Відновити із встановленими з'єднаннями TCP"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "Обмежено правами доступу до облікового запису користувача"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "Продовжити"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "Повторні спроби"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "Повторіть спробу із іншим ключем пошуку."
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "Виконати перевірку працездатності"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "Запущено"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "Шукати за назвою і описом"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "Шукати за реєстром"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "Шукати"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "Шукати образ"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "Рядок для пошуку або розташування контейнера"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "Пошук…"
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "Шукаємо: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "Спільний"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "Показати"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "Показати образи"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "Показати проміжні образи"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "Стислий показ"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "Додаткові відомості"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "Розмір"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "Запустити"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "Початковий період"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "Запустити podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "Почніть щось вводити, щоб виконати пошук образів."
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "Запущено"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "Стан"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "Стан"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "Зупинити"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "Зупинено"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "Підтримка збереження встановлених з'єднань TCP"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "Система"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "Також доступна загальносистемна служба Podman"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "Мітка"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "Мітки"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Інтерфейс користувача Cockpit для контейнерів Podman."
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "Час ініціалізації, потрібний контейнеру для самозапуску."
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr ""
+"Максимальний припустимий час для завершення перевірки працездатності до "
+"моменту реєстрації помилки."
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr ""
+"Кількість повторних спроб, якими можна скористатися, доки перевірка "
+"працездатності вважатиметься непройденою."
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "Час очікування"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "Діагностика проблем"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "Введіть щось для фільтрування…"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "Не вдалося завантажити журнал образу"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "Непрацездатний"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "Працює з $0"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "Скористатися застарілим форматом Docker"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "Використовується"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "Користувач"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "Також доступна служба користувачів Podman"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "Користувач:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "Значення"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "Томи"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "Якщо непрацездатні"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "За допомогою термінала"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "Придатний до запису"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "контейнер"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "отримання"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "вузол[:порт]/[користувач]/контейнер[:мітка]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "образ"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "у"
+
+#: src/ImageDeleteModal.jsx:79
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate"
+msgstr "Приховати проміжні образи"
+
+#: src/ImageDeleteModal.jsx:59
+#, fuzzy
+#| msgid "Hide intermediate images"
+msgid "intermediate image"
+msgstr "Приховати проміжні образи"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "н/д"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "не доступне"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "група коконів"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "порти"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "секунд"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "система"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "не використано"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "користувач:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "томи"
+
+#~ msgid "Delete $0"
+#~ msgstr "Вилучити $0"
+
+#~ msgid "select all"
+#~ msgstr "вибрати все"
+
+#~ msgid "Failure action"
+#~ msgstr "Дія при критичній помилці"
+
+#~ msgid "Restarting"
+#~ msgstr "Перезапуск"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "Підтвердьте вилучення $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "Підтвердьте вилучення кокона $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "Підтвердьте примусове вилучення кокона $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "Підтвердьте примусове вилучення $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "Контейнер працює."
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "Не включати зміни у кореневій файловій системі при експортуванні"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "Типове із одним варіантом вибору"
+
+#~ msgid "Start after creation"
+#~ msgstr "Запустити після створення"
+
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "Вилучити невикористані $0 образів:"
+
+#~ msgid "created"
+#~ msgstr "створено"
+
+#~ msgid "exited"
+#~ msgstr "здійснено вихід"
+
+#~ msgid "paused"
+#~ msgstr "призупинено"
+
+#~ msgid "running"
+#~ msgstr "працює"
+
+#~ msgid "stopped"
+#~ msgstr "зупинився"
+
+#~ msgid "user"
+#~ msgstr "користувач"
+
+#~ msgid "Add on build variable"
+#~ msgstr "Додати змінну збирання"
+
+#~ msgid "Commit image"
+#~ msgstr "Надіслати образ"
+
+#~ msgid "Format"
+#~ msgstr "Формат"
+
+#~ msgid "Message"
+#~ msgstr "Повідомлення"
+
+#~ msgid "Pause the container"
+#~ msgstr "Призупинити роботу контейнера"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "Встановити змінну збирання"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "Встановити змінні збирання контейнера"
+
+#~ msgid "Add item"
+#~ msgstr "Додати запис"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "Порт основної системи (необов'язковий)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP-адреса (необов'язкова)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "Лише читання"
+
+#~ msgid "IP prefix length"
+#~ msgstr "Довжина префікса IP"
+
+#~ msgid "Get new image"
+#~ msgstr "Отримати новий образ"
+
+#~ msgid "Run"
+#~ msgstr "Запустити"
+
+#~ msgid "On build"
+#~ msgstr "При збиранні"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "Ви справді хочете вилучити цей образ?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "Не вдалося долучитися до цього контейнера: $0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "Не вдалося відкрити канал: $0"
+
+#~ msgid "Everything"
+#~ msgstr "Все"
+
+#~ msgid "Security"
+#~ msgstr "Безпека"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "Скануванням від $time ($type) вразливостей не знайдено."
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "У цій версії Web Console не передбачено підтримки терміналів."
diff --git a/po/zh_CN.po b/po/zh_CN.po
new file mode 100644
index 0000000..1954c49
--- /dev/null
+++ b/po/zh_CN.po
@@ -0,0 +1,1480 @@
+# #-#-#-#-# podman.js.pot (PACKAGE VERSION) #-#-#-#-#
+# SOME DESCRIPTIVE TITLE.
+# Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER
+# This file is distributed under the same license as the PACKAGE package.
+# Pany <geekpany@gmail.com>, 2020.
+# Harry Chen <harrychen0314@gmail.com>, 2020.
+msgid ""
+msgstr ""
+"Project-Id-Version: PACKAGE_VERSION\n"
+"Report-Msgid-Bugs-To: \n"
+"POT-Creation-Date: 2024-02-27 02:46+0000\n"
+"PO-Revision-Date: 2023-12-22 22:18+0000\n"
+"Last-Translator: Jingge Chen <mariocanfly@hotmail.com>\n"
+"Language-Team: Chinese (Simplified) <https://translate.fedoraproject.org/"
+"projects/cockpit-podman/main/zh_CN/>\n"
+"Language: zh_CN\n"
+"MIME-Version: 1.0\n"
+"Content-Type: text/plain; charset=UTF-8\n"
+"Content-Transfer-Encoding: 8bit\n"
+"Plural-Forms: nplurals=1; plural=0\n"
+"X-Generator: Weblate 5.3\n"
+
+#: src/Images.jsx:87
+msgid "$0 container"
+msgid_plural "$0 containers"
+msgstr[0] "$0容器"
+
+#: src/Images.jsx:269
+msgid "$0 image total, $1"
+msgid_plural "$0 images total, $1"
+msgstr[0] "共$0个镜像,$1"
+
+#: src/ContainerHealthLogs.jsx:35
+msgid "$0 second"
+msgid_plural "$0 seconds"
+msgstr[0] "$0 秒"
+
+#: src/Images.jsx:273
+msgid "$0 unused image, $1"
+msgid_plural "$0 unused images, $1"
+msgstr[0] "$0个未使用镜像,$1"
+
+#: src/PublishPort.jsx:30 src/PublishPort.jsx:41
+msgid "1 to 65535"
+msgstr "1 到 65535"
+
+#: src/ImageRunModal.jsx:1134
+msgid "Action to take once the container transitions to an unhealthy state."
+msgstr "容器转换到不健康状态后要执行的操作。"
+
+#: src/PodCreateModal.jsx:179 src/ImageRunModal.jsx:995
+msgid "Add port mapping"
+msgstr "添加端口映射"
+
+#: src/ImageRunModal.jsx:1017
+msgid "Add variable"
+msgstr "添加变量"
+
+#: src/PodCreateModal.jsx:191 src/ImageRunModal.jsx:1005
+msgid "Add volume"
+msgstr "添加卷"
+
+#: src/Containers.jsx:768 src/ContainerHeader.jsx:21
+#: src/ImageDeleteModal.jsx:104 src/ImageRunModal.jsx:702
+msgid "All"
+msgstr "所有"
+
+#: src/ImageSearchModal.jsx:176
+msgid "All registries"
+msgstr "所有 registry"
+
+#: src/ImageRunModal.jsx:965
+msgid "Always"
+msgstr "总是"
+
+#: src/PodActions.jsx:55
+msgid "An error occurred"
+msgstr "发生了错误"
+
+#: src/ContainerCommitModal.jsx:105
+msgid "Author"
+msgstr "作者"
+
+#: src/app.jsx:641
+msgid "Automatically start podman on boot"
+msgstr "开机自动启动 podman"
+
+#: src/Containers.jsx:543 src/Containers.jsx:546 src/Containers.jsx:605
+msgid "CPU"
+msgstr "CPU"
+
+#: src/ImageRunModal.jsx:918
+msgid "CPU Shares help"
+msgstr "CPU 份额帮助"
+
+#: src/ImageRunModal.jsx:916
+msgid "CPU shares"
+msgstr "CPU 份额"
+
+#: src/ImageRunModal.jsx:920
+msgid ""
+"CPU shares determine the priority of running containers. Default priority is "
+"1024. A higher number prioritizes this container. A lower number decreases "
+"priority."
+msgstr ""
+"CPU 份额决定了运行容器的优先级。默认优先级为 1024。数字越大,容器的优先级越"
+"高。数字越小,优先级越低。"
+
+#: src/PodCreateModal.jsx:213 src/ImageSearchModal.jsx:152
+#: src/ContainerDeleteModal.jsx:34 src/ContainerCommitModal.jsx:157
+#: src/ContainerCheckpointModal.jsx:50 src/PruneUnusedContainersModal.jsx:96
+#: src/PruneUnusedImagesModal.jsx:97 src/ImageDeleteModal.jsx:98
+#: src/ForceRemoveModal.jsx:25 src/ImageRunModal.jsx:1175 src/PodActions.jsx:50
+#: src/ContainerRenameModal.jsx:97 src/ContainerRestoreModal.jsx:53
+msgid "Cancel"
+msgstr "取消"
+
+#: src/Containers.jsx:315
+msgid "Checking health"
+msgstr "检查健康状况"
+
+#: src/Containers.jsx:228 src/ContainerCheckpointModal.jsx:46
+msgid "Checkpoint"
+msgstr "检查点"
+
+#: src/ImageRunModal.jsx:775
+msgid "Checkpoint and restore support"
+msgstr "检查点和恢复支持"
+
+#: src/ContainerCheckpointModal.jsx:41
+msgid "Checkpoint container $0"
+msgstr "检查点容器 $0"
+
+#: src/Containers.jsx:561
+msgid "Click to see published ports"
+msgstr "点击以查看公布的端口"
+
+#: src/Containers.jsx:576
+msgid "Click to see volumes"
+msgstr "点击以查看卷"
+
+#: org.cockpit-project.podman.metainfo.xml:6
+msgid "Cockpit component for Podman containers"
+msgstr "Podman 容器的 Cockpit 组件"
+
+#: src/ImageDetails.jsx:15 src/ContainerCommitModal.jsx:112
+#: src/ContainerDetails.jsx:39 src/ImageRunModal.jsx:871
+#: src/ImageRunModal.jsx:1026 src/ContainerHealthLogs.jsx:60
+msgid "Command"
+msgstr "命令"
+
+#: src/ImageHistory.jsx:33
+msgid "Comments"
+msgstr "注释"
+
+#: src/ContainerCommitModal.jsx:144 src/Containers.jsx:263
+msgid "Commit"
+msgstr "提交"
+
+#: src/ContainerCommitModal.jsx:136
+msgid "Commit container"
+msgstr "提交容器"
+
+#: src/util.js:23
+msgid "Configured"
+msgstr "已配置"
+
+#: src/Containers.jsx:470
+msgid "Console"
+msgstr "控制台"
+
+#: src/Containers.jsx:603
+msgid "Container"
+msgstr "容器"
+
+#: src/ImageRunModal.jsx:258
+msgid "Container failed to be created"
+msgstr "容器创建失败"
+
+#: src/ImageRunModal.jsx:241
+msgid "Container failed to be started"
+msgstr "容器启动失败"
+
+#: src/ContainerTerminal.jsx:259
+msgid "Container is not running"
+msgstr "容器未运行"
+
+#: src/ImageRunModal.jsx:742
+msgid "Container name"
+msgstr "容器名称"
+
+#: src/ContainerRenameModal.jsx:28 src/ContainerRenameModal.jsx:39
+msgid "Container name is required."
+msgstr "容器名称是必需的。"
+
+#: src/Volume.jsx:50
+msgid "Container path"
+msgstr "容器路径"
+
+#: src/Volume.jsx:23
+msgid "Container path must not be empty"
+msgstr "容器路径不能为空"
+
+#: src/PublishPort.jsx:105
+msgid "Container port"
+msgstr "容器端口"
+
+#: src/PublishPort.jsx:37
+msgid "Container port must not be empty"
+msgstr "容器端口不能为空"
+
+#: src/Containers.jsx:822 src/Containers.jsx:828 src/Containers.jsx:858
+msgid "Containers"
+msgstr "容器"
+
+#: src/PodCreateModal.jsx:210 src/ImageRunModal.jsx:1172
+msgid "Create"
+msgstr "创建"
+
+#: src/ContainerCommitModal.jsx:137
+msgid "Create a new image based on the current state of the $0 container."
+msgstr "根据 $0 容器的当前状态创建一个新镜像。"
+
+#: src/ImageRunModal.jsx:1169
+msgid "Create and run"
+msgstr "创建并运行"
+
+#: src/Containers.jsx:785 src/Images.jsx:405 src/Images.jsx:419
+#: src/ImageRunModal.jsx:1166
+msgid "Create container"
+msgstr "创建容器"
+
+#: src/ImageRunModal.jsx:1166
+msgid "Create container in $0"
+msgstr "在 $0 中创建容器"
+
+#: src/Containers.jsx:868
+msgid "Create container in pod"
+msgstr "在 pod 中创建容器"
+
+#: src/PodCreateModal.jsx:206 src/Containers.jsx:777
+msgid "Create pod"
+msgstr "创建 pod"
+
+#: src/ImageHistory.jsx:33 src/ContainerDetails.jsx:63
+#: src/PruneUnusedContainersModal.jsx:65 src/util.js:23 src/util.js:26
+#: src/Images.jsx:178
+msgid "Created"
+msgstr "已创建"
+
+#: src/ImageHistory.jsx:33
+msgid "Created by"
+msgstr "创建"
+
+#: src/ImageRunModal.jsx:939
+msgid "Decrease CPU shares"
+msgstr "减少 CPU 共享"
+
+#: src/ImageRunModal.jsx:1049
+msgid "Decrease interval"
+msgstr "缩短间隔"
+
+#: src/ImageRunModal.jsx:978
+msgid "Decrease maximum retries"
+msgstr "减小最大重试次数"
+
+#: src/ImageRunModal.jsx:897
+msgid "Decrease memory"
+msgstr "减少内存"
+
+#: src/ImageRunModal.jsx:1123
+msgid "Decrease retries"
+msgstr "减少重试"
+
+#: src/ImageRunModal.jsx:1099
+msgid "Decrease start period"
+msgstr "减少启动周期"
+
+#: src/ImageRunModal.jsx:1074
+msgid "Decrease timeout"
+msgstr "减少超时"
+
+#: src/ContainerDeleteModal.jsx:33 src/Containers.jsx:282 src/Images.jsx:425
+#: src/PodActions.jsx:46 src/PodActions.jsx:180
+msgid "Delete"
+msgstr "删除"
+
+#: src/ImageDeleteModal.jsx:92
+msgid "Delete $0 image?"
+msgstr "删除 $0 图片?"
+
+#: src/ContainerDeleteModal.jsx:31 src/ForceRemoveModal.jsx:18
+msgid "Delete $0?"
+msgstr "删除 $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete image"
+msgstr "删除镜像"
+
+#: src/PodActions.jsx:41
+msgid "Delete pod $0?"
+msgstr "删除 pod $0?"
+
+#: src/ImageDeleteModal.jsx:96
+msgid "Delete tagged images"
+msgstr "删除标记的镜像"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused system images:"
+msgstr "删除未使用的系统镜像:"
+
+#: src/PruneUnusedImagesModal.jsx:32
+msgid "Delete unused user images:"
+msgstr "删除未使用的用户镜像:"
+
+#: src/ContainerDeleteModal.jsx:37
+msgid "Deleting a container will erase all data in it."
+msgstr "删除容器会清空其中的全部数据。"
+
+#: src/Containers.jsx:71
+msgid "Deleting a running container will erase all data in it."
+msgstr "删除正在运行的容器将会清除其中的所有数据。"
+
+#: src/PodActions.jsx:61
+msgid "Deleting this pod will remove the following containers:"
+msgstr "删除这个 Pod 将会移除这些容器:"
+
+#: src/Containers.jsx:453 src/Images.jsx:147 src/ImageRunModal.jsx:757
+msgid "Details"
+msgstr "详情"
+
+#: src/Images.jsx:180
+msgid "Disk space"
+msgstr "磁盘空间"
+
+#: src/ContainerCommitModal.jsx:126
+msgid ""
+"Docker format is useful when sharing the image with Docker or Moby Engine"
+msgstr "当与 Docker 或 Moby Engine 共享镜像时,Docker 格式非常有用"
+
+#: src/ImageSearchModal.jsx:149
+msgid "Download"
+msgstr "下载"
+
+#: src/Images.jsx:345
+msgid "Download new image"
+msgstr "现在新镜像"
+
+#: src/PodActions.jsx:57
+msgid "Empty pod $0 will be permanently removed."
+msgstr "空 pod $0 将被永久删除。"
+
+#: src/ImageDetails.jsx:21 src/ImageRunModal.jsx:866
+msgid "Entrypoint"
+msgstr "入口点"
+
+#: src/ContainerIntegration.jsx:114 src/ImageRunModal.jsx:1016
+msgid "Environment variables"
+msgstr "环境变量"
+
+#: src/util.js:26
+msgid "Error"
+msgstr "错误"
+
+#: src/Notification.jsx:42 src/Images.jsx:56
+msgid "Error message"
+msgstr "错误信息"
+
+#: src/ContainerTerminal.jsx:263
+msgid "Error occurred while connecting console"
+msgstr "连接到控制台时出现错误"
+
+#: src/ContainerCommitModal.jsx:107
+msgid "Example, Your Name <yourname@example.com>"
+msgstr "示例,您的名字 <yourname@example.com>"
+
+#: src/ImageRunModal.jsx:821
+msgid "Example: $0"
+msgstr "示例:$0"
+
+#: src/ContainerDetails.jsx:14 src/util.js:23 src/util.js:26
+msgid "Exited"
+msgstr "已退出"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Failed health run"
+msgstr "失败的健康运行"
+
+#: src/ContainerCheckpointModal.jsx:28
+msgid "Failed to checkpoint container $0"
+msgstr "检查点容器 $0 失败"
+
+#: src/ImageRunModal.jsx:247
+msgid "Failed to clean up container"
+msgstr "清理容器失败"
+
+#: src/ContainerCommitModal.jsx:81
+msgid "Failed to commit container $0"
+msgstr "提交容器 $0 失败"
+
+#: src/ImageRunModal.jsx:312
+msgid "Failed to create container $0"
+msgstr "创建容器 $0 失败"
+
+#: src/Images.jsx:53
+msgid "Failed to download image $0:$1"
+msgstr "下载镜像 $0:$1 失败"
+
+#: src/Containers.jsx:60
+msgid "Failed to force remove container $0"
+msgstr "强制移除容器 $0 失败"
+
+#: src/ImageDeleteModal.jsx:49
+msgid "Failed to force remove image $0"
+msgstr "强制删除镜像失败 $0"
+
+#: src/PodActions.jsx:116
+msgid "Failed to force restart pod $0"
+msgstr "强制重启 pod $0 失败"
+
+#: src/PodActions.jsx:94
+msgid "Failed to force stop pod $0"
+msgstr "强制停止 Pod $0 失败"
+
+#: src/Containers.jsx:117
+msgid "Failed to pause container $0"
+msgstr "暂停容器 $0 失败"
+
+#: src/PodActions.jsx:161
+msgid "Failed to pause pod $0"
+msgstr "暂停 Pod $0 失败"
+
+#: src/PruneUnusedContainersModal.jsx:57
+msgid "Failed to prune unused containers"
+msgstr "删除未使用的容器失败"
+
+#: src/PruneUnusedImagesModal.jsx:73
+msgid "Failed to prune unused images"
+msgstr "删除未使用的映像失败"
+
+#: src/ImageRunModal.jsx:318
+msgid "Failed to pull image $0"
+msgstr "拉取镜像 $0 失败"
+
+#: src/ContainerDeleteModal.jsx:21
+msgid "Failed to remove container $0"
+msgstr "移除容器 $0 失败"
+
+#: src/ImageDeleteModal.jsx:73
+msgid "Failed to remove image $0"
+msgstr "删除镜像 $0 失败"
+
+#: src/ContainerRenameModal.jsx:54
+msgid "Failed to rename container $0"
+msgstr "重命名容器 $0 失败"
+
+#: src/Containers.jsx:148
+msgid "Failed to restart container $0"
+msgstr "重启容器 $0 失败"
+
+#: src/PodActions.jsx:105
+msgid "Failed to restart pod $0"
+msgstr "重启 pod $0 失败"
+
+#: src/ContainerRestoreModal.jsx:31
+msgid "Failed to restore container $0"
+msgstr "恢复容器 $0 失败"
+
+#: src/Containers.jsx:107
+msgid "Failed to resume container $0"
+msgstr "恢复容器 $0 失败"
+
+#: src/PodActions.jsx:146
+msgid "Failed to resume pod $0"
+msgstr "恢复 Pod $0 失败"
+
+#: src/ImageRunModal.jsx:305
+msgid "Failed to run container $0"
+msgstr "运行容器 $0 失败"
+
+#: src/Containers.jsx:134 src/ContainerHealthLogs.jsx:94
+msgid "Failed to run health check on container $0"
+msgstr "在容器 $0 上运行健康检查失败"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images."
+msgstr "搜索镜像失败。"
+
+#: src/ImageSearchModal.jsx:93 src/ImageRunModal.jsx:399
+msgid "Failed to search for images: $0"
+msgstr "搜索镜像 $0 失败"
+
+#: src/ImageSearchModal.jsx:92 src/ImageRunModal.jsx:397
+msgid "Failed to search for new images"
+msgstr "搜索新镜像失败"
+
+#: src/Containers.jsx:97
+msgid "Failed to start container $0"
+msgstr "启动容器 $0 失败"
+
+#: src/PodActions.jsx:131
+msgid "Failed to start pod $0"
+msgstr "启动 Pod $0 失败"
+
+#: src/Containers.jsx:87
+msgid "Failed to stop container $0"
+msgstr "停止容器 $0 失败"
+
+#: src/PodActions.jsx:83
+msgid "Failed to stop pod $0"
+msgstr "停止 Pod $0 失败"
+
+#: src/ContainerHealthLogs.jsx:84
+msgid "Failing streak"
+msgstr "streak 失败"
+
+#: src/ContainerCommitModal.jsx:151
+msgid "Force commit"
+msgstr "强制提交"
+
+#: src/ForceRemoveModal.jsx:23 src/PodActions.jsx:46
+msgid "Force delete"
+msgstr "强制删除"
+
+#: src/PodActions.jsx:40
+msgid "Force delete pod $0?"
+msgstr "强制删除 pod $0?"
+
+#: src/Containers.jsx:203 src/PodActions.jsx:120
+msgid "Force restart"
+msgstr "强制重启"
+
+#: src/Containers.jsx:195 src/ImageRunModal.jsx:61 src/PodActions.jsx:98
+#: src/ContainerHealthLogs.jsx:42
+msgid "Force stop"
+msgstr "强制停止"
+
+#: src/ImageRunModal.jsx:908
+msgid "GB"
+msgstr "GB"
+
+#: src/ContainerDetails.jsx:51
+msgid "Gateway"
+msgstr "网关"
+
+#: src/Containers.jsx:479 src/ImageRunModal.jsx:1025
+msgid "Health check"
+msgstr "健康检查"
+
+#: src/ImageRunModal.jsx:1034
+msgid "Health check interval help"
+msgstr "健康检查间隔帮助"
+
+#: src/ImageRunModal.jsx:1109
+msgid "Health check retries help"
+msgstr "健康检查重试帮助"
+
+#: src/ImageRunModal.jsx:1084
+msgid "Health check start period help"
+msgstr "健康检查启动周期帮助"
+
+#: src/ImageRunModal.jsx:1059
+msgid "Health check timeout help"
+msgstr "健康检查超时帮助"
+
+#: src/ImageRunModal.jsx:1132
+msgid "Health failure check action help"
+msgstr "健康检查失败操作帮助"
+
+#: src/Containers.jsx:311
+msgid "Healthy"
+msgstr "健康"
+
+#: src/Images.jsx:300
+msgid "Hide images"
+msgstr "隐藏镜像"
+
+#: src/Images.jsx:250
+msgid "Hide intermediate images"
+msgstr "隐藏中间镜像"
+
+#: src/Images.jsx:156
+msgid "History"
+msgstr "历史"
+
+#: src/Volume.jsx:36
+msgid "Host path"
+msgstr "主机路径"
+
+#: src/PublishPort.jsx:78
+msgid "Host port"
+msgstr "主机端口"
+
+#: src/PublishPort.jsx:81
+msgid "Host port help"
+msgstr "主机端口帮助"
+
+#: src/ContainerDetails.jsx:31 src/Images.jsx:179
+msgid "ID"
+msgstr "ID"
+
+#: src/PublishPort.jsx:55 src/ContainerDetails.jsx:47
+msgid "IP address"
+msgstr "IP 地址"
+
+#: src/PublishPort.jsx:58
+msgid "IP address help"
+msgstr "IP 地址帮助"
+
+#: src/ImageRunModal.jsx:786
+msgid "Ideal for development"
+msgstr "非常适合开发"
+
+#: src/ImageRunModal.jsx:769
+msgid "Ideal for running services"
+msgstr "非常适合运行服务"
+
+#: src/PublishPort.jsx:60
+msgid ""
+"If host IP is set to 0.0.0.0 or not set at all, the port will be bound on "
+"all IPs on the host."
+msgstr "如果主机 IP 设置为 0.0.0.0 或没有设置,端口将被绑定到主机上的所有 IP。"
+
+#: src/PublishPort.jsx:83
+msgid ""
+"If the host port is not set the container port will be randomly assigned a "
+"port on the host."
+msgstr "如果主机端口没有设置,容器端口将被随机分配到主机上的一个端口。"
+
+#: src/ContainerRestoreModal.jsx:63
+msgid "Ignore IP address if set statically"
+msgstr "如果以静态方式设置,则忽略 IP 地址"
+
+#: src/ContainerRestoreModal.jsx:66
+msgid "Ignore MAC address if set statically"
+msgstr "忽略被静态设定的 MAC 地址"
+
+#: src/ContainerDetails.jsx:35 src/Images.jsx:176 src/ImageRunModal.jsx:814
+msgid "Image"
+msgstr "镜像"
+
+#: src/ContainerCommitModal.jsx:44
+msgid "Image name is not unique"
+msgstr "镜像名称不是唯一的"
+
+#: src/ContainerCommitModal.jsx:35
+msgid "Image name is required"
+msgstr "镜像名称是必需的"
+
+#: src/ImageRunModal.jsx:816
+msgid "Image selection help"
+msgstr "镜像选择帮助"
+
+#: src/Images.jsx:256 src/Images.jsx:286
+msgid "Images"
+msgstr "镜像"
+
+#: src/ImageRunModal.jsx:940
+msgid "Increase CPU shares"
+msgstr "增加 CPU 共享"
+
+#: src/ImageRunModal.jsx:1050
+msgid "Increase interval"
+msgstr "增加间隔"
+
+#: src/ImageRunModal.jsx:979
+msgid "Increase maximum retries"
+msgstr "增加最大重试次数"
+
+#: src/ImageRunModal.jsx:898
+msgid "Increase memory"
+msgstr "增加内存"
+
+#: src/ImageRunModal.jsx:1124
+msgid "Increase retries"
+msgstr "增加重试"
+
+#: src/ImageRunModal.jsx:1100
+msgid "Increase start period"
+msgstr "增加开始周期"
+
+#: src/ImageRunModal.jsx:1075
+msgid "Increase timeout"
+msgstr "增加超时"
+
+#: src/Containers.jsx:460 src/ImageRunModal.jsx:989
+msgid "Integration"
+msgstr "集成"
+
+#: src/ImageRunModal.jsx:1032 src/ContainerHealthLogs.jsx:64
+msgid "Interval"
+msgstr "间隔"
+
+#: src/ImageRunModal.jsx:1036
+msgid "Interval how often health check is run."
+msgstr "健康检查运行的频率间隔。"
+
+#: src/PodCreateModal.jsx:113 src/ContainerRenameModal.jsx:32
+msgid ""
+"Invalid characters. Name can only contain letters, numbers, and certain "
+"punctuation (_ . -)."
+msgstr "无效的字符。名称只能包含字母、数字和某些标点符号 (_ . -)。"
+
+#: src/ImageRunModal.jsx:906
+msgid "KB"
+msgstr "KB"
+
+#: src/ContainerCheckpointModal.jsx:55 src/ContainerRestoreModal.jsx:58
+msgid "Keep all temporary checkpoint files"
+msgstr "保留所有临时检查点文件"
+
+#: src/Env.jsx:56
+msgid "Key"
+msgstr "键"
+
+#: src/Env.jsx:18
+msgid "Key must not be empty"
+msgstr "键不能为空"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Last 5 runs"
+msgstr "最后 5 个运行"
+
+#: src/ContainerDetails.jsx:71
+msgid "Latest checkpoint"
+msgstr "最新的检查点"
+
+#: src/ContainerCheckpointModal.jsx:57
+msgid "Leave running after writing checkpoint to disk"
+msgstr "在将检查点写入磁盘后保持运行"
+
+#: src/ContainerIntegration.jsx:93 src/ImageHistory.jsx:59
+msgid "Loading details..."
+msgstr "加载详细信息..."
+
+#: src/ContainerLogs.jsx:54
+msgid "Loading logs..."
+msgstr "正在加载日志..."
+
+#: src/ImageUsedBy.jsx:12 src/Containers.jsx:617
+msgid "Loading..."
+msgstr "加载中……"
+
+#: src/ImageRunModal.jsx:711
+msgid "Local"
+msgstr "本地"
+
+#: src/ImageRunModal.jsx:502
+msgid "Local images"
+msgstr "本地镜像"
+
+#: src/Containers.jsx:465 src/ContainerHealthLogs.jsx:102
+msgid "Logs"
+msgstr "日志"
+
+#: src/ContainerDetails.jsx:55
+msgid "MAC address"
+msgstr "MAC 地址"
+
+#: src/ImageRunModal.jsx:907
+msgid "MB"
+msgstr "MB"
+
+#: src/ImageRunModal.jsx:971
+msgid "Maximum retries"
+msgstr "最大重试次数"
+
+#: src/Containers.jsx:550 src/Containers.jsx:553 src/Containers.jsx:606
+msgid "Memory"
+msgstr "内存"
+
+#: src/ImageRunModal.jsx:884
+msgid "Memory limit"
+msgstr "内存限制"
+
+#: src/ImageRunModal.jsx:901
+msgid "Memory unit"
+msgstr "内存单元"
+
+#: src/Volume.jsx:64
+msgid "Mode"
+msgstr "模式"
+
+#: src/ImageDeleteModal.jsx:102
+msgid "Multiple tags exist for this image. Select the tagged images to delete."
+msgstr "此镜像存在多个标签。选择标记的镜像以删除。"
+
+#: src/PublishPort.jsx:24
+msgid "Must be a valid IP address"
+msgstr "必须是有效的 IP 地址"
+
+#: src/PodCreateModal.jsx:144 src/PruneUnusedContainersModal.jsx:64
+#: src/ImageRunModal.jsx:739
+msgid "Name"
+msgstr "名称"
+
+#: src/ImageRunModal.jsx:612
+msgid "Name already in use"
+msgstr "名称已被使用"
+
+#: src/ContainerRenameModal.jsx:68
+msgid "New container name"
+msgstr "新容器名称"
+
+#: src/ContainerCommitModal.jsx:90
+msgid "New image name"
+msgstr "新镜像名称"
+
+#: src/ImageRunModal.jsx:963
+msgid "No"
+msgstr "否"
+
+#: src/ImageRunModal.jsx:58 src/ContainerHealthLogs.jsx:39
+msgid "No action"
+msgstr "无操作"
+
+#: src/Containers.jsx:614
+msgid "No containers"
+msgstr "没有容器"
+
+#: src/ImageUsedBy.jsx:14
+msgid "No containers are using this image"
+msgstr "没有使用该镜像的容器"
+
+#: src/Containers.jsx:615
+msgid "No containers in this pod"
+msgstr "此 pod 中没有容器"
+
+#: src/Containers.jsx:619
+msgid "No containers that match the current filter"
+msgstr "没有符合当前筛选条件的容器"
+
+#: src/ImageRunModal.jsx:1014
+msgid "No environment variables specified"
+msgstr "没有指定环境变量"
+
+#: src/Images.jsx:183
+msgid "No images"
+msgstr "没有镜像"
+
+#: src/ImageSearchModal.jsx:185 src/ImageRunModal.jsx:839
+msgid "No images found"
+msgstr "没有找到镜像"
+
+#: src/Images.jsx:187
+msgid "No images that match the current filter"
+msgstr "没有符合当前筛选条件的镜像"
+
+#: src/Volume.jsx:75
+msgid "No label"
+msgstr "无标签"
+
+#: src/PodCreateModal.jsx:176 src/ImageRunModal.jsx:992
+msgid "No ports exposed"
+msgstr "没有公开的端口"
+
+#: src/ImageSearchModal.jsx:189
+msgid "No results for $0"
+msgstr "没有 $0 的结果"
+
+#: src/Containers.jsx:621
+msgid "No running containers"
+msgstr "没有正在运行的容器"
+
+#: src/PodCreateModal.jsx:188 src/ImageRunModal.jsx:1002
+msgid "No volumes specified"
+msgstr "没有指定卷"
+
+#: src/ImageRunModal.jsx:964
+msgid "On failure"
+msgstr "失败时"
+
+#: src/Containers.jsx:769
+msgid "Only running"
+msgstr "仅运行"
+
+#: src/ContainerCommitModal.jsx:118
+msgid "Options"
+msgstr "选项"
+
+#: src/PodCreateModal.jsx:162 src/ImageSearchModal.jsx:159
+#: src/Containers.jsx:604 src/ContainerHeader.jsx:15
+#: src/PruneUnusedContainersModal.jsx:69 src/Images.jsx:177
+#: src/ImageRunModal.jsx:759
+msgid "Owner"
+msgstr "所有者"
+
+#: src/ImageRunModal.jsx:761
+msgid "Owner help"
+msgstr "所有者帮助"
+
+#: src/ContainerHealthLogs.jsx:115
+msgid "Passed health run"
+msgstr "通过的健康运行"
+
+#: src/ImageRunModal.jsx:1022
+msgid ""
+"Paste one or more lines of key=value pairs into any field for bulk import"
+msgstr "将一个或多个 key=value 对行粘贴到批量导入的任何字段"
+
+#: src/Containers.jsx:211 src/PodActions.jsx:165
+msgid "Pause"
+msgstr "暂停"
+
+#: src/ContainerCommitModal.jsx:122
+msgid "Pause container when creating image"
+msgstr "创建镜像时暂停容器"
+
+#: src/util.js:23 src/util.js:26
+msgid "Paused"
+msgstr "已暂停"
+
+#: src/PodCreateModal.jsx:89
+msgid "Pod failed to be created"
+msgstr "创建 Pod 失败"
+
+#: src/PodCreateModal.jsx:147
+msgid "Pod name"
+msgstr "Pod 名称"
+
+#: org.cockpit-project.podman.metainfo.xml:5
+msgid "Podman"
+msgstr "Podman"
+
+#: src/index.html:20 src/manifest.json:0
+msgid "Podman containers"
+msgstr "Podman 容器"
+
+#: src/app.jsx:637
+msgid "Podman service is not active"
+msgstr "Podman 服务未激活"
+
+#: src/PodCreateModal.jsx:178 src/ImageRunModal.jsx:994
+msgid "Port mapping"
+msgstr "端口映射"
+
+#: src/ContainerIntegration.jsx:106 src/ImageDetails.jsx:39
+msgid "Ports"
+msgstr "端口"
+
+#: src/ImageRunModal.jsx:778
+msgid "Ports under 1024 can be mapped"
+msgstr "1024 以下的端口可以映射"
+
+#: src/Volume.jsx:77
+msgid "Private"
+msgstr "私有"
+
+#: src/PublishPort.jsx:122
+msgid "Protocol"
+msgstr "协议"
+
+#: src/PruneUnusedContainersModal.jsx:94 src/PruneUnusedImagesModal.jsx:95
+msgid "Prune"
+msgstr "删除"
+
+#: src/Containers.jsx:339 src/PruneUnusedContainersModal.jsx:87
+msgid "Prune unused containers"
+msgstr "删除未使用的容器"
+
+#: src/PruneUnusedImagesModal.jsx:88 src/Images.jsx:357
+msgid "Prune unused images"
+msgstr "删除未使用的镜像"
+
+#: src/PruneUnusedContainersModal.jsx:90 src/PruneUnusedContainersModal.jsx:94
+msgid "Pruning containers"
+msgstr "正在删除容器"
+
+#: src/PruneUnusedImagesModal.jsx:91 src/PruneUnusedImagesModal.jsx:95
+msgid "Pruning images"
+msgstr "删除镜像"
+
+#: src/ImageRunModal.jsx:860
+msgid "Pull latest image"
+msgstr "拉取最新的镜像"
+
+#: src/Images.jsx:323
+msgid "Pulling"
+msgstr "正在拉取"
+
+#: src/ContainerIntegration.jsx:42
+msgid "Read-only access"
+msgstr "只读访问"
+
+#: src/ContainerIntegration.jsx:41
+msgid "Read-write access"
+msgstr "读写访问"
+
+#: src/PublishPort.jsx:137 src/Env.jsx:91 src/Volume.jsx:84
+msgid "Remove item"
+msgstr "删除项目"
+
+#: src/PruneUnusedContainersModal.jsx:99
+msgid "Removes selected non-running containers"
+msgstr "删除选择的、没有在运行的容器"
+
+#: src/util.js:23
+msgid "Removing"
+msgstr "删除"
+
+#: src/Containers.jsx:181 src/ContainerRenameModal.jsx:92
+msgid "Rename"
+msgstr "重命名"
+
+#: src/ContainerRenameModal.jsx:85
+msgid "Rename container $0"
+msgstr "重命名容器 $0"
+
+#: src/ImageRunModal.jsx:772
+msgid "Resource limits can be set"
+msgstr "可以设置资源限制"
+
+#: src/Containers.jsx:199 src/util.js:23 src/ImageRunModal.jsx:59
+#: src/PodActions.jsx:109 src/ContainerHealthLogs.jsx:40
+msgid "Restart"
+msgstr "重启"
+
+#: src/ImageRunModal.jsx:948
+msgid "Restart policy"
+msgstr "重启策略"
+
+#: src/ImageRunModal.jsx:950 src/ImageRunModal.jsx:960
+msgid "Restart policy help"
+msgstr "重启策略帮助"
+
+#: src/ImageRunModal.jsx:952
+msgid "Restart policy to follow when containers exit."
+msgstr "容器退出时遵循重启策略。"
+
+#: src/ImageRunModal.jsx:952
+msgid ""
+"Restart policy to follow when containers exit. Using linger for auto-"
+"starting containers may not work in some circumstances, such as when "
+"ecryptfs, systemd-homed, NFS, or 2FA are used on a user account."
+msgstr ""
+"容器退出时重启策略。在某些情况下,使用 linger 进行自动启动容器可能无法正常工"
+"作,如在用户账户中使用了 ecryptfs、systemd-homed、NFS 或 2FA。"
+
+#: src/Containers.jsx:249 src/ContainerRestoreModal.jsx:49
+msgid "Restore"
+msgstr "恢复"
+
+#: src/ContainerRestoreModal.jsx:44
+msgid "Restore container $0"
+msgstr "恢复容器 $0"
+
+#: src/ContainerRestoreModal.jsx:60
+msgid "Restore with established TCP connections"
+msgstr "使用已建立的 TCP 连接恢复"
+
+#: src/ImageRunModal.jsx:789
+msgid "Restricted by user account permissions"
+msgstr "受用户帐户权限限制"
+
+#: src/Containers.jsx:218 src/PodActions.jsx:150
+msgid "Resume"
+msgstr "继续"
+
+#: src/ImageRunModal.jsx:1107 src/ContainerHealthLogs.jsx:68
+msgid "Retries"
+msgstr "重试"
+
+#: src/ImageSearchModal.jsx:190
+msgid "Retry another term."
+msgstr "重试另一个项。"
+
+#: src/Containers.jsx:272 src/ContainerHealthLogs.jsx:98
+msgid "Run health check"
+msgstr "运行健康检查"
+
+#: src/ImageUsedBy.jsx:35 src/util.js:23 src/util.js:26
+msgid "Running"
+msgstr "正在运行"
+
+#: src/Volume.jsx:71
+msgid "SELinux"
+msgstr "SELinux"
+
+#: src/ImageSearchModal.jsx:167
+msgid "Search by name or description"
+msgstr "按名称或描述搜索"
+
+#: src/ImageRunModal.jsx:701
+msgid "Search by registry"
+msgstr "按照注册表搜索"
+
+#: src/ImageSearchModal.jsx:164
+msgid "Search for"
+msgstr "搜索"
+
+#: src/ImageSearchModal.jsx:136
+msgid "Search for an image"
+msgstr "搜索镜像"
+
+#: src/ImageRunModal.jsx:844
+msgid "Search string or container location"
+msgstr "搜索字符串或容器位置"
+
+#: src/ImageSearchModal.jsx:183
+msgid "Searching..."
+msgstr "搜索中..."
+
+#: src/ImageRunModal.jsx:822
+msgid "Searching: $0"
+msgstr "搜索: $0"
+
+#: src/Volume.jsx:76
+msgid "Shared"
+msgstr "共享"
+
+#: src/Containers.jsx:764
+msgid "Show"
+msgstr "显示"
+
+#: src/Images.jsx:300
+msgid "Show images"
+msgstr "显示镜像"
+
+#: src/Images.jsx:250
+msgid "Show intermediate images"
+msgstr "显示中间镜像"
+
+#: src/ContainerIntegration.jsx:82
+msgid "Show less"
+msgstr "显示更少"
+
+#: src/ContainerIntegration.jsx:82 src/PruneUnusedImagesModal.jsx:48
+msgid "Show more"
+msgstr "显示更多"
+
+#: src/ImageHistory.jsx:33
+msgid "Size"
+msgstr "大小"
+
+#: src/Containers.jsx:238 src/PodActions.jsx:135 src/app.jsx:683
+msgid "Start"
+msgstr "启动"
+
+#: src/ImageRunModal.jsx:1082 src/ContainerHealthLogs.jsx:72
+msgid "Start period"
+msgstr "开始期间"
+
+#: src/app.jsx:644
+msgid "Start podman"
+msgstr "启动 podman"
+
+#: src/ImageSearchModal.jsx:185
+msgid "Start typing to look for images."
+msgstr "开始键入来查找镜像。"
+
+#: src/ContainerHealthLogs.jsx:105
+msgid "Started at"
+msgstr "开始于"
+
+#: src/Containers.jsx:607 src/ContainerDetails.jsx:67
+msgid "State"
+msgstr "状态"
+
+#: src/ContainerHealthLogs.jsx:56
+msgid "Status"
+msgstr "状态"
+
+#: src/Containers.jsx:191 src/ImageRunModal.jsx:60 src/PodActions.jsx:87
+#: src/ContainerHealthLogs.jsx:41
+msgid "Stop"
+msgstr "停止"
+
+#: src/util.js:23 src/util.js:26
+msgid "Stopped"
+msgstr "已停止"
+
+#: src/ContainerCheckpointModal.jsx:60
+msgid "Support preserving established TCP connections"
+msgstr "支持保留已建立的 TCP 连接"
+
+#: src/PodCreateModal.jsx:164 src/ContainerHeader.jsx:20
+#: src/ImageRunModal.jsx:766 src/ImageRunModal.jsx:801
+msgid "System"
+msgstr "系统"
+
+#: src/app.jsx:690
+msgid "System Podman service is also available"
+msgstr "系统 Podman 服务同样可用"
+
+#: src/PublishPort.jsx:128
+msgid "TCP"
+msgstr "TCP"
+
+#: src/ImageSearchModal.jsx:139 src/ContainerCommitModal.jsx:98
+msgid "Tag"
+msgstr "标签"
+
+#: src/ImageDetails.jsx:27
+msgid "Tags"
+msgstr "标签"
+
+#: org.cockpit-project.podman.metainfo.xml:10
+msgid "The Cockpit user interface for Podman containers."
+msgstr "Podman 容器的 Cockpit 用户界面。"
+
+#: src/ImageRunModal.jsx:1086
+msgid "The initialization time needed for a container to bootstrap."
+msgstr "容器进行 bootstrap 所需的初始化时间。"
+
+#: src/ImageRunModal.jsx:1061
+msgid ""
+"The maximum time allowed to complete the health check before an interval is "
+"considered failed."
+msgstr "当间隔被视为失败前,允许完成健康检查的最长时间。"
+
+#: src/ImageRunModal.jsx:1111
+msgid ""
+"The number of retries allowed before a healthcheck is considered to be "
+"unhealthy."
+msgstr "在健康检查被视为不健康前,允许的重试次数。"
+
+#: src/ImageRunModal.jsx:1057 src/ContainerHealthLogs.jsx:76
+msgid "Timeout"
+msgstr "超时"
+
+#: src/app.jsx:649
+msgid "Troubleshoot"
+msgstr "故障排除"
+
+#: src/ContainerHeader.jsx:28
+msgid "Type to filter…"
+msgstr "输入筛选条件……"
+
+#: src/PublishPort.jsx:129
+msgid "UDP"
+msgstr "UDP"
+
+#: src/ImageHistory.jsx:59
+msgid "Unable to load image history"
+msgstr "无法加载镜像历史记录"
+
+#: src/Containers.jsx:313
+msgid "Unhealthy"
+msgstr "不健康"
+
+#: src/ContainerDetails.jsx:12
+msgid "Up since $0"
+msgstr "自 $0 开始运行"
+
+#: src/ContainerCommitModal.jsx:127
+msgid "Use legacy Docker format"
+msgstr "使用旧的 Docker 格式"
+
+#: src/ImageDetails.jsx:33 src/Images.jsx:181
+msgid "Used by"
+msgstr "使用者"
+
+#: src/app.jsx:67 src/app.jsx:528
+msgid "User"
+msgstr "用户"
+
+#: src/app.jsx:697
+msgid "User Podman service is also available"
+msgstr "用户 Podman 服务同样可用"
+
+#: src/PodCreateModal.jsx:169 src/ImageRunModal.jsx:783
+#: src/ImageRunModal.jsx:807
+msgid "User:"
+msgstr "用户:"
+
+#: src/Env.jsx:72
+msgid "Value"
+msgstr "值"
+
+#: src/PodCreateModal.jsx:190 src/ContainerIntegration.jsx:110
+#: src/ImageRunModal.jsx:1004
+msgid "Volumes"
+msgstr "卷"
+
+#: src/ImageRunModal.jsx:1130 src/ContainerHealthLogs.jsx:80
+msgid "When unhealthy"
+msgstr "当不健康时"
+
+#: src/ImageRunModal.jsx:880
+msgid "With terminal"
+msgstr "使用终端"
+
+#: src/Volume.jsx:66
+msgid "Writable"
+msgstr "可写入"
+
+#: src/manifest.json:0
+msgid "container"
+msgstr "容器"
+
+#: src/ImageRunModal.jsx:289
+msgid "downloading"
+msgstr "下载"
+
+#: src/ImageRunModal.jsx:820
+msgid "host[:port]/[user]/container[:tag]"
+msgstr "host[:port]/[user]/container[:tag]"
+
+#: src/manifest.json:0
+msgid "image"
+msgstr "镜像"
+
+#: src/ImageSearchModal.jsx:172
+msgid "in"
+msgstr "于"
+
+#: src/ImageDeleteModal.jsx:79
+msgid "intermediate"
+msgstr "中间体"
+
+#: src/ImageDeleteModal.jsx:59
+msgid "intermediate image"
+msgstr "中间镜像"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "n/a"
+msgstr "不适用"
+
+#: src/Containers.jsx:391 src/Containers.jsx:392
+msgid "not available"
+msgstr "不可用"
+
+#: src/Containers.jsx:885
+msgid "pod group"
+msgstr "pod 组"
+
+#: src/manifest.json:0
+msgid "podman"
+msgstr "podman"
+
+#: src/Containers.jsx:570
+msgid "ports"
+msgstr "端口"
+
+#: src/ImageRunModal.jsx:1054 src/ImageRunModal.jsx:1079
+#: src/ImageRunModal.jsx:1104
+msgid "seconds"
+msgstr "秒"
+
+#: src/ImageSearchModal.jsx:160 src/Containers.jsx:428
+#: src/PruneUnusedContainersModal.jsx:28 src/Images.jsx:132
+msgid "system"
+msgstr "系统"
+
+#: src/Images.jsx:83 src/Images.jsx:90
+msgid "unused"
+msgstr "未使用"
+
+#: src/Containers.jsx:428 src/PruneUnusedContainersModal.jsx:28
+#: src/Images.jsx:132
+msgid "user:"
+msgstr "用户:"
+
+#: src/Containers.jsx:585
+msgid "volumes"
+msgstr "卷"
+
+#~ msgid "Delete $0"
+#~ msgstr "删除 $0"
+
+#~ msgid "select all"
+#~ msgstr "全选"
+
+#~ msgid "Restarting"
+#~ msgstr "正在重启"
+
+#~ msgid "Confirm deletion of $0"
+#~ msgstr "确认删除 $0"
+
+#~ msgid "Confirm deletion of pod $0"
+#~ msgstr "确认删除 pod $0"
+
+#~ msgid "Confirm force deletion of pod $0"
+#~ msgstr "确认强制删除 pod $0"
+
+#~ msgid "Confirm forced deletion of $0"
+#~ msgstr "确认强制删除 $0"
+
+#~ msgid "Container is currently running."
+#~ msgstr "容器正在运行。"
+
+#~ msgid "Do not include root file-system changes when exporting"
+#~ msgstr "导出时不包含 root 文件系统更改"
+
+#~ msgid "Default with single selectable"
+#~ msgstr "默认使用单选"
+
+#~ msgid "Start after creation"
+#~ msgstr "创建后启动"
+
+#, fuzzy
+#~| msgid "Delete tagged images"
+#~ msgid "Delete unused $0 images:"
+#~ msgstr "删除标记的镜像"
+
+#~ msgid "created"
+#~ msgstr "创建"
+
+#~ msgid "exited"
+#~ msgstr "退出"
+
+#~ msgid "paused"
+#~ msgstr "已暂停"
+
+#~ msgid "running"
+#~ msgstr "正在运行"
+
+#~ msgid "stopped"
+#~ msgstr "已停止"
+
+#, fuzzy
+#~| msgid "user:"
+#~ msgid "user"
+#~ msgstr "用户:"
+
+#~ msgid "Add on build variable"
+#~ msgstr "添加构建变量"
+
+#~ msgid "Commit image"
+#~ msgstr "提交镜像"
+
+#~ msgid "Format"
+#~ msgstr "格式"
+
+#~ msgid "Message"
+#~ msgstr "消息"
+
+#~ msgid "Pause the container"
+#~ msgstr "暂停该容器"
+
+#~ msgid "Remove on build variable"
+#~ msgstr "删除构建变量"
+
+#~ msgid "Set container on build variables"
+#~ msgstr "设置容器构建时的变量"
+
+#~ msgid "Add item"
+#~ msgstr "添加项目"
+
+#~ msgid "Host port (optional)"
+#~ msgstr "主机端口(可选)"
+
+#~ msgid "IP (optional)"
+#~ msgstr "IP(可选)"
+
+#~ msgid "ReadOnly"
+#~ msgstr "只读"
+
+#~ msgid "IP prefix length"
+#~ msgstr "IP 前缀长度"
+
+#~ msgid "Get new image"
+#~ msgstr "获取新镜像"
+
+#~ msgid "Run"
+#~ msgstr "运行"
+
+#~ msgid "On build"
+#~ msgstr "构建时"
+
+#~ msgid "Are you sure you want to delete this image?"
+#~ msgstr "您确定要删除该镜像吗?"
+
+#~ msgid "Could not attach to this container: $0"
+#~ msgstr "无法附加到该容器:$0"
+
+#~ msgid "Could not open channel: $0"
+#~ msgstr "无法打开通道:$0"
+
+#~ msgid "Everything"
+#~ msgstr "全部"
+
+#~ msgid "Security"
+#~ msgstr "安全性"
+
+#~ msgid "The scan from $time ($type) found no vulnerabilities."
+#~ msgstr "$time ($type) 发起的扫描未发现漏洞。"
+
+#~ msgid "This version of the Web Console does not support a terminal."
+#~ msgstr "该版本的网页控制台不支持终端。"
diff --git a/pyproject.toml b/pyproject.toml
new file mode 100644
index 0000000..bc90c32
--- /dev/null
+++ b/pyproject.toml
@@ -0,0 +1,50 @@
+[tool.ruff]
+exclude = [
+ ".git/",
+ "modules/",
+ "node_modules/",
+]
+line-length = 118
+src = []
+
+[tool.ruff.lint]
+select = [
+ "A", # flake8-builtins
+ "B", # flake8-bugbear
+ "C4", # flake8-comprehensions
+ "D300", # pydocstyle: Forbid ''' in docstrings
+ "DTZ", # flake8-datetimez
+ "E", # pycodestyle
+ "EXE", # flake8-executable
+ "F", # pyflakes
+ "FBT", # flake8-boolean-trap
+ "G", # flake8-logging-format
+ "I", # isort
+ "ICN", # flake8-import-conventions
+ "ISC", # flake8-implicit-str-concat
+ "PLE", # pylint errors
+ "PGH", # pygrep-hooks
+ "RSE", # flake8-raise
+ "RUF", # ruff rules
+ "T10", # flake8-debugger
+ "TCH", # flake8-type-checking
+ "UP032", # f-string
+ "W", # warnings (mostly whitespace)
+ "YTT", # flake8-2020
+]
+ignore = [
+ "FBT002", # Boolean default value in function definition
+ "FBT003", # Boolean positional value in function call
+]
+
+[tool.ruff.lint.flake8-pytest-style]
+fixture-parentheses = false
+mark-parentheses = false
+
+[tool.ruff.lint.isort]
+known-first-party = ["cockpit"]
+
+[tool.vulture]
+ignore_names = [
+ "test[A-Z0-9]*",
+]
diff --git a/src/ContainerCheckpointModal.jsx b/src/ContainerCheckpointModal.jsx
new file mode 100644
index 0000000..2bc5b1c
--- /dev/null
+++ b/src/ContainerCheckpointModal.jsx
@@ -0,0 +1,68 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { Form } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { useDialogs } from "dialogs.jsx";
+import cockpit from 'cockpit';
+
+import * as client from './client.js';
+
+const _ = cockpit.gettext;
+
+const ContainerCheckpointModal = ({ containerWillCheckpoint, onAddNotification }) => {
+ const Dialogs = useDialogs();
+ const [inProgress, setProgress] = useState(false);
+ const [keep, setKeep] = useState(false);
+ const [leaveRunning, setLeaveRunning] = useState(false);
+ const [tcpEstablished, setTcpEstablished] = useState(false);
+
+ const handleCheckpointContainer = () => {
+ setProgress(true);
+ client.postContainer(containerWillCheckpoint.isSystem, "checkpoint", containerWillCheckpoint.Id, {
+ keep,
+ leaveRunning,
+ tcpEstablished,
+ })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to checkpoint container $0"), containerWillCheckpoint.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ setProgress(false);
+ })
+ .finally(() => {
+ Dialogs.close();
+ });
+ };
+
+ return (
+ <Modal isOpen
+ showClose={false}
+ position="top" variant="medium"
+ title={cockpit.format(_("Checkpoint container $0"), containerWillCheckpoint.Name)}
+ footer={<>
+ <Button variant="primary" isDisabled={inProgress}
+ isLoading={inProgress}
+ onClick={handleCheckpointContainer}>
+ {_("Checkpoint")}
+ </Button>
+ <Button variant="link" isDisabled={inProgress}
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ <Form isHorizontal>
+ <Checkbox label={_("Keep all temporary checkpoint files")} id="checkpoint-dialog-keep"
+ name="keep" isChecked={keep} onChange={(_, val) => setKeep(val)} />
+ <Checkbox label={_("Leave running after writing checkpoint to disk")}
+ id="checkpoint-dialog-leaveRunning" name="leaveRunning"
+ isChecked={leaveRunning} onChange={(_, val) => setLeaveRunning(val)} />
+ <Checkbox label={_("Support preserving established TCP connections")}
+ id="checkpoint-dialog-tcpEstablished" name="tcpEstablished"
+ isChecked={tcpEstablished} onChange={(_, val) => setTcpEstablished(val) } />
+ </Form>
+ </Modal>
+ );
+};
+
+export default ContainerCheckpointModal;
diff --git a/src/ContainerCommitModal.jsx b/src/ContainerCommitModal.jsx
new file mode 100644
index 0000000..4784ac8
--- /dev/null
+++ b/src/ContainerCommitModal.jsx
@@ -0,0 +1,166 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import cockpit from 'cockpit';
+
+import { FormHelper } from 'cockpit-components-form-helper.jsx';
+import * as utils from './util.js';
+import * as client from './client.js';
+import { ErrorNotification } from './Notification.jsx';
+import { fmt_to_fragments } from 'utils.jsx';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+const ContainerCommitModal = ({ container, localImages }) => {
+ const Dialogs = useDialogs();
+
+ const [imageName, setImageName] = useState("");
+ const [tag, setTag] = useState("");
+ const [author, setAuthor] = useState("");
+ const [command, setCommand] = useState(utils.quote_cmdline(container.Config.Cmd));
+ const [pause, setPause] = useState(false);
+ const [useDocker, setUseDocker] = useState(false);
+
+ const [dialogError, setDialogError] = useState("");
+ const [dialogErrorDetail, setDialogErrorDetail] = useState("");
+ const [commitInProgress, setCommitInProgress] = useState(false);
+ const [nameError, setNameError] = useState("");
+
+ const handleCommit = (force) => {
+ if (!force && !imageName) {
+ setNameError(_("Image name is required"));
+ return;
+ }
+
+ let full_name = imageName + ":" + (tag !== "" ? tag : "latest");
+ if (full_name.indexOf("/") < 0)
+ full_name = "localhost/" + full_name;
+
+ if (!force && localImages.some(image => image.isSystem === container.isSystem && image.Name === full_name)) {
+ setNameError(_("Image name is not unique"));
+ return;
+ }
+
+ function quote(word) {
+ word = word.replace(/"/g, '\\"');
+ return '"' + word + '"';
+ }
+
+ const commitData = {};
+ commitData.container = container.Id;
+ commitData.repo = imageName;
+ commitData.author = author;
+ commitData.pause = pause;
+
+ if (useDocker)
+ commitData.format = 'docker';
+
+ if (tag)
+ commitData.tag = tag;
+
+ commitData.changes = [];
+ if (command.trim() !== "") {
+ let cmdData = "";
+ const words = utils.unquote_cmdline(command.trim());
+ const cmdStr = words.map(quote).join(", ");
+ cmdData = "CMD [" + cmdStr + "]";
+ commitData.changes.push(cmdData);
+ }
+
+ setCommitInProgress(true);
+ setNameError("");
+ setDialogError("");
+ setDialogErrorDetail("");
+ client.commitContainer(container.isSystem, commitData)
+ .then(() => Dialogs.close())
+ .catch(ex => {
+ setDialogError(cockpit.format(_("Failed to commit container $0"), container.Name));
+ setDialogErrorDetail(cockpit.format("$0: $1", ex.message, ex.reason));
+ setCommitInProgress(false);
+ });
+ };
+
+ const commitContent = (
+ <Form isHorizontal>
+ {dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} onDismiss={() => setDialogError("")} />}
+ <FormGroup fieldId="commit-dialog-image-name" label={_("New image name")}>
+ <TextInput id="commit-dialog-image-name"
+ value={imageName}
+ validated={nameError ? "error" : "default"}
+ onChange={(_, value) => { setNameError(""); setImageName(value) }} />
+ <FormHelper fieldId="commit-dialog-image-name" helperTextInvalid={nameError} />
+ </FormGroup>
+
+ <FormGroup fieldId="commit-dialog-image-tag" label={_("Tag")}>
+ <TextInput id="commit-dialog-image-tag"
+ placeholder="latest" // Do not translate
+ value={tag}
+ onChange={(_, value) => { setNameError(""); setTag(value) }} />
+ </FormGroup>
+
+ <FormGroup fieldId="commit-dialog-author" label={_("Author")}>
+ <TextInput id="commit-dialog-author"
+ placeholder={_("Example, Your Name <yourname@example.com>")}
+ value={author}
+ onChange={(_, value) => setAuthor(value)} />
+ </FormGroup>
+
+ <FormGroup fieldId="commit-dialog-command" label={_("Command")}>
+ <TextInput id="commit-dialog-command"
+ value={command}
+ onChange={(_, value) => setCommand(value)} />
+ </FormGroup>
+
+ <FormGroup fieldId="commit-dialog-pause" label={_("Options")} isStack hasNoPaddingTop>
+ <Checkbox id="commit-dialog-pause"
+ isChecked={pause}
+ onChange={(_, val) => setPause(val)}
+ label={_("Pause container when creating image")} />
+ <Checkbox id="commit-dialog-docker"
+ isChecked={useDocker}
+ onChange={(_, val) => setUseDocker(val)}
+ description={_("Docker format is useful when sharing the image with Docker or Moby Engine")}
+ label={_("Use legacy Docker format")} />
+ </FormGroup>
+ </Form>
+ );
+
+ return (
+ <Modal isOpen
+ showClose={false}
+ position="top" variant="medium"
+ title={_("Commit container")}
+ description={fmt_to_fragments(_("Create a new image based on the current state of the $0 container."), <b>{container.Name}</b>)}
+ footer={<>
+ <Button variant="primary"
+ className="btn-ctr-commit"
+ isLoading={commitInProgress && !nameError}
+ isDisabled={commitInProgress || nameError}
+ onClick={() => handleCommit(false)}>
+ {_("Commit")}
+ </Button>
+ {nameError && <Button variant="warning"
+ className="btn-ctr-commit-force"
+ isLoading={commitInProgress}
+ isDisabled={commitInProgress}
+ onClick={() => handleCommit(true)}>
+ {_("Force commit")}
+ </Button>}
+ <Button variant="link"
+ className="btn-ctr-cancel-commit"
+ isDisabled={commitInProgress}
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ {commitContent}
+ </Modal>
+ );
+};
+
+export default ContainerCommitModal;
diff --git a/src/ContainerDeleteModal.jsx b/src/ContainerDeleteModal.jsx
new file mode 100644
index 0000000..c50068f
--- /dev/null
+++ b/src/ContainerDeleteModal.jsx
@@ -0,0 +1,42 @@
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { useDialogs } from "dialogs.jsx";
+import cockpit from 'cockpit';
+
+import * as client from './client.js';
+
+const _ = cockpit.gettext;
+
+const ContainerDeleteModal = ({ containerWillDelete, onAddNotification }) => {
+ const Dialogs = useDialogs();
+
+ const handleRemoveContainer = () => {
+ const container = containerWillDelete;
+ const id = container ? container.Id : "";
+
+ Dialogs.close();
+ client.delContainer(container.isSystem, id, false)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to remove container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ titleIconVariant="warning"
+ onClose={Dialogs.close}
+ title={cockpit.format(_("Delete $0?"), containerWillDelete.Name)}
+ footer={<>
+ <Button variant="danger" className="btn-ctr-delete" onClick={handleRemoveContainer}>{_("Delete")}</Button>{' '}
+ <Button variant="link" onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ {_("Deleting a container will erase all data in it.")}
+ </Modal>
+ );
+};
+
+export default ContainerDeleteModal;
diff --git a/src/ContainerDetails.jsx b/src/ContainerDetails.jsx
new file mode 100644
index 0000000..c6af977
--- /dev/null
+++ b/src/ContainerDetails.jsx
@@ -0,0 +1,80 @@
+import React from 'react';
+import cockpit from 'cockpit';
+import * as utils from './util.js';
+
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
+
+const _ = cockpit.gettext;
+
+const render_container_state = (container) => {
+ if (container.State.Status === "running") {
+ return cockpit.format(_("Up since $0"), utils.localize_time(Date.parse(container.State.StartedAt) / 1000));
+ }
+ return cockpit.format(_("Exited"));
+};
+
+const ContainerDetails = ({ container }) => {
+ const networkOptions = (
+ [
+ container.NetworkSettings?.IPAddress,
+ container.NetworkSettings?.Gateway,
+ container.NetworkSettings?.MacAddress,
+ ].some(itm => !!itm)
+ );
+
+ return (
+ <Flex>
+ <FlexItem>
+ <DescriptionList className='container-details-basic'>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("ID")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.truncate_id(container.Id)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Image")}</DescriptionListTerm>
+ <DescriptionListDescription>{container.ImageName}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Command")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.quote_cmdline(container.Config?.Cmd)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ </DescriptionList>
+ </FlexItem>
+ <FlexItem>
+ {networkOptions && <DescriptionList columnModifier={{ default: '2Col' }} className='container-details-networking'>
+ {container.NetworkSettings?.IPAddress && <DescriptionListGroup>
+ <DescriptionListTerm>{_("IP address")}</DescriptionListTerm>
+ <DescriptionListDescription>{container.NetworkSettings.IPAddress}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {container.NetworkSettings?.Gateway && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Gateway")}</DescriptionListTerm>
+ <DescriptionListDescription>{container.NetworkSettings.Gateway}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {container.NetworkSettings?.MacAddress && <DescriptionListGroup>
+ <DescriptionListTerm>{_("MAC address")}</DescriptionListTerm>
+ <DescriptionListDescription>{container.NetworkSettings.MacAddress}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ </DescriptionList>}
+ </FlexItem>
+ <FlexItem>
+ <DescriptionList className='container-details-state'>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Created")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.localize_time(Date.parse(container.Created) / 1000)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("State")}</DescriptionListTerm>
+ <DescriptionListDescription>{render_container_state(container)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ {container.State?.Checkpointed && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Latest checkpoint")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.localize_time(Date.parse(container.State.CheckpointedAt) / 1000)}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ </DescriptionList>
+ </FlexItem>
+ </Flex>
+ );
+};
+
+export default ContainerDetails;
diff --git a/src/ContainerHeader.jsx b/src/ContainerHeader.jsx
new file mode 100644
index 0000000..0a1da1a
--- /dev/null
+++ b/src/ContainerHeader.jsx
@@ -0,0 +1,37 @@
+import React from 'react';
+import cockpit from 'cockpit';
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar";
+const _ = cockpit.gettext;
+
+const ContainerHeader = ({ user, twoOwners, ownerFilter, handleOwnerChanged, textFilter, handleFilterChanged }) => {
+ return (
+ <Toolbar className="pf-m-page-insets">
+ <ToolbarContent>
+ { twoOwners &&
+ <>
+ <ToolbarItem variant="label">
+ {_("Owner")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <FormSelect id="containers-containers-owner" value={ownerFilter} onChange={(_, value) => handleOwnerChanged(value)}>
+ <FormSelectOption value='user' label={user} />
+ <FormSelectOption value='system' label={_("System")} />
+ <FormSelectOption value='all' label={_("All")} />
+ </FormSelect>
+ </ToolbarItem>
+ </>
+ }
+ <ToolbarItem>
+ <TextInput id="containers-filter"
+ placeholder={_("Type to filter…")}
+ value={textFilter}
+ onChange={(_, value) => handleFilterChanged(value)} />
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ );
+};
+
+export default ContainerHeader;
diff --git a/src/ContainerHealthLogs.jsx b/src/ContainerHealthLogs.jsx
new file mode 100644
index 0000000..5db9aa0
--- /dev/null
+++ b/src/ContainerHealthLogs.jsx
@@ -0,0 +1,131 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2022 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import cockpit from 'cockpit';
+import * as utils from './util.js';
+import * as client from './client.js';
+
+import { ListingTable } from "cockpit-components-table.jsx";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { CheckCircleIcon, ErrorCircleOIcon } from "@patternfly/react-icons";
+
+const _ = cockpit.gettext;
+
+const format_nanoseconds = (ns) => {
+ const seconds = ns / 1000000000;
+ return cockpit.format(cockpit.ngettext("$0 second", "$0 seconds", seconds), seconds);
+};
+
+const HealthcheckOnFailureActionText = {
+ none: _("No action"),
+ restart: _("Restart"),
+ stop: _("Stop"),
+ kill: _("Force stop"),
+};
+
+const ContainerHealthLogs = ({ container, onAddNotification, state }) => {
+ const healthCheck = container.Config?.Healthcheck ?? container.Config?.Health ?? {}; // not-covered: only on old version
+ const healthState = container.State?.Healthcheck ?? container.State?.Health ?? {}; // not-covered: only on old version
+ const logs = [...(healthState.Log || [])].reverse(); // not-covered: Log should always exist, belt-and-suspenders
+
+ return (
+ <>
+ <Flex alignItems={{ default: "alignItemsFlexStart" }}>
+ <FlexItem grow={{ default: 'grow' }}>
+ <DescriptionList isAutoFit id="container-details-healthcheck">
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Status")}</DescriptionListTerm>
+ <DescriptionListDescription>{state}</DescriptionListDescription>
+ </DescriptionListGroup>
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Command")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.quote_cmdline(healthCheck.Test)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ {healthCheck.Interval && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Interval")}</DescriptionListTerm>
+ <DescriptionListDescription>{format_nanoseconds(healthCheck.Interval)}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {healthCheck.Retries && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Retries")}</DescriptionListTerm>
+ <DescriptionListDescription>{healthCheck.Retries}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {healthCheck.StartPeriod && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Start period")}</DescriptionListTerm>
+ <DescriptionListDescription>{format_nanoseconds(healthCheck.StartPeriod)}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {healthCheck.Timeout && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Timeout")}</DescriptionListTerm>
+ <DescriptionListDescription>{format_nanoseconds(healthCheck.Timeout)}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {container.Config?.HealthcheckOnFailureAction && <DescriptionListGroup>
+ <DescriptionListTerm>{_("When unhealthy")}</DescriptionListTerm>
+ <DescriptionListDescription>{HealthcheckOnFailureActionText[container.Config.HealthcheckOnFailureAction]}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {healthState.FailingStreak && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Failing streak")}</DescriptionListTerm>
+ <DescriptionListDescription>{healthState.FailingStreak}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ </DescriptionList>
+ </FlexItem>
+ { container.State.Status === "running" &&
+ <FlexItem>
+ <Button variant="secondary" onClick={() => {
+ client.runHealthcheck(container.isSystem, container.Id)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to run health check on container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ }}>
+ {_("Run health check")}
+ </Button>
+ </FlexItem>}
+ </Flex>
+ <ListingTable aria-label={_("Logs")}
+ className="health-logs"
+ variant='compact'
+ columns={[_("Last 5 runs"), _("Started at")]}
+ rows={
+ logs.map(log => {
+ const id = "hc" + log.Start + container.Id;
+ return {
+ expandedContent: log.Output ? <pre>{log.Output}</pre> : null,
+ columns: [
+ {
+ title: <Flex flexWrap={{ default: 'nowrap' }} spaceItems={{ default: 'spaceItemsSm' }} alignItems={{ default: 'alignItemsCenter' }}>
+ {log.ExitCode === 0 ? <CheckCircleIcon className="green" /> : <ErrorCircleOIcon className="red" />}
+ <span>{log.ExitCode === 0 ? _("Passed health run") : _("Failed health run")}</span>
+ </Flex>
+ },
+ utils.localize_time(Date.parse(log.Start) / 1000)
+ ],
+ props: {
+ key: id,
+ "data-row-id": id,
+ },
+ };
+ })
+ } />
+ </>
+ );
+};
+
+export default ContainerHealthLogs;
diff --git a/src/ContainerIntegration.jsx b/src/ContainerIntegration.jsx
new file mode 100644
index 0000000..56fbd0d
--- /dev/null
+++ b/src/ContainerIntegration.jsx
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import cockpit from 'cockpit';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+const _ = cockpit.gettext;
+
+// ports is a mapping like { "5000/tcp": [{"HostIp": "", "HostPort": "6000"}] }
+export const renderContainerPublishedPorts = ports => {
+ if (!ports)
+ return null;
+
+ const items = [];
+ Object.entries(ports).forEach(([containerPort, hostBindings]) => {
+ (hostBindings ?? []).forEach(binding => { // not-covered: null was observed in the wild, but unknown how to reproduce
+ items.push(
+ <ListItem key={ containerPort + binding.HostIp + binding.HostPort }>
+ { binding.HostIp || "0.0.0.0" }:{ binding.HostPort } &rarr; { containerPort }
+ </ListItem>
+ );
+ });
+ });
+
+ return <List isPlain>{items}</List>;
+};
+
+export const renderContainerVolumes = (volumes) => {
+ if (!volumes.length)
+ return null;
+
+ const result = volumes.map(volume => {
+ return (
+ <ListItem key={volume.Source + volume.Destination}>
+ {volume.Source}
+ {volume.RW
+ ? <Tooltip content={_("Read-write access")}><span> &harr; </span></Tooltip>
+ : <Tooltip content={_("Read-only access")}><span> &rarr; </span></Tooltip>}
+ {volume.Destination}
+ </ListItem>
+ );
+ });
+
+ return <List isPlain>{result}</List>;
+};
+
+const ContainerEnv = ({ containerEnv, imageEnv }) => {
+ // filter out some Environment variables set by podman or by image
+ const toRemoveEnv = [...imageEnv, 'container=podman'];
+ let toShow = containerEnv.filter(variable => {
+ if (toRemoveEnv.includes(variable)) {
+ return false;
+ }
+
+ return !variable.match(/(HOME|TERM)=.*/);
+ });
+
+ // append filtered out variables to always shown variables when 'show more' is clicked
+ const [showMore, setShowMore] = useState(false);
+ if (showMore)
+ toShow = toShow.concat(containerEnv.filter(variable => !toShow.includes(variable)));
+
+ if (!toShow.length)
+ return null;
+
+ const result = toShow.map(variable => {
+ return (
+ <ListItem key={variable}>
+ {variable}
+ </ListItem>
+ );
+ });
+
+ result.push(
+ <ListItem key='show-more-env-button'>
+ <Button variant='link' isInline
+ onClick={() => setShowMore(!showMore)}>
+ {showMore ? _("Show less") : _("Show more")}
+ </Button>
+ </ListItem>
+ );
+
+ return <List isPlain>{result}</List>;
+};
+
+const ContainerIntegration = ({ container, localImages }) => {
+ if (localImages === null) { // not-covered: not a stable UI state
+ return (
+ <EmptyStatePanel title={_("Loading details...")} loading />
+ );
+ }
+
+ const ports = renderContainerPublishedPorts(container.NetworkSettings.Ports);
+ const volumes = renderContainerVolumes(container.Mounts);
+
+ const image = localImages.filter(img => img.Id === container.Image)[0];
+ const env = <ContainerEnv containerEnv={container.Config.Env} imageEnv={image.Env} />;
+
+ return (
+ <DescriptionList isAutoColumnWidths columnModifier={{ md: '3Col' }} className='container-integration'>
+ {ports && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Ports")}</DescriptionListTerm>
+ <DescriptionListDescription>{ports}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {volumes && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Volumes")}</DescriptionListTerm>
+ <DescriptionListDescription>{volumes}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ {env && <DescriptionListGroup>
+ <DescriptionListTerm>{_("Environment variables")}</DescriptionListTerm>
+ <DescriptionListDescription>{env}</DescriptionListDescription>
+ </DescriptionListGroup>}
+ </DescriptionList>
+ );
+};
+
+export default ContainerIntegration;
diff --git a/src/ContainerLogs.jsx b/src/ContainerLogs.jsx
new file mode 100644
index 0000000..1b31911
--- /dev/null
+++ b/src/ContainerLogs.jsx
@@ -0,0 +1,175 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2020 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import { Terminal } from "xterm";
+import { CanvasAddon } from 'xterm-addon-canvas';
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import cockpit from 'cockpit';
+import rest from './rest.js';
+import * as client from './client.js';
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+import "./ContainerTerminal.css";
+
+const _ = cockpit.gettext;
+
+class ContainerLogs extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onStreamClose = this.onStreamClose.bind(this);
+ this.onStreamMessage = this.onStreamMessage.bind(this);
+ this.connectStream = this.connectStream.bind(this);
+
+ this.view = new Terminal({
+ cols: 80,
+ rows: 24,
+ convertEol: true,
+ cursorBlink: false,
+ disableStdin: true,
+ fontSize: 12,
+ fontFamily: 'Menlo, Monaco, Consolas, monospace',
+ screenReaderMode: true
+ });
+ this.view._core.cursorHidden = true;
+ this.view.write(_("Loading logs..."));
+
+ this.logRef = React.createRef();
+
+ this.state = {
+ opened: false,
+ loading: true,
+ errorMessage: "",
+ streamer: null,
+ };
+ }
+
+ componentDidMount() {
+ this._ismounted = true;
+ this.connectStream();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ // Connect channel when there is none and container started
+ if (!this.state.streamer && this.props.containerStatus === "running" && prevProps.containerStatus !== "running")
+ this.connectStream();
+ if (prevProps.width !== this.props.width) {
+ this.resize(this.props.width);
+ }
+ }
+
+ resize(width) {
+ // 24 PF padding * 4
+ // 3 line border
+ // 21 inner padding of xterm.js
+ // xterm.js scrollbar 20
+ const padding = 24 * 4 + 3 + 21 + 20;
+ const realWidth = this.view._core._renderService.dimensions.css.cell.width;
+ const cols = Math.floor((width - padding) / realWidth);
+ this.view.resize(cols, 24);
+ }
+
+ componentWillUnmount() {
+ this._ismounted = false;
+ if (this.state.streamer)
+ this.state.streamer.close();
+ this.view.dispose();
+ }
+
+ connectStream() {
+ if (this.state.streamer !== null)
+ return;
+
+ // Show the terminal. Once it was shown, do not show it again but reuse the previous one
+ if (!this.state.opened) {
+ this.view.open(this.logRef.current);
+ this.view.loadAddon(new CanvasAddon());
+ this.setState({ opened: true });
+ }
+ this.resize(this.props.width);
+
+ const connection = rest.connect(client.getAddress(this.props.system), this.props.system);
+ const options = {
+ method: "GET",
+ path: client.VERSION + "libpod/containers/" + this.props.containerId + "/logs",
+ body: "",
+ binary: true,
+ params: {
+ follow: true,
+ stdout: true,
+ stderr: true,
+ },
+ };
+
+ connection.monitor(options, this.onStreamMessage, this.props.system, true)
+ .then(this.onStreamClose)
+ .catch(e => {
+ const error = JSON.parse(new TextDecoder().decode(e.message));
+ this.setState({
+ errorMessage: error.message,
+ streamer: null,
+ });
+ });
+ this.setState({
+ streamer: connection,
+ errorMessage: "",
+ });
+ }
+
+ onStreamMessage(data) {
+ if (data) {
+ if (this.state.loading) {
+ this.view.reset();
+ this.view._core.cursorHidden = true;
+ this.setState({ loading: false });
+ }
+ // First 8 bytes encode information about stream and frame
+ // See 'Stream format' on https://docs.docker.com/engine/api/v1.40/#operation/ContainerAttach
+ this.view.writeln(data.slice(8));
+ }
+ }
+
+ onStreamClose() {
+ if (this._ismounted) {
+ this.setState({
+ streamer: null,
+ });
+ this.view.write("Streaming disconnected");
+ }
+ }
+
+ render() {
+ let element = <div className="container-logs" ref={this.logRef} />;
+ if (this.state.errorMessage)
+ element = <EmptyStatePanel icon={ExclamationCircleIcon} title={this.state.errorMessage} />;
+
+ return element;
+ }
+}
+
+ContainerLogs.propTypes = {
+ containerId: PropTypes.string.isRequired,
+ system: PropTypes.bool.isRequired,
+ width: PropTypes.number.isRequired
+};
+
+export default ContainerLogs;
diff --git a/src/ContainerRenameModal.jsx b/src/ContainerRenameModal.jsx
new file mode 100644
index 0000000..d686412
--- /dev/null
+++ b/src/ContainerRenameModal.jsx
@@ -0,0 +1,107 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import cockpit from 'cockpit';
+
+import * as client from './client.js';
+import * as utils from './util.js';
+import { ErrorNotification } from './Notification.jsx';
+import { useDialogs } from "dialogs.jsx";
+import { FormHelper } from 'cockpit-components-form-helper.jsx';
+
+const _ = cockpit.gettext;
+
+const ContainerRenameModal = ({ container, updateContainer }) => {
+ const Dialogs = useDialogs();
+ const [name, setName] = useState(container.Name);
+ const { version } = utils.usePodmanInfo();
+ const [nameError, setNameError] = useState(null);
+ const [dialogError, setDialogError] = useState(null);
+ const [dialogErrorDetail, setDialogErrorDetail] = useState(null);
+
+ const handleInputChange = (targetName, value) => {
+ if (targetName === "name") {
+ setName(value);
+ if (value === "") {
+ setNameError(_("Container name is required."));
+ } else if (utils.is_valid_container_name(value)) {
+ setNameError(null);
+ } else {
+ setNameError(_("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -)."));
+ }
+ }
+ };
+
+ const handleRename = () => {
+ if (!name) {
+ setNameError(_("Container name is required."));
+ return;
+ }
+
+ setNameError(null);
+ setDialogError(null);
+ client.renameContainer(container.isSystem, container.Id, { name })
+ .then(() => {
+ Dialogs.close();
+ // HACK: This is a workaround for missing API rename event in Podman versions less than 4.1.
+ if (version.localeCompare("4.1", undefined, { numeric: true, sensitivity: 'base' }) < 0) {
+ updateContainer(container.Id, container.isSystem); // not-covered: only on old version
+ }
+ })
+ .catch(ex => {
+ setDialogError(cockpit.format(_("Failed to rename container $0"), container.Name)); // not-covered: OS error
+ setDialogErrorDetail(cockpit.format("$0: $1", ex.message, ex.reason));
+ });
+ };
+
+ const handleKeyDown = (event) => {
+ if (event.key === "Enter") {
+ event.preventDefault();
+ handleRename();
+ }
+ };
+
+ const renameContent = (
+ <Form isHorizontal>
+ <FormGroup fieldId="rename-dialog-container-name" label={_("New container name")}>
+ <TextInput id="rename-dialog-container-name"
+ value={name}
+ validated={nameError ? "error" : "default"}
+ type="text"
+ aria-label={nameError}
+ onChange={(_, value) => handleInputChange("name", value)} />
+ <FormHelper fieldId="commit-dialog-image-name" helperTextInvalid={nameError} />
+ </FormGroup>
+ </Form>
+ );
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ onKeyDown={handleKeyDown}
+ title={cockpit.format(_("Rename container $0"), container.Name)}
+ footer={<>
+ <Button variant="primary"
+ className="btn-ctr-rename"
+ id="btn-rename-dialog-container"
+ isDisabled={nameError}
+ onClick={handleRename}>
+ {_("Rename")}
+ </Button>
+ <Button variant="link"
+ className="btn-ctr-cancel-commit"
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ {dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} onDismiss={() => setDialogError(null)} />}
+ {renameContent}
+ </Modal>
+ );
+};
+
+export default ContainerRenameModal;
diff --git a/src/ContainerRestoreModal.jsx b/src/ContainerRestoreModal.jsx
new file mode 100644
index 0000000..c585932
--- /dev/null
+++ b/src/ContainerRestoreModal.jsx
@@ -0,0 +1,74 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { Form } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { useDialogs } from "dialogs.jsx";
+import cockpit from 'cockpit';
+
+import * as client from './client.js';
+
+const _ = cockpit.gettext;
+
+const ContainerRestoreModal = ({ containerWillRestore, onAddNotification }) => {
+ const Dialogs = useDialogs();
+
+ const [inProgress, setInProgress] = useState(false);
+ const [keep, setKeep] = useState(false);
+ const [tcpEstablished, setTcpEstablished] = useState(false);
+ const [ignoreStaticIP, setIgnoreStaticIP] = useState(false);
+ const [ignoreStaticMAC, setIgnoreStaticMAC] = useState(false);
+
+ const handleRestoreContainer = () => {
+ setInProgress(true);
+ client.postContainer(containerWillRestore.isSystem, "restore", containerWillRestore.Id, {
+ keep,
+ tcpEstablished,
+ ignoreStaticIP,
+ ignoreStaticMAC,
+ })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to restore container $0"), containerWillRestore.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ setInProgress(false);
+ })
+ .finally(() => {
+ Dialogs.close();
+ });
+ };
+
+ return (
+ <Modal isOpen
+ showClose={false}
+ position="top" variant="medium"
+ title={cockpit.format(_("Restore container $0"), containerWillRestore.Name)}
+ footer={<>
+ <Button variant="primary" isDisabled={inProgress}
+ isLoading={inProgress}
+ onClick={handleRestoreContainer}>
+ {_("Restore")}
+ </Button>
+ <Button variant="link" isDisabled={inProgress}
+ onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ <Form isHorizontal>
+ <Checkbox label={_("Keep all temporary checkpoint files")} id="restore-dialog-keep" name="keep"
+ isChecked={keep} onChange={(_, val) => setKeep(val)} />
+ <Checkbox label={_("Restore with established TCP connections")}
+ id="restore-dialog-tcpEstablished" name="tcpEstablished"
+ isChecked={tcpEstablished} onChange={(_, val) => setTcpEstablished(val)} />
+ <Checkbox label={_("Ignore IP address if set statically")} id="restore-dialog-ignoreStaticIP"
+ name="ignoreStaticIP" isChecked={ignoreStaticIP}
+ onChange={(_, val) => setIgnoreStaticIP(val)} />
+ <Checkbox label={_("Ignore MAC address if set statically")} id="restore-dialog-ignoreStaticMAC"
+ name="ignoreStaticMAC" isChecked={ignoreStaticMAC}
+ onChange={(_, val) => setIgnoreStaticMAC(val)} />
+ </Form>
+ </Modal>
+ );
+};
+
+export default ContainerRestoreModal;
diff --git a/src/ContainerTerminal.css b/src/ContainerTerminal.css
new file mode 100644
index 0000000..27d2f5b
--- /dev/null
+++ b/src/ContainerTerminal.css
@@ -0,0 +1,7 @@
+@import "xterm/css/xterm.css";
+
+.terminal {
+ /* 5px all around and on right +11 since the scrollbar is 11px wide */
+ padding-block: 5px;
+ padding-inline: 5px 16px;
+}
diff --git a/src/ContainerTerminal.jsx b/src/ContainerTerminal.jsx
new file mode 100644
index 0000000..bd883ca
--- /dev/null
+++ b/src/ContainerTerminal.jsx
@@ -0,0 +1,278 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import PropTypes from 'prop-types';
+import cockpit from 'cockpit';
+import { Terminal } from "xterm";
+import { CanvasAddon } from 'xterm-addon-canvas';
+import { ErrorNotification } from './Notification.jsx';
+
+import * as client from './client.js';
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+
+import "./ContainerTerminal.css";
+
+const _ = cockpit.gettext;
+const decoder = cockpit.utf8_decoder();
+const encoder = cockpit.utf8_encoder();
+
+function sequence_find(seq, find) {
+ let f;
+ const fl = find.length;
+ let s;
+ const sl = (seq.length - fl) + 1;
+ for (s = 0; s < sl; s++) {
+ for (f = 0; f < fl; f++) {
+ if (seq[s + f] !== find[f])
+ break;
+ }
+ if (f == fl)
+ return s;
+ }
+
+ return -1;
+}
+
+class ContainerTerminal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ this.onChannelClose = this.onChannelClose.bind(this);
+ this.onChannelMessage = this.onChannelMessage.bind(this);
+ this.disconnectChannel = this.disconnectChannel.bind(this);
+ this.connectChannel = this.connectChannel.bind(this);
+ this.resize = this.resize.bind(this);
+ this.connectToTty = this.connectToTty.bind(this);
+ this.execAndConnect = this.execAndConnect.bind(this);
+ this.setUpBuffer = this.setUpBuffer.bind(this);
+
+ this.terminalRef = React.createRef();
+
+ this.term = new Terminal({
+ cols: 80,
+ rows: 24,
+ screenKeys: true,
+ cursorBlink: true,
+ fontSize: 12,
+ fontFamily: 'Menlo, Monaco, Consolas, monospace',
+ screenReaderMode: true
+ });
+
+ this.state = {
+ container: props.containerId,
+ sessionId: props.containerId,
+ channel: null,
+ buffer: null,
+ opened: false,
+ errorMessage: "",
+ };
+ }
+
+ componentDidMount() {
+ this.connectChannel();
+ }
+
+ componentDidUpdate(prevProps, prevState) {
+ // Connect channel when there is none and either container started or tty was resolved
+ if (!this.state.channel && (
+ (this.props.containerStatus === "running" && prevProps.containerStatus !== "running") ||
+ (this.props.tty !== undefined && prevProps.tty === undefined)))
+ this.connectChannel();
+ if (prevProps.width !== this.props.width) {
+ this.resize(this.props.width);
+ }
+ }
+
+ resize(width) {
+ // 24 PF padding * 4
+ // 3 line border
+ // 21 inner padding of xterm.js
+ // xterm.js scrollbar 20
+ const padding = 24 * 4 + 3 + 21 + 20;
+ const realWidth = this.term._core._renderService.dimensions.css.cell.width;
+ const cols = Math.floor((width - padding) / realWidth);
+ this.term.resize(cols, 24);
+ client.resizeContainersTTY(this.props.system, this.state.sessionId, this.props.tty, cols, 24)
+ .catch(e => this.setState({ errorMessage: e.message }));
+ }
+
+ connectChannel() {
+ if (this.state.channel)
+ return;
+
+ if (this.props.containerStatus !== "running")
+ return;
+
+ if (this.props.tty === undefined)
+ return;
+
+ if (this.props.tty)
+ this.connectToTty();
+ else
+ this.execAndConnect();
+ }
+
+ setUpBuffer(channel) {
+ const buffer = channel.buffer();
+
+ // Parse the full HTTP response
+ buffer.callback = (data) => {
+ let ret = 0;
+ let pos = 0;
+ let headers = "";
+
+ // Double line break separates header from body
+ pos = sequence_find(data, [13, 10, 13, 10]);
+ if (pos == -1)
+ return ret;
+
+ if (data.subarray)
+ headers = cockpit.utf8_decoder().decode(data.subarray(0, pos));
+ else
+ headers = cockpit.utf8_decoder().decode(data.slice(0, pos));
+
+ const parts = headers.split("\r\n", 1)[0].split(" ");
+ // Check if we got `101` as we expect `HTTP/1.1 101 UPGRADED`
+ if (parts[1] != "101") {
+ console.log(parts.slice(2).join(" "));
+ buffer.callback = null;
+ return;
+ } else if (data.subarray) {
+ data = data.subarray(pos + 4);
+ ret += pos + 4;
+ } else {
+ data = data.slice(pos + 4);
+ ret += pos + 4;
+ }
+ // Set up callback for new incoming messages and if the first response
+ // contained any body, pass it into the callback
+ buffer.callback = this.onChannelMessage;
+ const consumed = this.onChannelMessage(data);
+ return ret + consumed;
+ };
+
+ channel.addEventListener('close', this.onChannelClose);
+
+ // Show the terminal. Once it was shown, do not show it again but reuse the previous one
+ if (!this.state.opened) {
+ this.term.open(this.terminalRef.current);
+ this.term.loadAddon(new CanvasAddon());
+ this.setState({ opened: true });
+
+ this.term.onData((data) => {
+ if (this.state.channel)
+ this.state.channel.send(encoder.encode(data));
+ });
+ }
+ channel.send(String.fromCharCode(12)); // Send SIGWINCH to show prompt on attaching
+
+ return buffer;
+ }
+
+ execAndConnect() {
+ client.execContainer(this.props.system, this.state.container)
+ .then(r => {
+ const channel = cockpit.channel({
+ payload: "stream",
+ unix: client.getAddress(this.props.system),
+ superuser: this.props.system ? "require" : null,
+ binary: true
+ });
+
+ const body = JSON.stringify({ Detach: false, Tty: false });
+ channel.send("POST " + client.VERSION + "libpod/exec/" + encodeURIComponent(r.Id) +
+ "/start HTTP/1.0\r\n" +
+ "Upgrade: WebSocket\r\nConnection: Upgrade\r\nContent-Length: " + body.length + "\r\n\r\n" + body);
+
+ const buffer = this.setUpBuffer(channel);
+ this.setState({ channel, errorMessage: "", buffer, sessionId: r.Id }, () => this.resize(this.props.width));
+ })
+ .catch(e => this.setState({ errorMessage: e.message }));
+ }
+
+ connectToTty() {
+ const channel = cockpit.channel({
+ payload: "stream",
+ unix: client.getAddress(this.props.system),
+ superuser: this.props.system ? "require" : null,
+ binary: true
+ });
+
+ channel.send("POST " + client.VERSION + "libpod/containers/" + encodeURIComponent(this.state.container) +
+ "/attach?&stdin=true&stdout=true&stderr=true HTTP/1.0\r\n" +
+ "Upgrade: WebSocket\r\nConnection: Upgrade\r\nContent-Length: 0\r\n\r\n");
+
+ const buffer = this.setUpBuffer(channel);
+ this.setState({ channel, errorMessage: "", buffer });
+ this.resize(this.props.width);
+ }
+
+ componentWillUnmount() {
+ this.disconnectChannel();
+ if (this.state.channel)
+ this.state.channel.close();
+ this.term.dispose();
+ }
+
+ onChannelMessage(buffer) {
+ if (buffer)
+ this.term.write(decoder.decode(buffer));
+ return buffer.length;
+ }
+
+ onChannelClose(event, options) {
+ this.term.write('\x1b[31m disconnected \x1b[m\r\n');
+ this.disconnectChannel();
+ this.setState({ channel: null });
+ this.term.cursorHidden = true;
+ }
+
+ disconnectChannel() {
+ if (this.state.buffer)
+ this.state.buffer.callback = null; // eslint-disable-line react/no-direct-mutation-state
+ if (this.state.channel) {
+ this.state.channel.removeEventListener('close', this.onChannelClose);
+ }
+ }
+
+ render() {
+ let element = <div className="container-terminal" ref={this.terminalRef} />;
+
+ if (this.props.containerStatus !== "running" && !this.state.opened)
+ element = <EmptyStatePanel title={_("Container is not running")} />;
+
+ return (
+ <>
+ {this.state.errorMessage && <ErrorNotification errorMessage={_("Error occurred while connecting console")} errorDetail={this.state.errorMessage} onDismiss={() => this.setState({ errorMessage: "" })} />}
+ {element}
+ </>
+ );
+ }
+}
+
+ContainerTerminal.propTypes = {
+ containerId: PropTypes.string.isRequired,
+ containerStatus: PropTypes.string.isRequired,
+ width: PropTypes.number.isRequired,
+ system: PropTypes.bool.isRequired,
+ tty: PropTypes.bool,
+};
+
+export default ContainerTerminal;
diff --git a/src/Containers.jsx b/src/Containers.jsx
new file mode 100644
index 0000000..b18f406
--- /dev/null
+++ b/src/Containers.jsx
@@ -0,0 +1,877 @@
+import React from 'react';
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Card, CardBody, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card";
+import { Divider } from "@patternfly/react-core/dist/esm/components/Divider";
+import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
+import { LabelGroup } from "@patternfly/react-core/dist/esm/components/Label";
+import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Tooltip } from "@patternfly/react-core/dist/esm/components/Tooltip";
+import { Toolbar, ToolbarContent, ToolbarItem } from "@patternfly/react-core/dist/esm/components/Toolbar";
+import { cellWidth, SortByDirection } from '@patternfly/react-table';
+import { MicrochipIcon, MemoryIcon, PortIcon, VolumeIcon, } from '@patternfly/react-icons';
+
+import cockpit from 'cockpit';
+import { ListingTable } from "cockpit-components-table.jsx";
+import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
+import ContainerDetails from './ContainerDetails.jsx';
+import ContainerIntegration, { renderContainerPublishedPorts, renderContainerVolumes } from './ContainerIntegration.jsx';
+import ContainerTerminal from './ContainerTerminal.jsx';
+import ContainerLogs from './ContainerLogs.jsx';
+import ContainerHealthLogs from './ContainerHealthLogs.jsx';
+import ContainerDeleteModal from './ContainerDeleteModal.jsx';
+import ContainerCheckpointModal from './ContainerCheckpointModal.jsx';
+import ContainerRestoreModal from './ContainerRestoreModal.jsx';
+import ForceRemoveModal from './ForceRemoveModal.jsx';
+import * as utils from './util.js';
+import * as client from './client.js';
+import ContainerCommitModal from './ContainerCommitModal.jsx';
+import ContainerRenameModal from './ContainerRenameModal.jsx';
+import { useDialogs, DialogsContext } from "dialogs.jsx";
+
+import './Containers.scss';
+import '@patternfly/patternfly/utilities/Accessibility/accessibility.css';
+import { ImageRunModal } from './ImageRunModal.jsx';
+import { PodActions } from './PodActions.jsx';
+import { PodCreateModal } from './PodCreateModal.jsx';
+import PruneUnusedContainersModal from './PruneUnusedContainersModal.jsx';
+
+import { KebabDropdown } from "cockpit-components-dropdown.jsx";
+
+const _ = cockpit.gettext;
+
+const ContainerActions = ({ container, healthcheck, onAddNotification, localImages, updateContainer }) => {
+ const Dialogs = useDialogs();
+ const { version } = utils.usePodmanInfo();
+ const isRunning = container.State.Status == "running";
+ const isPaused = container.State.Status === "paused";
+
+ const deleteContainer = (event) => {
+ if (container.State.Status == "running") {
+ const handleForceRemoveContainer = () => {
+ const id = container ? container.Id : "";
+
+ return client.delContainer(container.isSystem, id, true)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to force remove container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ throw ex;
+ })
+ .finally(() => {
+ Dialogs.close();
+ });
+ };
+
+ Dialogs.show(<ForceRemoveModal name={container.Name}
+ handleForceRemove={handleForceRemoveContainer}
+ reason={_("Deleting a running container will erase all data in it.")} />);
+ } else {
+ Dialogs.show(<ContainerDeleteModal containerWillDelete={container}
+ onAddNotification={onAddNotification} />);
+ }
+ };
+
+ const stopContainer = (force) => {
+ const args = {};
+
+ if (force)
+ args.t = 0;
+ client.postContainer(container.isSystem, "stop", container.Id, args)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to stop container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const startContainer = () => {
+ client.postContainer(container.isSystem, "start", container.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to start container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const resumeContainer = () => {
+ client.postContainer(container.isSystem, "unpause", container.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to resume container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const pauseContainer = () => {
+ client.postContainer(container.isSystem, "pause", container.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to pause container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const commitContainer = () => {
+ Dialogs.show(<ContainerCommitModal container={container}
+ localImages={localImages} />);
+ };
+
+ const runHealthcheck = () => {
+ client.runHealthcheck(container.isSystem, container.Id)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to run health check on container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const restartContainer = (force) => {
+ const args = {};
+
+ if (force)
+ args.t = 0;
+ client.postContainer(container.isSystem, "restart", container.Id, args)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to restart container $0"), container.Name); // not-covered: OS error
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ };
+
+ const renameContainer = () => {
+ if (container.State.Status !== "running" ||
+ version.localeCompare("3.0.1", undefined, { numeric: true, sensitivity: 'base' }) >= 0) {
+ Dialogs.show(<ContainerRenameModal container={container}
+ updateContainer={updateContainer} />);
+ }
+ };
+
+ const checkpointContainer = () => {
+ Dialogs.show(<ContainerCheckpointModal containerWillCheckpoint={container}
+ onAddNotification={onAddNotification} />);
+ };
+
+ const restoreContainer = () => {
+ Dialogs.show(<ContainerRestoreModal containerWillRestore={container}
+ onAddNotification={onAddNotification} />);
+ };
+
+ const addRenameAction = () => {
+ actions.push(
+ <DropdownItem key="rename"
+ onClick={() => renameContainer()}>
+ {_("Rename")}
+ </DropdownItem>
+ );
+ };
+
+ const actions = [];
+ if (isRunning || isPaused) {
+ actions.push(
+ <DropdownItem key="stop"
+ onClick={() => stopContainer()}>
+ {_("Stop")}
+ </DropdownItem>,
+ <DropdownItem key="force-stop"
+ onClick={() => stopContainer(true)}>
+ {_("Force stop")}
+ </DropdownItem>,
+ <DropdownItem key="restart"
+ onClick={() => restartContainer()}>
+ {_("Restart")}
+ </DropdownItem>,
+ <DropdownItem key="force-restart"
+ onClick={() => restartContainer(true)}>
+ {_("Force restart")}
+ </DropdownItem>
+ );
+
+ if (!isPaused) {
+ actions.push(
+ <DropdownItem key="pause"
+ onClick={() => pauseContainer()}>
+ {_("Pause")}
+ </DropdownItem>
+ );
+ } else {
+ actions.push(
+ <DropdownItem key="resume"
+ onClick={() => resumeContainer()}>
+ {_("Resume")}
+ </DropdownItem>
+ );
+ }
+
+ if (container.isSystem && !isPaused) {
+ actions.push(
+ <Divider key="separator-0" />,
+ <DropdownItem key="checkpoint"
+ onClick={() => checkpointContainer()}>
+ {_("Checkpoint")}
+ </DropdownItem>
+ );
+ }
+ }
+
+ if (!isRunning && !isPaused) {
+ actions.push(
+ <DropdownItem key="start"
+ onClick={() => startContainer()}>
+ {_("Start")}
+ </DropdownItem>
+ );
+ if (version.localeCompare("3", undefined, { numeric: true, sensitivity: 'base' }) >= 0) {
+ addRenameAction();
+ }
+ if (container.isSystem && container.State?.CheckpointPath) {
+ actions.push(
+ <Divider key="separator-0" />,
+ <DropdownItem key="restore"
+ onClick={() => restoreContainer()}>
+ {_("Restore")}
+ </DropdownItem>
+ );
+ }
+ } else { // running or paused
+ if (version.localeCompare("3.0.1", undefined, { numeric: true, sensitivity: 'base' }) >= 0) {
+ addRenameAction();
+ }
+ }
+
+ actions.push(<Divider key="separator-1" />);
+ actions.push(
+ <DropdownItem key="commit"
+ onClick={() => commitContainer()}>
+ {_("Commit")}
+ </DropdownItem>
+ );
+
+ if (isRunning && healthcheck !== "") {
+ actions.push(<Divider key="separator-1-1" />);
+ actions.push(
+ <DropdownItem key="healthcheck"
+ onClick={() => runHealthcheck()}>
+ {_("Run health check")}
+ </DropdownItem>
+ );
+ }
+
+ actions.push(<Divider key="separator-2" />);
+ actions.push(
+ <DropdownItem key="delete"
+ className="pf-m-danger"
+ onClick={deleteContainer}>
+ {_("Delete")}
+ </DropdownItem>
+ );
+
+ return <KebabDropdown position="right" dropdownItems={actions} />;
+};
+
+export let onDownloadContainer = function funcOnDownloadContainer(container) {
+ this.setState(prevState => ({
+ downloadingContainers: [...prevState.downloadingContainers, container]
+ }));
+};
+
+export let onDownloadContainerFinished = function funcOnDownloadContainerFinished(container) {
+ this.setState(prevState => ({
+ downloadingContainers: prevState.downloadingContainers.filter(entry => entry.name !== container.name),
+ }));
+};
+
+const localize_health = (state) => {
+ if (state === "healthy")
+ return _("Healthy");
+ else if (state === "unhealthy")
+ return _("Unhealthy");
+ else if (state === "starting")
+ return _("Checking health");
+ else
+ console.error("Unexpected health check status", state);
+ return null;
+};
+
+const ContainerOverActions = ({ handlePruneUnusedContainers, unusedContainers }) => {
+ const actions = [
+ <DropdownItem key="prune-unused-containers"
+ id="prune-unused-containers-button"
+ component="button"
+ className="pf-m-danger btn-delete"
+ onClick={() => handlePruneUnusedContainers()}
+ isDisabled={unusedContainers.length === 0}>
+ {_("Prune unused containers")}
+ </DropdownItem>,
+ ];
+
+ return <KebabDropdown toggleButtonId="containers-actions-dropdown" position="right" dropdownItems={actions} />;
+};
+
+class Containers extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ width: 0,
+ downloadingContainers: [],
+ showPruneUnusedContainersModal: false,
+ };
+ this.renderRow = this.renderRow.bind(this);
+ this.onWindowResize = this.onWindowResize.bind(this);
+ this.podStats = this.podStats.bind(this);
+
+ this.cardRef = React.createRef();
+
+ onDownloadContainer = onDownloadContainer.bind(this);
+ onDownloadContainerFinished = onDownloadContainerFinished.bind(this);
+
+ window.addEventListener('resize', this.onWindowResize);
+ }
+
+ componentDidMount() {
+ this.onWindowResize();
+ }
+
+ componentWillUnmount() {
+ window.removeEventListener('resize', this.onWindowResize);
+ }
+
+ renderRow(containersStats, container, localImages) {
+ const containerStats = containersStats[container.Id + container.isSystem.toString()];
+ const image = container.ImageName;
+ const isToolboxContainer = container.Config?.Labels?.["com.github.containers.toolbox"] === "true";
+ const isDistroboxContainer = container.Config?.Labels?.manager === "distrobox";
+ let localized_health = null;
+
+ // this needs to get along with stub containers from image run dialog, where most properties don't exist yet
+ // HACK: Podman renamed `Healthcheck` to `Health` randomly
+ // https://github.com/containers/podman/commit/119973375
+ const healthcheck = container.State?.Health?.Status ?? container.State?.Healthcheck?.Status; // not-covered: only on old version
+ const status = container.State?.Status ?? ""; // not-covered: race condition
+
+ let proc = "";
+ let mem = "";
+ if (this.props.cgroupVersion == 'v1' && !container.isSystem && status == 'running') { // not-covered: only on old version
+ proc = <div><abbr title={_("not available")}>{_("n/a")}</abbr></div>;
+ mem = <div><abbr title={_("not available")}>{_("n/a")}</abbr></div>;
+ }
+ if (containerStats && status === "running") {
+ if (containerStats.CPU != undefined)
+ proc = containerStats.CPU.toFixed(2) + "%";
+ if (containerStats.MemUsage != undefined && containerStats.MemLimit != undefined)
+ mem = utils.format_memory_and_limit(containerStats.MemUsage, containerStats.MemLimit);
+ }
+ const info_block = (
+ <div className="container-block">
+ <Flex alignItems={{ default: 'alignItemsCenter' }}>
+ <span className="container-name">{container.Name}</span>
+ {isToolboxContainer && <Badge className='ct-badge-toolbox'>toolbox</Badge>}
+ {isDistroboxContainer && <Badge className='ct-badge-distrobox'>distrobox</Badge>}
+ </Flex>
+ <small>{image}</small>
+ <small>{utils.quote_cmdline(container.Config?.Cmd)}</small>
+ </div>
+ );
+
+ let containerStateClass = "ct-badge-container-" + status.toLowerCase();
+ if (container.isDownloading)
+ containerStateClass += " downloading";
+
+ const containerState = status.charAt(0).toUpperCase() + status.slice(1);
+
+ const state = [<Badge key={containerState} isRead className={containerStateClass}>{_(containerState)}</Badge>]; // States are defined in util.js
+ if (healthcheck) {
+ localized_health = localize_health(healthcheck);
+ if (localized_health)
+ state.push(<Badge key={healthcheck} isRead className={"ct-badge-container-" + healthcheck}>{localized_health}</Badge>);
+ }
+
+ const columns = [
+ { title: info_block, sortKey: container.Name ?? container.Id },
+ {
+ title: container.isSystem ? _("system") : <div><span className="ct-grey-text">{_("user:")} </span>{this.props.user}</div>,
+ props: { modifier: "nowrap" },
+ sortKey: container.isSystem.toString()
+ },
+ { title: proc, props: { modifier: "nowrap" }, sortKey: containerState === "Running" ? containerStats?.CPU ?? -1 : -1 },
+ { title: mem, props: { modifier: "nowrap" }, sortKey: containerStats?.MemUsage ?? -1 },
+ { title: <LabelGroup isVertical>{state}</LabelGroup>, sortKey: containerState },
+ ];
+
+ if (!container.isDownloading) {
+ columns.push({
+ title: <ContainerActions container={container}
+ healthcheck={healthcheck}
+ onAddNotification={this.props.onAddNotification}
+ localImages={localImages}
+ updateContainer={this.props.updateContainer} />,
+ props: { className: "pf-v5-c-table__action" }
+ });
+ }
+
+ const tty = !!container.Config?.Tty;
+
+ const tabs = [];
+ if (container.State) {
+ tabs.push({
+ name: _("Details"),
+ renderer: ContainerDetails,
+ data: { container }
+ });
+
+ if (!container.isDownloading) {
+ tabs.push({
+ name: _("Integration"),
+ renderer: ContainerIntegration,
+ data: { container, localImages }
+ });
+ tabs.push({
+ name: _("Logs"),
+ renderer: ContainerLogs,
+ data: { containerId: container.Id, containerStatus: container.State.Status, width: this.state.width, system: container.isSystem }
+ });
+ tabs.push({
+ name: _("Console"),
+ renderer: ContainerTerminal,
+ data: { containerId: container.Id, containerStatus: container.State.Status, width: this.state.width, system: container.isSystem, tty }
+ });
+ }
+ }
+
+ if (healthcheck) {
+ tabs.push({
+ name: _("Health check"),
+ renderer: ContainerHealthLogs,
+ data: { container, onAddNotification: this.props.onAddNotification, state: localized_health }
+ });
+ }
+
+ return {
+ expandedContent: <ListingPanel colSpan='4' tabRenderers={tabs} />,
+ columns,
+ initiallyExpanded: document.location.hash.substr(1) === container.Id,
+ props: {
+ key: container.Id + container.isSystem.toString(),
+ "data-row-id": container.Id + container.isSystem.toString(),
+ "data-started-at": container.State?.StartedAt,
+ },
+ };
+ }
+
+ onWindowResize() {
+ this.setState({ width: this.cardRef.current.clientWidth });
+ }
+
+ podStats(pod) {
+ const { containersStats } = this.props;
+ // when no containers exists pod.Containers is null
+ if (!containersStats || !pod.Containers) {
+ return null;
+ }
+
+ // As podman does not provide per pod memory/cpu statistics we do the following:
+ // - don't add up CPU usage, instead display the highest found CPU usage of the containers in a pod
+ // - add up memory usage so it displays the total memory of the pod.
+ let cpu = 0;
+ let mem = 0;
+ for (const container of pod.Containers) {
+ const containerStats = containersStats[container.Id + pod.isSystem.toString()];
+ if (!containerStats)
+ continue;
+
+ if (containerStats.CPU != undefined) {
+ const val = containerStats.CPU === 0 ? containerStats.CPU : containerStats.CPU.toFixed(2);
+ if (val > cpu)
+ cpu = val;
+ }
+ if (containerStats.MemUsage != undefined)
+ mem += containerStats.MemUsage;
+ }
+
+ return {
+ cpu,
+ mem,
+ };
+ }
+
+ renderPodDetails(pod, podStatus) {
+ const podStats = this.podStats(pod);
+ const infraContainer = this.props.containers[pod.InfraId + pod.isSystem.toString()];
+ const numPorts = Object.keys(infraContainer?.NetworkSettings?.Ports ?? {}).length;
+
+ return (
+ <>
+ {podStats && podStatus === "Running" &&
+ <>
+ <Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
+ <Tooltip content={_("CPU")}>
+ <MicrochipIcon />
+ </Tooltip>
+ <Text component={TextVariants.p} className="pf-v5-u-hidden-on-sm">{_("CPU")}</Text>
+ <Text component={TextVariants.p} className="pod-cpu">{podStats.cpu}%</Text>
+ </Flex>
+ <Flex className='pod-stat' spaceItems={{ default: 'spaceItemsSm' }}>
+ <Tooltip content={_("Memory")}>
+ <MemoryIcon />
+ </Tooltip>
+ <Text component={TextVariants.p} className="pf-v5-u-hidden-on-sm">{_("Memory")}</Text>
+ <Text component={TextVariants.p} className="pod-memory">{utils.format_memory_and_limit(podStats.mem) || "0 KB"}</Text>
+ </Flex>
+ </>
+ }
+ {infraContainer &&
+ <>
+ {numPorts > 0 &&
+ <Tooltip content={_("Click to see published ports")}>
+ <Popover
+ enableFlip
+ bodyContent={renderContainerPublishedPorts(infraContainer.NetworkSettings.Ports)}
+ >
+ <Button size="sm" variant="link" className="pod-details-button pod-details-ports-btn"
+ icon={<PortIcon className="pod-details-button-color" />}
+ >
+ {numPorts}
+ <Text component={TextVariants.p} className="pf-v5-u-hidden-on-sm">{_("ports")}</Text>
+ </Button>
+ </Popover>
+ </Tooltip>
+ }
+ {infraContainer.Mounts && infraContainer.Mounts.length !== 0 &&
+ <Tooltip content={_("Click to see volumes")}>
+ <Popover
+ enableFlip
+ bodyContent={renderContainerVolumes(infraContainer.Mounts)}
+ >
+ <Button size="sm" variant="link" className="pod-details-button pod-details-volumes-btn"
+ icon={<VolumeIcon className="pod-details-button-color" />}
+ >
+ {infraContainer.Mounts.length}
+ <Text component={TextVariants.p} className="pf-v5-u-hidden-on-sm">{_("volumes")}</Text>
+ </Button>
+ </Popover>
+ </Tooltip>
+ }
+ </>
+ }
+ </>
+ );
+ }
+
+ onOpenPruneUnusedContainersDialog = () => {
+ this.setState({ showPruneUnusedContainersModal: true });
+ };
+
+ render() {
+ const Dialogs = this.context;
+ const columnTitles = [
+ { title: _("Container"), transforms: [cellWidth(20)], sortable: true },
+ { title: _("Owner"), sortable: true },
+ { title: _("CPU"), sortable: true },
+ { title: _("Memory"), sortable: true },
+ { title: _("State"), sortable: true },
+ ''
+ ];
+ const partitionedContainers = { 'no-pod': [] };
+ let filtered = [];
+ const unusedContainers = [];
+
+ let emptyCaption = _("No containers");
+ const emptyCaptionPod = _("No containers in this pod");
+ if (this.props.containers === null || this.props.pods === null)
+ emptyCaption = _("Loading...");
+ else if (this.props.textFilter.length > 0)
+ emptyCaption = _("No containers that match the current filter");
+ else if (this.props.filter == "running")
+ emptyCaption = _("No running containers");
+
+ if (this.props.containers !== null && this.props.pods !== null) {
+ filtered = Object.keys(this.props.containers).filter(id => !(this.props.filter == "running") || ["running", "restarting"].includes(this.props.containers[id].State.Status));
+
+ if (this.props.userServiceAvailable && this.props.systemServiceAvailable && this.props.ownerFilter !== "all") {
+ filtered = filtered.filter(id => {
+ if (this.props.ownerFilter === "system" && !this.props.containers[id].isSystem)
+ return false;
+ if (this.props.ownerFilter !== "system" && this.props.containers[id].isSystem)
+ return false;
+ return true;
+ });
+ }
+
+ if (this.props.textFilter.length > 0) {
+ const lcf = this.props.textFilter.toLowerCase();
+ filtered = filtered.filter(id => this.props.containers[id].Name.toLowerCase().indexOf(lcf) >= 0 ||
+ (this.props.containers[id].Pod &&
+ this.props.pods[this.props.containers[id].Pod + this.props.containers[id].isSystem.toString()].Name.toLowerCase().indexOf(lcf) >= 0) ||
+ this.props.containers[id].ImageName.toLowerCase().indexOf(lcf) >= 0
+ );
+ }
+
+ // Remove infra containers
+ filtered = filtered.filter(id => !this.props.containers[id].IsInfra);
+
+ const getHealth = id => {
+ const state = this.props.containers[id]?.State;
+ return state?.Health?.Status || state?.Healthcheck?.Status;
+ };
+
+ filtered.sort((a, b) => {
+ // Show unhealthy containers first
+ const a_health = getHealth(a);
+ const b_health = getHealth(b);
+ if (a_health !== b_health) {
+ if (a_health === "unhealthy")
+ return -1;
+ if (b_health === "unhealthy")
+ return 1;
+ }
+ // User containers are in front of system ones
+ if (this.props.containers[a].isSystem !== this.props.containers[b].isSystem)
+ return this.props.containers[a].isSystem ? 1 : -1;
+ return this.props.containers[a].Name > this.props.containers[b].Name ? 1 : -1;
+ });
+
+ Object.keys(this.props.pods || {}).forEach(pod => { partitionedContainers[pod] = [] });
+
+ filtered.forEach(id => {
+ const container = this.props.containers[id];
+ if (container)
+ (partitionedContainers[container.Pod ? (container.Pod + container.isSystem.toString()) : 'no-pod'] || []).push(container);
+ });
+
+ // Append downloading containers
+ this.state.downloadingContainers.forEach(cont => {
+ partitionedContainers['no-pod'].push(cont);
+ });
+
+ // Apply filters to pods
+ Object.keys(partitionedContainers).forEach(section => {
+ const lcf = this.props.textFilter.toLowerCase();
+ if (section != "no-pod") {
+ const pod = this.props.pods[section];
+ if ((this.props.filter == "running" && pod.Status != "Running") ||
+ // If nor the pod name nor any container inside the pod fit the filter, hide the whole pod
+ (!partitionedContainers[section].length && pod.Name.toLowerCase().indexOf(lcf) < 0) ||
+ ((this.props.userServiceAvailable && this.props.systemServiceAvailable && this.props.ownerFilter !== "all") &&
+ ((this.props.ownerFilter === "system" && !pod.isSystem) ||
+ (this.props.ownerFilter !== "system" && pod.isSystem))))
+ delete partitionedContainers[section];
+ }
+ });
+ // If there are pods to show and the generic container list is empty don't show it at all
+ if (Object.keys(partitionedContainers).length > 1 && !partitionedContainers["no-pod"].length)
+ delete partitionedContainers["no-pod"];
+
+ const prune_states = ["created", "configured", "stopped", "exited"];
+ for (const containerid of Object.keys(this.props.containers)) {
+ const container = this.props.containers[containerid];
+ // Ignore pods and running containers
+ if (!prune_states.includes(container.State.Status) || container.Pod)
+ continue;
+
+ unusedContainers.push({
+ id: container.Id + container.isSystem.toString(),
+ name: container.Name,
+ created: container.Created,
+ system: container.isSystem,
+ });
+ }
+ }
+
+ // Convert to the search result output
+ let localImages = null;
+ if (this.props.images) {
+ localImages = Object.keys(this.props.images).reduce((images, id) => {
+ const img = this.props.images[id];
+ if (img.RepoTags) {
+ img.Index = img.RepoTags[0].split('/')[0];
+ img.Name = img.RepoTags[0];
+ img.toString = function imgToString() { return this.Name };
+ images.push(img);
+ }
+ return images;
+ }, []);
+ }
+
+ const createContainer = (inPod) => {
+ if (localImages)
+ Dialogs.show(
+ <utils.PodmanInfoContext.Consumer>
+ {(podmanInfo) => (
+ <DialogsContext.Consumer>
+ {(Dialogs) => (
+ <ImageRunModal user={this.props.user}
+ localImages={localImages}
+ pod={inPod}
+ systemServiceAvailable={this.props.systemServiceAvailable}
+ userServiceAvailable={this.props.userServiceAvailable}
+ onAddNotification={this.props.onAddNotification}
+ podmanInfo={podmanInfo}
+ dialogs={Dialogs} />
+ )}
+ </DialogsContext.Consumer>
+ )}
+ </utils.PodmanInfoContext.Consumer>);
+ };
+
+ const createPod = () => {
+ Dialogs.show(<PodCreateModal
+ systemServiceAvailable={this.props.systemServiceAvailable}
+ userServiceAvailable={this.props.userServiceAvailable}
+ user={this.props.user}
+ onAddNotification={this.props.onAddNotification} />);
+ };
+
+ const filterRunning = (
+ <Toolbar>
+ <ToolbarContent className="containers-containers-toolbarcontent">
+ <ToolbarItem variant="label" htmlFor="containers-containers-filter">
+ {_("Show")}
+ </ToolbarItem>
+ <ToolbarItem>
+ <FormSelect id="containers-containers-filter" value={this.props.filter} onChange={(_, value) => this.props.handleFilterChange(value)}>
+ <FormSelectOption value='all' label={_("All")} />
+ <FormSelectOption value='running' label={_("Only running")} />
+ </FormSelect>
+ </ToolbarItem>
+ <Divider orientation={{ default: "vertical" }} />
+ <ToolbarItem>
+ <Button variant="secondary" key="create-new-pod-action"
+ id="containers-containers-create-pod-btn"
+ onClick={() => createPod()}>
+ {_("Create pod")}
+ </Button>
+ </ToolbarItem>
+ <ToolbarItem>
+ <Button variant="primary" key="get-new-image-action"
+ id="containers-containers-create-container-btn"
+ isDisabled={localImages === null}
+ onClick={() => createContainer(null)}>
+ {_("Create container")}
+ </Button>
+ </ToolbarItem>
+ <ToolbarItem>
+ <ContainerOverActions unusedContainers={unusedContainers} handlePruneUnusedContainers={this.onOpenPruneUnusedContainersDialog} />
+ </ToolbarItem>
+ </ToolbarContent>
+ </Toolbar>
+ );
+
+ const sortRows = (rows, direction, idx) => {
+ // CPU / Memory /States
+ const isNumeric = idx == 2 || idx == 3 || idx == 4;
+ const stateOrderMapping = {};
+ utils.states.forEach((elem, index) => {
+ stateOrderMapping[elem] = index;
+ });
+ const sortedRows = rows.sort((a, b) => {
+ let aitem = a.columns[idx].sortKey ?? a.columns[idx].title;
+ let bitem = b.columns[idx].sortKey ?? b.columns[idx].title;
+ // Sort the states based on the order defined in utils. so Running first.
+ if (idx === 4) {
+ aitem = stateOrderMapping[aitem];
+ bitem = stateOrderMapping[bitem];
+ }
+ if (isNumeric) {
+ return bitem - aitem;
+ } else {
+ return aitem.localeCompare(bitem);
+ }
+ });
+ return direction === SortByDirection.asc ? sortedRows : sortedRows.reverse();
+ };
+
+ const card = (
+ <Card id="containers-containers" className="containers-containers" isClickable isSelectable>
+ <CardHeader actions={{ actions: filterRunning }}>
+ <CardTitle><Text component={TextVariants.h2}>{_("Containers")}</Text></CardTitle>
+ </CardHeader>
+ <CardBody>
+ <Flex direction={{ default: 'column' }}>
+ {(this.props.containers === null || this.props.pods === null)
+ ? <ListingTable variant='compact'
+ aria-label={_("Containers")}
+ emptyCaption={emptyCaption}
+ columns={columnTitles}
+ sortMethod={sortRows}
+ rows={[]}
+ sortBy={{ index: 0, direction: SortByDirection.asc }} />
+ : Object.keys(partitionedContainers)
+ .sort((a, b) => {
+ if (a == "no-pod") return -1;
+ else if (b == "no-pod") return 1;
+
+ // User pods are in front of system ones
+ if (this.props.pods[a].isSystem !== this.props.pods[b].isSystem)
+ return this.props.pods[a].isSystem ? 1 : -1;
+ return this.props.pods[a].Name > this.props.pods[b].Name ? 1 : -1;
+ })
+ .map(section => {
+ const tableProps = {};
+ const rows = partitionedContainers[section].map(container => {
+ return this.renderRow(this.props.containersStats, container,
+ localImages);
+ });
+ let caption;
+ let podStatus;
+ if (section !== 'no-pod') {
+ const pod = this.props.pods[section];
+ tableProps['aria-label'] = cockpit.format("Containers of pod $0", pod.Name);
+ podStatus = pod.Status;
+ caption = pod.Name;
+ } else {
+ tableProps['aria-label'] = _("Containers");
+ }
+
+ const actions = caption && (
+ <>
+ <Badge isRead className={"ct-badge-pod-" + podStatus.toLowerCase()}>{_(podStatus)}</Badge>
+ <Button variant="secondary"
+ className="create-container-in-pod"
+ isDisabled={localImages === null}
+ onClick={() => createContainer(this.props.pods[section])}>
+ {_("Create container in pod")}
+ </Button>
+ <PodActions onAddNotification={this.props.onAddNotification} pod={this.props.pods[section]} />
+ </>
+ );
+ return (
+ <Card key={'table-' + section}
+ id={'table-' + (section == "no-pod" ? section : this.props.pods[section].Name)}
+ isPlain={section == "no-pod"}
+ isFlat={section != "no-pod"}
+ className="container-pod"
+ isClickable
+ isSelectable>
+ {caption && <CardHeader actions={{ actions, className: "panel-actions" }}>
+ <CardTitle>
+ <Flex justifyContent={{ default: 'justifyContentFlexStart' }}>
+ <h3 className='pod-name'>{caption}</h3>
+ <span>{_("pod group")}</span>
+ {this.renderPodDetails(this.props.pods[section], podStatus)}
+ </Flex>
+ </CardTitle>
+ </CardHeader>}
+ <ListingTable variant='compact'
+ emptyCaption={section == "no-pod" ? emptyCaption : emptyCaptionPod}
+ columns={columnTitles}
+ sortMethod={sortRows}
+ rows={rows}
+ {...tableProps} />
+ </Card>
+ );
+ })}
+ </Flex>
+ {this.state.showPruneUnusedContainersModal &&
+ <PruneUnusedContainersModal
+ close={() => this.setState({ showPruneUnusedContainersModal: false })}
+ unusedContainers={unusedContainers}
+ onAddNotification={this.props.onAddNotification}
+ userSystemServiceAvailable={this.props.userServiceAvailable && this.props.systemServiceAvailable}
+ user={this.props.user} /> }
+ </CardBody>
+ </Card>
+ );
+
+ return <div ref={this.cardRef}>{card}</div>;
+ }
+}
+
+export default Containers;
diff --git a/src/Containers.scss b/src/Containers.scss
new file mode 100644
index 0000000..309c7a2
--- /dev/null
+++ b/src/Containers.scss
@@ -0,0 +1,93 @@
+@import "global-variables";
+
+.container-pod {
+ .pf-v5-c-card__header {
+ border-color: #ddd;
+ padding-block-start: var(--pf-v5-global--spacer--md);
+ }
+
+ .pod-header-details {
+ border-color: #ddd;
+ margin-block-start: var(--pf-v5-global--spacer--md);
+ margin-inline: var(--pf-v5-global--spacer--md);
+ }
+
+ .pod-details-button {
+ padding-inline: 0;
+ margin-inline-end: var(--pf-v5-global--spacer--md);
+ }
+
+ .pod-details-button-color {
+ color: var(--pf-v5-c-button--m-secondary--Color);
+ }
+
+ .pf-v5-c-card__title {
+ padding: 0;
+ font-weight: var(--pf-v5-global--FontWeight--normal);
+ font-size: var(--pf-v5-global--FontSize--sm);
+
+ .pod-name {
+ font-weight: var(--pf-v5-global--FontWeight--bold);
+ font-size: var(--pf-v5-global--FontSize--md);
+ padding-inline-end: 1rem;
+ }
+ }
+
+ > .pf-v5-c-card__header {
+ &:not(:last-child) {
+ padding-block-end: var(--pf-v5-global--spacer-sm);
+ }
+
+ // Reduce vertical padding of pod header items
+ > .pf-v5-c-card__title > .pf-l-flex {
+ row-gap: var(--pf-v5-global--spacer--sm);
+ }
+ }
+}
+
+// override ct-card font size, so cpu/ram don't look absurdly big
+#app .pf-v5-c-card.container-pod div.pf-v5-c-card__title-text {
+ font-weight: normal;
+ font-size: var(--pf-v5-global--FontSize--md);
+}
+
+.pod-stat {
+ @media (max-width: $pf-v5-global--breakpoint--sm - 1) {
+ // Place each pod stat on its own row
+ flex-basis: 100%;
+ display: grid;
+ // Give labels to the same space
+ grid-template-columns: minmax(auto, 4rem) 1fr;
+
+ > svg {
+ // Hide icons in mobile to be consistent with container lists
+ display: none;
+ }
+ }
+
+ // Center the icons for proper vertical alignment
+ > svg {
+ align-self: center;
+ }
+}
+
+.ct-table-empty td {
+ padding-block: var(--pf-v5-global--spacer--sm) var(--pf-v5-global--spacer--lg);
+ padding-inline: var(--pf-v5-global--spacer--md);
+}
+
+/* HACK - force DescriptionList to wrap but not fill the width */
+#container-details-healthcheck {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+/* Upstream issue https://github.com/patternfly/patternfly/pull/3714 */
+.containers-containers .pf-v5-c-toolbar__content-section {
+ gap: var(--pf-v5-global--spacer--sm);
+}
+
+/* Drop the excessive margin for a Dropdown button */
+.containers-containers .pf-v5-c-toolbar__content-section > :nth-last-child(2) {
+ margin-inline-end: 0;
+}
diff --git a/src/Env.jsx b/src/Env.jsx
new file mode 100644
index 0000000..e36b658
--- /dev/null
+++ b/src/Env.jsx
@@ -0,0 +1,96 @@
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { FormHelper } from "cockpit-components-form-helper.jsx";
+import { Grid } from "@patternfly/react-core/dist/esm/layouts/Grid";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { TrashIcon } from '@patternfly/react-icons';
+import cockpit from 'cockpit';
+
+import * as utils from './util.js';
+
+const _ = cockpit.gettext;
+
+export function validateEnvVar(env, key) {
+ switch (key) {
+ case "envKey":
+ if (!env)
+ return _("Key must not be empty");
+ break;
+ case "envValue":
+ break;
+ default:
+ console.error(`Unknown key "${key}"`); // not-covered: unreachable assertion
+ }
+}
+
+const handleEnvValue = (key, value, idx, onChange, additem, itemCount, companionField) => {
+ // Allow the input of KEY=VALUE separated value pairs for bulk import only if the other
+ // field is not empty.
+ if (value.includes('=') && !companionField) {
+ const parts = value.trim().split(" ");
+ let index = idx;
+ for (const part of parts) {
+ const [envKey, ...envVar] = part.split('=');
+ if (!envKey || !envVar) {
+ continue;
+ }
+
+ if (index !== idx) {
+ additem();
+ }
+ onChange(index, 'envKey', envKey);
+ onChange(index, 'envValue', envVar.join('='));
+ index++;
+ }
+ } else {
+ onChange(idx, key, value);
+ }
+};
+
+export const EnvVar = ({ id, item, onChange, idx, removeitem, additem, itemCount, validationFailed, onValidationChange }) =>
+ (
+ <Grid hasGutter id={id}>
+ <FormGroup className="pf-m-6-col-on-md"
+ id={id + "-key-group"}
+ label={_("Key")}
+ fieldId={id + "-key-address"}
+ isRequired
+ >
+ <TextInput id={id + "-key"}
+ value={item.envKey || ''}
+ validated={validationFailed?.envKey ? "error" : "default"}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "envKey", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, envKey: validateEnvVar(value, "envKey") }));
+ handleEnvValue('envKey', value, idx, onChange, additem, itemCount, item.envValue);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.envKey} />
+ </FormGroup>
+ <FormGroup className="pf-m-6-col-on-md"
+ id={id + "-value-group"}
+ label={_("Value")}
+ fieldId={id + "-value-address"}
+ isRequired
+ >
+ <TextInput id={id + "-value"}
+ value={item.envValue || ''}
+ validated={validationFailed?.envValue ? "error" : "default"}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "envValue", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, envValue: validateEnvVar(value, "envValue") }));
+ handleEnvValue('envValue', value, idx, onChange, additem, itemCount, item.envValue);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.envValue} />
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-md remove-button-group">
+ <Button variant='plain'
+ className="btn-close"
+ id={id + "-btn-close"}
+ size="sm"
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />}
+ onClick={() => removeitem(idx)} />
+ </FormGroup>
+ </Grid>
+ );
diff --git a/src/ForceRemoveModal.jsx b/src/ForceRemoveModal.jsx
new file mode 100644
index 0000000..5d55697
--- /dev/null
+++ b/src/ForceRemoveModal.jsx
@@ -0,0 +1,33 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { useDialogs } from "dialogs.jsx";
+import cockpit from 'cockpit';
+
+const _ = cockpit.gettext;
+
+const ForceRemoveModal = ({ name, reason, handleForceRemove }) => {
+ const Dialogs = useDialogs();
+ const [inProgress, setInProgress] = useState(false);
+ return (
+ <Modal isOpen
+ showClose={false}
+ position="top" variant="medium"
+ titleIconVariant="warning"
+ onClose={Dialogs.close}
+ title={cockpit.format(_("Delete $0?"), name)}
+ footer={<>
+ <Button variant="danger" isDisabled={inProgress} isLoading={inProgress}
+ onClick={() => { setInProgress(true); handleForceRemove().catch(() => setInProgress(false)) }}
+ >
+ {_("Force delete")}
+ </Button>
+ <Button variant="link" isDisabled={inProgress} onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ {reason}
+ </Modal>
+ );
+};
+
+export default ForceRemoveModal;
diff --git a/src/ImageDeleteModal.jsx b/src/ImageDeleteModal.jsx
new file mode 100644
index 0000000..a3fb5a7
--- /dev/null
+++ b/src/ImageDeleteModal.jsx
@@ -0,0 +1,121 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { List, ListItem } from '@patternfly/react-core/dist/esm/components/List';
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Stack, StackItem } from "@patternfly/react-core/dist/esm/layouts/Stack";
+import { useDialogs } from "dialogs.jsx";
+
+import cockpit from 'cockpit';
+
+import ForceRemoveModal from './ForceRemoveModal.jsx';
+import * as client from './client.js';
+
+const _ = cockpit.gettext;
+
+function sortTags(a, b) {
+ if (a.endsWith(":latest"))
+ return -1;
+ if (b.endsWith(":latest"))
+ return 1;
+ return a.localeCompare(b);
+}
+
+export const ImageDeleteModal = ({ imageWillDelete, onAddNotification }) => {
+ const Dialogs = useDialogs();
+ const repoTags = imageWillDelete.RepoTags ? imageWillDelete.RepoTags : [];
+ const isIntermediateImage = repoTags.length === 0;
+
+ const [tags, setTags] = useState(repoTags.sort(sortTags).reduce((acc, item, i) => {
+ acc[item] = (i === 0);
+ return acc;
+ }, {}));
+
+ const checkedTags = Object.keys(tags).sort(sortTags)
+ .filter(x => tags[x]);
+
+ const onValueChanged = (item, value) => {
+ setTags(prevState => ({
+ ...prevState,
+ [item]: value,
+ }));
+ };
+
+ const handleRemoveImage = (tags, all) => {
+ const handleForceRemoveImage = () => {
+ Dialogs.close();
+ return client.delImage(imageWillDelete.isSystem, imageWillDelete.Id, true)
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to force remove image $0"), imageWillDelete.RepoTags[0]);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ throw ex;
+ });
+ };
+
+ Dialogs.close();
+ if (all)
+ client.delImage(imageWillDelete.isSystem, imageWillDelete.Id, false)
+ .catch(ex => {
+ Dialogs.show(<ForceRemoveModal name={isIntermediateImage ? _("intermediate image") : repoTags[0]}
+ handleForceRemove={handleForceRemoveImage}
+ reason={ex.message} />);
+ });
+ else {
+ // Call another untag once previous one resolved. Calling all at once can result in undefined behavior
+ const tag = tags.shift();
+ const i = tag.lastIndexOf(":");
+ client.untagImage(imageWillDelete.isSystem, imageWillDelete.Id, tag.substring(0, i), tag.substring(i + 1, tag.length))
+ .then(() => {
+ if (tags.length > 0)
+ handleRemoveImage(tags, all);
+ })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to remove image $0"), tag);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ }
+ };
+
+ const imageName = repoTags[0]?.split(":")[0].split("/").at(-1) ?? _("intermediate");
+
+ let isAllSelected = null;
+ if (checkedTags.length === repoTags.length)
+ isAllSelected = true;
+ else if (checkedTags.length === 0)
+ isAllSelected = false;
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ titleIconVariant="warning"
+ onClose={Dialogs.close}
+ title={cockpit.format(_("Delete $0 image?"), imageName)}
+ footer={<>
+ <Button id="btn-img-delete" variant="danger" isDisabled={!isIntermediateImage && checkedTags.length === 0}
+ onClick={() => handleRemoveImage(checkedTags, checkedTags.length === repoTags.length)}>
+ {isIntermediateImage ? _("Delete image") : _("Delete tagged images")}
+ </Button>
+ <Button variant="link" onClick={Dialogs.close}>{_("Cancel")}</Button>
+ </>}
+ >
+ <Stack hasGutter>
+ { repoTags.length > 1 && <StackItem>{_("Multiple tags exist for this image. Select the tagged images to delete.")}</StackItem> }
+ <StackItem isFilled>
+ {repoTags.length > 1 && <Checkbox isChecked={isAllSelected} id='delete-all' label={_("All")} aria-label='All'
+ onChange={(_event, checked) => repoTags.forEach(item => onValueChanged(item, checked))}
+ body={
+ repoTags.map(x => (
+ <Checkbox isChecked={checkedTags.indexOf(x) > -1}
+ id={"delete-" + x}
+ aria-label={x}
+ key={x}
+ label={x}
+ onChange={(_event, checked) => onValueChanged(x, checked)} />
+ ))
+ } />}
+ {repoTags.length === 1 && <List><ListItem>{repoTags[0]}</ListItem></List>}
+ </StackItem>
+ </Stack>
+ </Modal>
+ );
+};
diff --git a/src/ImageDetails.jsx b/src/ImageDetails.jsx
new file mode 100644
index 0000000..6266243
--- /dev/null
+++ b/src/ImageDetails.jsx
@@ -0,0 +1,47 @@
+import React from 'react';
+import cockpit from 'cockpit';
+import * as utils from './util.js';
+
+import { DescriptionList, DescriptionListDescription, DescriptionListGroup, DescriptionListTerm } from "@patternfly/react-core/dist/esm/components/DescriptionList";
+
+import ImageUsedBy from './ImageUsedBy.jsx';
+const _ = cockpit.gettext;
+
+const ImageDetails = ({ containers, image, showAll }) => {
+ return (
+ <DescriptionList className='image-details' isAutoFit>
+ {image.Command !== "" &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Command")}</DescriptionListTerm>
+ <DescriptionListDescription>{utils.quote_cmdline(image.Command)}</DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ {image.Entrypoint &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Entrypoint")}</DescriptionListTerm>
+ <DescriptionListDescription>{image.Entrypoint.join(" ")}</DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ {image.RepoTags &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Tags")}</DescriptionListTerm>
+ <DescriptionListDescription>{image.RepoTags ? image.RepoTags.join(" ") : ""}</DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ {containers &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Used by")}</DescriptionListTerm>
+ <DescriptionListDescription><ImageUsedBy containers={containers} showAll={showAll} /></DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ {image.Ports.length !== 0 &&
+ <DescriptionListGroup>
+ <DescriptionListTerm>{_("Ports")}</DescriptionListTerm>
+ <DescriptionListDescription>{image.Ports.join(', ')}</DescriptionListDescription>
+ </DescriptionListGroup>
+ }
+ </DescriptionList>
+ );
+};
+
+export default ImageDetails;
diff --git a/src/ImageHistory.jsx b/src/ImageHistory.jsx
new file mode 100644
index 0000000..05f3170
--- /dev/null
+++ b/src/ImageHistory.jsx
@@ -0,0 +1,65 @@
+import React, { useState, useEffect } from 'react';
+import cockpit from 'cockpit';
+import * as utils from './util.js';
+import * as client from './client.js';
+
+import { ListingTable } from "cockpit-components-table.jsx";
+
+const _ = cockpit.gettext;
+
+const IdColumn = Id => {
+ Id = utils.truncate_id(Id);
+ // Not an id but <missing> or something else
+ if (/<[a-z]+>/.test(Id)) {
+ return <div className="pf-v5-u-disabled-color-100">{Id}</div>;
+ }
+ return Id;
+};
+
+const ImageDetails = ({ image }) => {
+ const [history, setHistory] = useState([]);
+ const [error, setError] = useState(null);
+ const isSystem = image.isSystem;
+ const id = image.Id;
+
+ useEffect(() => {
+ client.imageHistory(isSystem, id).then(setHistory)
+ .catch(ex => {
+ console.error("Cannot get image history", ex);
+ setError(true);
+ });
+ }, [isSystem, id]);
+
+ const columns = ["ID", _("Created"), _("Created by"), _("Size"), _("Comments")];
+ let showComments = false;
+ const rows = history.map(layer => {
+ const row = {
+ columns: [
+ { title: IdColumn(layer.Id), props: { className: "ignore-pixels" } },
+ { title: utils.localize_time(layer.Created), props: { className: "ignore-pixels" } },
+ { title: layer.CreatedBy, props: { className: "ignore-pixels" } },
+ { title: cockpit.format_bytes(layer.Size), props: { className: "ignore-pixels" } },
+ { title: layer.Comment, props: { className: "ignore-pixels" } },
+ ]
+ };
+ if (layer.Comment) {
+ showComments = true;
+ }
+ return row;
+ });
+
+ if (!showComments) {
+ columns.pop();
+ }
+
+ return (
+ <ListingTable
+ variant='compact'
+ isStickyHeader
+ emptyCaption={error ? _("Unable to load image history") : _("Loading details...")}
+ columns={columns}
+ rows={rows} />
+ );
+};
+
+export default ImageDetails;
diff --git a/src/ImageRunModal.jsx b/src/ImageRunModal.jsx
new file mode 100644
index 0000000..477be02
--- /dev/null
+++ b/src/ImageRunModal.jsx
@@ -0,0 +1,1183 @@
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { FormHelper } from "cockpit-components-form-helper.jsx";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Grid, GridItem } from "@patternfly/react-core/dist/esm/layouts/Grid";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio";
+import { Select, SelectGroup, SelectOption, SelectVariant } from "@patternfly/react-core/dist/esm/deprecated/components/Select";
+import { NumberInput } from "@patternfly/react-core/dist/esm/components/NumberInput";
+import { InputGroup, InputGroupText } from "@patternfly/react-core/dist/esm/components/InputGroup";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { Tab, TabTitleText, Tabs } from "@patternfly/react-core/dist/esm/components/Tabs";
+import { Text, TextContent, TextList, TextListItem, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
+import { ToggleGroup, ToggleGroupItem } from "@patternfly/react-core/dist/esm/components/ToggleGroup";
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
+import { OutlinedQuestionCircleIcon } from '@patternfly/react-icons';
+import * as dockerNames from 'docker-names';
+
+import { ErrorNotification } from './Notification.jsx';
+import * as utils from './util.js';
+import * as client from './client.js';
+import rest from './rest.js';
+import cockpit from 'cockpit';
+import { onDownloadContainer, onDownloadContainerFinished } from './Containers.jsx';
+import { PublishPort, validatePublishPort } from './PublishPort.jsx';
+import { DynamicListForm } from 'cockpit-components-dynamic-list.jsx';
+import { validateVolume, Volume } from './Volume.jsx';
+import { EnvVar, validateEnvVar } from './Env.jsx';
+
+import { debounce } from 'throttle-debounce';
+
+import "./ImageRunModal.scss";
+
+const _ = cockpit.gettext;
+
+const systemOwner = "system";
+
+const units = {
+ KB: {
+ name: "KB",
+ baseExponent: 1,
+ },
+ MB: {
+ name: "MB",
+ baseExponent: 2,
+ },
+ GB: {
+ name: "GB",
+ baseExponent: 3,
+ },
+};
+
+// healthchecks.go HealthCheckOnFailureAction
+const HealthCheckOnFailureActionOrder = [
+ { value: 0, label: _("No action") },
+ { value: 3, label: _("Restart") },
+ { value: 4, label: _("Stop") },
+ { value: 2, label: _("Force stop") },
+];
+
+export class ImageRunModal extends React.Component {
+ constructor(props) {
+ super(props);
+
+ let command = "";
+ if (this.props.image && this.props.image.Command) {
+ command = utils.quote_cmdline(this.props.image.Command);
+ }
+
+ const entrypoint = utils.quote_cmdline(this.props.image?.Entrypoint);
+
+ let selectedImage = "";
+ if (this.props.image) {
+ selectedImage = utils.image_name(this.props.image);
+ }
+
+ let default_owner = this.props.systemServiceAvailable ? systemOwner : this.props.user;
+ if (this.props.pod)
+ default_owner = this.props.pod.isSystem ? systemOwner : this.props.user;
+
+ this.state = {
+ command,
+ containerName: dockerNames.getRandomName(),
+ entrypoint,
+ env: [],
+ hasTTY: true,
+ publish: [],
+ image: props.image,
+ memory: 512,
+ cpuShares: 1024,
+ memoryConfigure: false,
+ cpuSharesConfigure: false,
+ memoryUnit: 'MB',
+ validationFailed: {},
+ volumes: [],
+ restartPolicy: "no",
+ restartTries: 5,
+ pullLatestImage: false,
+ activeTabKey: 0,
+ owner: default_owner,
+ /* image select */
+ selectedImage,
+ searchFinished: false,
+ searchInProgress: false,
+ searchText: "",
+ imageResults: {},
+ isImageSelectOpen: false,
+ searchByRegistry: 'all',
+ /* health check */
+ healthcheck_command: "",
+ healthcheck_interval: 30,
+ healthcheck_timeout: 30,
+ healthcheck_start_period: 0,
+ healthcheck_retries: 3,
+ healthcheck_action: 0,
+ };
+ this.getCreateConfig = this.getCreateConfig.bind(this);
+ this.onValueChanged = this.onValueChanged.bind(this);
+ }
+
+ componentDidMount() {
+ this._isMounted = true;
+ this.onSearchTriggered(this.state.searchText);
+ }
+
+ componentWillUnmount() {
+ this._isMounted = false;
+
+ if (this.activeConnection)
+ this.activeConnection.close();
+ }
+
+ getCreateConfig() {
+ const createConfig = {};
+
+ if (this.props.pod) {
+ createConfig.pod = this.props.pod.Id;
+ }
+
+ if (this.state.image) {
+ createConfig.image = this.state.image.RepoTags ? this.state.image.RepoTags[0] : "";
+ } else {
+ let img = this.state.selectedImage.Name;
+ // Make implicit :latest
+ if (!img.includes(":")) {
+ img += ":latest";
+ }
+ createConfig.image = img;
+ }
+ if (this.state.containerName)
+ createConfig.name = this.state.containerName;
+ if (this.state.command) {
+ createConfig.command = utils.unquote_cmdline(this.state.command);
+ }
+ const resourceLimit = {};
+ if (this.state.memoryConfigure && this.state.memory) {
+ const memorySize = this.state.memory * (1000 ** units[this.state.memoryUnit].baseExponent);
+ resourceLimit.memory = { limit: memorySize };
+ createConfig.resource_limits = resourceLimit;
+ }
+ if (this.state.cpuSharesConfigure && parseInt(this.state.cpuShares) !== 0) {
+ resourceLimit.cpu = { shares: parseInt(this.state.cpuShares) };
+ createConfig.resource_limits = resourceLimit;
+ }
+ createConfig.terminal = this.state.hasTTY;
+ if (this.state.publish.some(port => port !== undefined))
+ createConfig.portmappings = this.state.publish
+ .filter(port => port?.containerPort)
+ .map(port => {
+ const pm = { container_port: parseInt(port.containerPort), protocol: port.protocol };
+ if (port.hostPort !== null)
+ pm.host_port = parseInt(port.hostPort);
+ if (port.IP !== null)
+ pm.host_ip = port.IP;
+ return pm;
+ });
+ if (this.state.env.some(item => item !== undefined)) {
+ const envs = {};
+ this.state.env.forEach(item => {
+ if (item !== undefined)
+ envs[item.envKey] = item.envValue;
+ });
+ createConfig.env = envs;
+ }
+ if (this.state.volumes.some(volume => volume !== undefined)) {
+ createConfig.mounts = this.state.volumes
+ .filter(volume => volume?.hostPath && volume?.containerPath)
+ .map(volume => {
+ const record = { source: volume.hostPath, destination: volume.containerPath, type: "bind" };
+ record.options = [];
+ if (volume.mode)
+ record.options.push(volume.mode);
+ if (volume.selinux)
+ record.options.push(volume.selinux);
+ return record;
+ });
+ }
+
+ if (this.state.restartPolicy !== "no") {
+ createConfig.restart_policy = this.state.restartPolicy;
+ if (this.state.restartPolicy === "on-failure" && this.state.restartTries !== null) {
+ createConfig.restart_tries = parseInt(this.state.restartTries);
+ }
+ // Enable podman-restart.service for system containers, for user
+ // sessions enable-linger needs to be enabled for containers to start on boot.
+ if (this.state.restartPolicy === "always" && (this.props.podmanInfo.userLingeringEnabled || this.props.systemServiceAvailable)) {
+ this.enablePodmanRestartService();
+ }
+ }
+
+ if (this.state.healthcheck_command !== "") {
+ createConfig.healthconfig = {
+ Interval: parseInt(this.state.healthcheck_interval) * 1000000000,
+ Retries: this.state.healthcheck_retries,
+ StartPeriod: parseInt(this.state.healthcheck_start_period) * 1000000000,
+ Test: utils.unquote_cmdline(this.state.healthcheck_command),
+ Timeout: parseInt(this.state.healthcheck_timeout) * 1000000000,
+ };
+ createConfig.health_check_on_failure_action = parseInt(this.state.healthcheck_action);
+ }
+
+ return createConfig;
+ }
+
+ createContainer = (isSystem, createConfig, runImage) => {
+ const Dialogs = this.props.dialogs;
+ client.createContainer(isSystem, createConfig)
+ .then(reply => {
+ if (runImage) {
+ client.postContainer(isSystem, "start", reply.Id, {})
+ .then(() => Dialogs.close())
+ .catch(ex => {
+ // If container failed to start remove it, so a user can fix the settings and retry and
+ // won't get another error that the container name is already taken.
+ client.delContainer(isSystem, reply.Id, true)
+ .then(() => {
+ this.setState({
+ dialogError: _("Container failed to be started"),
+ dialogErrorDetail: cockpit.format("$0: $1", ex.reason, ex.message)
+ });
+ })
+ .catch(ex => {
+ this.setState({
+ dialogError: _("Failed to clean up container"),
+ dialogErrorDetail: cockpit.format("$0: $1", ex.reason, ex.message)
+ });
+ });
+ });
+ } else {
+ Dialogs.close();
+ }
+ })
+ .catch(ex => {
+ this.setState({
+ dialogError: _("Container failed to be created"),
+ dialogErrorDetail: cockpit.format("$0: $1", ex.reason, ex.message)
+ });
+ });
+ };
+
+ async onCreateClicked(runImage = false) {
+ if (!await this.validateForm())
+ return;
+
+ const Dialogs = this.props.dialogs;
+ const createConfig = this.getCreateConfig();
+ const { pullLatestImage } = this.state;
+ const isSystem = this.isSystem();
+ let imageExists = true;
+
+ try {
+ await client.imageExists(isSystem, createConfig.image);
+ } catch (error) {
+ imageExists = false;
+ }
+
+ if (imageExists && !pullLatestImage) {
+ this.createContainer(isSystem, createConfig, runImage);
+ } else {
+ Dialogs.close();
+ const tempImage = { ...createConfig };
+
+ // Assign temporary properties to allow rendering
+ tempImage.Id = tempImage.name;
+ tempImage.isSystem = isSystem;
+ tempImage.State = { Status: _("downloading") };
+ tempImage.Created = new Date();
+ tempImage.Name = tempImage.name;
+ tempImage.Image = createConfig.image;
+ tempImage.isDownloading = true;
+
+ onDownloadContainer(tempImage);
+
+ client.pullImage(isSystem, createConfig.image).then(reply => {
+ client.createContainer(isSystem, createConfig)
+ .then(reply => {
+ if (runImage) {
+ client.postContainer(isSystem, "start", reply.Id, {})
+ .then(() => onDownloadContainerFinished(createConfig))
+ .catch(ex => {
+ onDownloadContainerFinished(createConfig);
+ const error = cockpit.format(_("Failed to run container $0"), tempImage.name);
+ this.props.onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ }
+ })
+ .catch(ex => {
+ onDownloadContainerFinished(createConfig);
+ const error = cockpit.format(_("Failed to create container $0"), tempImage.name);
+ this.props.onAddNotification({ type: 'danger', error, errorDetail: ex.reason });
+ });
+ })
+ .catch(ex => {
+ onDownloadContainerFinished(createConfig);
+ const error = cockpit.format(_("Failed to pull image $0"), tempImage.image);
+ this.props.onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ });
+ }
+ }
+
+ onValueChanged(key, value) {
+ this.setState({ [key]: value });
+ }
+
+ onPlusOne(key) {
+ this.setState(state => ({ [key]: parseInt(state[key]) + 1 }));
+ }
+
+ onMinusOne(key) {
+ this.setState(state => ({ [key]: parseInt(state[key]) - 1 }));
+ }
+
+ handleTabClick = (event, tabIndex) => {
+ // Prevent the form from being submitted.
+ event.preventDefault();
+ this.setState({
+ activeTabKey: tabIndex,
+ });
+ };
+
+ onSearchTriggered = value => {
+ // Do not call the SearchImage API if the input string is not at least 2 chars,
+ // The comparison was done considering the fact that we miss always one letter due to delayed setState
+ if (value.length < 2)
+ return;
+
+ // Don't search for a value with a tag specified
+ const patt = /:[\w|\d]+$/;
+ if (patt.test(value)) {
+ return;
+ }
+
+ if (this.activeConnection)
+ this.activeConnection.close();
+
+ this.setState({ searchFinished: false, searchInProgress: true });
+ this.activeConnection = rest.connect(client.getAddress(this.isSystem()), this.isSystem());
+ let searches = [];
+
+ // If there are registries configured search in them, or if a user searches for `docker.io/cockpit` let
+ // podman search in the user specified registry.
+ if (Object.keys(this.props.podmanInfo.registries).length !== 0 || value.includes('/')) {
+ searches.push(this.activeConnection.call({
+ method: "GET",
+ path: client.VERSION + "libpod/images/search",
+ body: "",
+ params: {
+ term: value,
+ }
+ }));
+ } else {
+ searches = searches.concat(utils.fallbackRegistries.map(registry =>
+ this.activeConnection.call({
+ method: "GET",
+ path: client.VERSION + "libpod/images/search",
+ body: "",
+ params: {
+ term: registry + "/" + value
+ }
+ })));
+ }
+
+ Promise.allSettled(searches)
+ .then(reply => {
+ if (reply && this._isMounted) {
+ let imageResults = [];
+ let dialogError = "";
+ let dialogErrorDetail = "";
+
+ for (const result of reply) {
+ if (result.status === "fulfilled") {
+ imageResults = imageResults.concat(JSON.parse(result.value));
+ } else {
+ dialogError = _("Failed to search for new images");
+ // TODO: add registry context, podman does not include it in the reply.
+ dialogErrorDetail = result.reason ? cockpit.format(_("Failed to search for images: $0"), result.reason.message) : _("Failed to search for images.");
+ }
+ }
+ // Group images on registry
+ const images = {};
+ imageResults.forEach(image => {
+ // Add Tag is it's there
+ image.toString = function imageToString() {
+ if (this.Tag) {
+ return this.Name + ':' + this.Tag;
+ }
+ return this.Name;
+ };
+
+ let index = image.Index;
+
+ // listTags results do not return the registry Index.
+ // https://github.com/containers/common/pull/803
+ if (!index) {
+ index = image.Name.split('/')[0];
+ }
+
+ if (index in images) {
+ images[index].push(image);
+ } else {
+ images[index] = [image];
+ }
+ });
+ this.setState({
+ imageResults: images || {},
+ searchFinished: true,
+ searchInProgress: false,
+ dialogError,
+ dialogErrorDetail,
+ });
+ }
+ });
+ };
+
+ clearImageSelection = () => {
+ // Reset command if it was prefilled
+ let command = this.state.command;
+ if (this.state.command === utils.quote_cmdline(this.state.selectedImage?.Command))
+ command = "";
+
+ this.setState({
+ selectedImage: "",
+ image: "",
+ isImageSelectOpen: false,
+ imageResults: {},
+ searchText: "",
+ searchFinished: false,
+ command,
+ entrypoint: "",
+ });
+ };
+
+ onImageSelectToggle = (_, isOpen) => {
+ this.setState({
+ isImageSelectOpen: isOpen,
+ });
+ };
+
+ onImageSelect = (event, value, placeholder) => {
+ if (event === undefined)
+ return;
+
+ let command = this.state.command;
+ if (value.Command && !command)
+ command = utils.quote_cmdline(value.Command);
+
+ const entrypoint = utils.quote_cmdline(value?.Entrypoint);
+
+ this.setState({
+ selectedImage: value,
+ isImageSelectOpen: false,
+ command,
+ entrypoint,
+ });
+ };
+
+ handleImageSelectInput = value => {
+ this.setState({
+ searchText: value,
+ // Reset searchFinished status when text input changes
+ searchFinished: false,
+ selectedImage: "",
+ });
+ this.onSearchTriggered(value);
+ };
+
+ debouncedInputChanged = debounce(300, this.handleImageSelectInput);
+
+ handleOwnerSelect = (event) => {
+ const value = event.currentTarget.value;
+ this.setState({
+ owner: value
+ });
+ };
+
+ filterImages = () => {
+ const { localImages } = this.props;
+ const { imageResults, searchText } = this.state;
+ const local = _("Local images");
+ const images = { ...imageResults };
+ const isSystem = this.isSystem();
+
+ let imageRegistries = [];
+ if (this.state.searchByRegistry == 'local' || this.state.searchByRegistry == 'all') {
+ imageRegistries.push(local);
+ images[local] = localImages;
+
+ if (this.state.searchByRegistry == 'all')
+ imageRegistries = imageRegistries.concat(Object.keys(imageResults));
+ } else {
+ imageRegistries.push(this.state.searchByRegistry);
+ }
+
+ // Strip out all non-allowed container image characters when filtering.
+ let regexString = searchText.replace(/[^\w_.:-]/g, "");
+ // Strip image registry option if set for comparing results for docker.io searching for docker.io/fedora
+ // returns docker.io/$username/fedora for example.
+ if (regexString.includes('/')) {
+ regexString = searchText.replace(searchText.split('/')[0], '');
+ }
+ const input = new RegExp(regexString, 'i');
+
+ const results = imageRegistries
+ .map((reg, index) => {
+ const filtered = (reg in images ? images[reg] : [])
+ .filter(image => {
+ if (image.isSystem && !isSystem) {
+ return false;
+ }
+ if ('isSystem' in image && !image.isSystem && isSystem) {
+ return false;
+ }
+ return image.Name.search(input) !== -1;
+ })
+ .map((image, index) => {
+ return (
+ <SelectOption
+ key={index}
+ value={image}
+ {...(image.Description && { description: image.Description })}
+ />
+ );
+ });
+
+ if (filtered.length === 0) {
+ return [];
+ } else {
+ return (
+ <SelectGroup label={reg} key={index} value={reg}>
+ {filtered}
+ </SelectGroup>
+ );
+ }
+ })
+ .filter(group => group.length !== 0); // filter out empty groups
+
+ // Remove <SelectGroup> when there is a filter selected.
+ if (this.state.searchByRegistry !== 'all' && imageRegistries.length === 1 && results.length === 1) {
+ return results[0].props.children;
+ }
+
+ return results;
+ };
+
+ // Similar to the output of podman search and podman's /libpod/images/search endpoint only show the root domain.
+ truncateRegistryDomain = (domain) => {
+ const parts = domain.split('.');
+ if (parts.length > 2) {
+ return parts[parts.length - 2] + "." + parts[parts.length - 1];
+ }
+ return domain;
+ };
+
+ enablePodmanRestartService = () => {
+ const argv = ["systemctl", "enable", "podman-restart.service"];
+ if (!this.isSystem()) {
+ argv.splice(1, 0, "--user");
+ }
+
+ cockpit.spawn(argv, { superuser: this.isSystem() ? "require" : "", err: "message" })
+ .catch(err => {
+ console.warn("Failed to start podman-restart.service:", JSON.stringify(err));
+ });
+ };
+
+ isSystem = () => {
+ const { owner } = this.state;
+ return owner === systemOwner;
+ };
+
+ isFormInvalid = validationFailed => {
+ const groupHasError = row => row && Object.values(row)
+ .filter(val => val) // Filter out empty/undefined properties
+ .length > 0; // If one field has error, the whole group (dynamicList) is invalid
+
+ // If at least one group is invalid, then the whole form is invalid
+ return validationFailed.publish?.some(groupHasError) ||
+ validationFailed.volumes?.some(groupHasError) ||
+ validationFailed.env?.some(groupHasError) ||
+ !!validationFailed.containerName;
+ };
+
+ async validateContainerName(containerName) {
+ try {
+ await client.containerExists(this.isSystem(), containerName);
+ } catch (error) {
+ return;
+ }
+ return _("Name already in use");
+ }
+
+ async validateForm() {
+ const { publish, volumes, env, containerName } = this.state;
+ const validationFailed = { };
+
+ const publishValidation = publish.map(a => {
+ if (a === undefined)
+ return undefined;
+
+ return {
+ IP: validatePublishPort(a.IP, "IP"),
+ hostPort: validatePublishPort(a.hostPort, "hostPort"),
+ containerPort: validatePublishPort(a.containerPort, "containerPort"),
+ };
+ });
+ if (publishValidation.some(entry => Object.keys(entry).length > 0))
+ validationFailed.publish = publishValidation;
+
+ const volumesValidation = volumes.map(a => {
+ if (a === undefined)
+ return undefined;
+
+ return {
+ hostPath: validateVolume(a.hostPath, "hostPath"),
+ containerPath: validateVolume(a.containerPath, "containerPath"),
+ };
+ });
+ if (volumesValidation.some(entry => Object.keys(entry).length > 0))
+ validationFailed.volumes = volumesValidation;
+
+ const envValidation = env.map(a => {
+ if (a === undefined)
+ return undefined;
+
+ return {
+ envKey: validateEnvVar(a.envKey, "envKey"),
+ envValue: validateEnvVar(a.envValue, "envValue"),
+ };
+ });
+ if (envValidation.some(entry => Object.keys(entry).length > 0))
+ validationFailed.env = envValidation;
+
+ const containerNameValidation = await this.validateContainerName(containerName);
+
+ if (containerNameValidation)
+ validationFailed.containerName = containerNameValidation;
+
+ this.setState({ validationFailed });
+
+ return !this.isFormInvalid(validationFailed);
+ }
+
+ /* Updates a validation object of the whole dynamic list's form (e.g. the whole port-mapping form)
+ *
+ * Arguments
+ * - key: [publish/volumes/env] - Specifies the validation of which dynamic form of the Image run dialog is being updated
+ * - value: An array of validation errors of the form. Each item of the array represents a row of the dynamic list.
+ * Index needs to corellate with a row number
+ */
+ dynamicListOnValidationChange = (key, value) => {
+ const validationFailedDelta = { ...this.state.validationFailed };
+
+ validationFailedDelta[key] = value;
+
+ if (validationFailedDelta[key].every(a => a === undefined))
+ delete validationFailedDelta[key];
+
+ this.onValueChanged('validationFailed', validationFailedDelta);
+ };
+
+ render() {
+ const Dialogs = this.props.dialogs;
+ const { registries, podmanRestartAvailable, userLingeringEnabled, userPodmanRestartAvailable, selinuxAvailable, version } = this.props.podmanInfo;
+ const { image } = this.props;
+ const dialogValues = this.state;
+ const { activeTabKey, owner, selectedImage } = this.state;
+
+ let imageListOptions = [];
+ if (!image) {
+ imageListOptions = this.filterImages();
+ }
+
+ const localImage = this.state.image || (selectedImage && this.props.localImages.some(img => img.Id === selectedImage.Id));
+ const podmanRegistries = registries && registries.search ? registries.search : utils.fallbackRegistries;
+
+ // Add the search component
+ const footer = (
+ <ToggleGroup className='image-search-footer' aria-label={_("Search by registry")}>
+ <ToggleGroupItem text={_("All")} key='all' isSelected={this.state.searchByRegistry == 'all'} onChange={(ev, _) => {
+ ev.stopPropagation();
+ this.setState({ searchByRegistry: 'all' });
+ }}
+ // Ignore SelectToggle's touchstart's default behaviour
+ onTouchStart={ev => {
+ ev.stopPropagation();
+ }}
+ />
+ <ToggleGroupItem text={_("Local")} key='local' isSelected={this.state.searchByRegistry == 'local'} onChange={(ev, _) => {
+ ev.stopPropagation();
+ this.setState({ searchByRegistry: 'local' });
+ }}
+ onTouchStart={ev => {
+ ev.stopPropagation();
+ }}
+ />
+ {podmanRegistries.map(registry => {
+ const index = this.truncateRegistryDomain(registry);
+ return (
+ <ToggleGroupItem
+ text={index} key={index}
+ isSelected={ this.state.searchByRegistry == index }
+ onChange={ (ev, _) => {
+ ev.stopPropagation();
+ this.setState({ searchByRegistry: index });
+ } }
+ onTouchStart={ ev => ev.stopPropagation() }
+ />
+ );
+ })}
+ </ToggleGroup>
+ );
+
+ const defaultBody = (
+ <Form>
+ {this.state.dialogError && <ErrorNotification errorMessage={this.state.dialogError} errorDetail={this.state.dialogErrorDetail} />}
+ <FormGroup id="image-name-group" fieldId='run-image-dialog-name' label={_("Name")} className="ct-m-horizontal">
+ <TextInput id='run-image-dialog-name'
+ className="image-name"
+ placeholder={_("Container name")}
+ validated={dialogValues.validationFailed.containerName ? "error" : "default"}
+ value={dialogValues.containerName}
+ onChange={(_, value) => {
+ utils.validationClear(dialogValues.validationFailed, "containerName", (value) => this.onValueChanged("validationFailed", value));
+ utils.validationDebounce(async () => {
+ const delta = await this.validateContainerName(value);
+ if (delta)
+ this.onValueChanged("validationFailed", { ...dialogValues.validationFailed, containerName: delta });
+ });
+ this.onValueChanged('containerName', value);
+ }} />
+ <FormHelper helperTextInvalid={dialogValues.validationFailed.containerName} />
+ </FormGroup>
+ <Tabs activeKey={activeTabKey} onSelect={this.handleTabClick}>
+ <Tab eventKey={0} title={<TabTitleText>{_("Details")}</TabTitleText>} className="pf-v5-c-form pf-m-horizontal">
+ { this.props.userServiceAvailable && this.props.systemServiceAvailable &&
+ <FormGroup isInline hasNoPaddingTop fieldId='run-image-dialog-owner' label={_("Owner")}
+ labelIcon={
+ <Popover aria-label={_("Owner help")}
+ enableFlip
+ bodyContent={
+ <>
+ <TextContent>
+ <Text component={TextVariants.h4}>{_("System")}</Text>
+ <TextList>
+ <TextListItem>
+ {_("Ideal for running services")}
+ </TextListItem>
+ <TextListItem>
+ {_("Resource limits can be set")}
+ </TextListItem>
+ <TextListItem>
+ {_("Checkpoint and restore support")}
+ </TextListItem>
+ <TextListItem>
+ {_("Ports under 1024 can be mapped")}
+ </TextListItem>
+ </TextList>
+ </TextContent>
+ <TextContent>
+ <Text component={TextVariants.h4}>{cockpit.format("$0 $1", _("User:"), this.props.user)}</Text>
+ <TextList>
+ <TextListItem>
+ {_("Ideal for development")}
+ </TextListItem>
+ <TextListItem>
+ {_("Restricted by user account permissions")}
+ </TextListItem>
+ </TextList>
+ </TextContent>
+ </>
+ }>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <Radio value="system"
+ label={_("System")}
+ id="run-image-dialog-owner-system"
+ isChecked={owner === "system"}
+ isDisabled={this.props.pod}
+ onChange={this.handleOwnerSelect} />
+ <Radio value={this.props.user}
+ label={cockpit.format("$0 $1", _("User:"), this.props.user)}
+ id="run-image-dialog-owner-user"
+ isDisabled={this.props.pod}
+ isChecked={owner === this.props.user}
+ onChange={this.handleOwnerSelect} />
+ </FormGroup>
+ }
+ <FormGroup fieldId="create-image-image-select-typeahead" label={_("Image")}
+ labelIcon={!this.props.image &&
+ <Popover aria-label={_("Image selection help")}
+ enableFlip
+ bodyContent={
+ <Flex direction={{ default: 'column' }}>
+ <FlexItem>{_("host[:port]/[user]/container[:tag]")}</FlexItem>
+ <FlexItem>{cockpit.format(_("Example: $0"), "quay.io/libpod/busybox")}</FlexItem>
+ <FlexItem>{cockpit.format(_("Searching: $0"), "quay.io/busybox")}</FlexItem>
+ </Flex>
+ }>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }
+ >
+ <Select
+ // We are unable to set id of the input directly, the select component appends
+ // '-select-typeahead' to toggleId.
+ toggleId='create-image-image'
+ isGrouped
+ {...(this.state.searchInProgress && { loadingVariant: 'spinner' })}
+ menuAppendTo={() => document.body}
+ variant={SelectVariant.typeahead}
+ noResultsFoundText={_("No images found")}
+ onToggle={this.onImageSelectToggle}
+ isOpen={this.state.isImageSelectOpen}
+ selections={selectedImage}
+ isInputValuePersisted
+ placeholderText={_("Search string or container location")}
+ onSelect={this.onImageSelect}
+ onClear={this.clearImageSelection}
+ // onFilter must be set or the spinner crashes https://github.com/patternfly/patternfly-react/issues/6384
+ onFilter={() => {}}
+ onTypeaheadInputChanged={this.debouncedInputChanged}
+ footer={footer}
+ isDisabled={!!this.props.image}
+ >
+ {imageListOptions}
+ </Select>
+ </FormGroup>
+
+ {(image || localImage) &&
+ <FormGroup fieldId="run-image-dialog-pull-latest-image">
+ <Checkbox isChecked={this.state.pullLatestImage} id="run-image-dialog-pull-latest-image"
+ onChange={(_event, value) => this.onValueChanged('pullLatestImage', value)} label={_("Pull latest image")}
+ />
+ </FormGroup>
+ }
+
+ {dialogValues.entrypoint &&
+ <FormGroup fieldId='run-image-dialog-entrypoint' hasNoPaddingTop label={_("Entrypoint")}>
+ <Text id="run-image-dialog-entrypoint">{dialogValues.entrypoint}</Text>
+ </FormGroup>
+ }
+
+ <FormGroup fieldId='run-image-dialog-command' label={_("Command")}>
+ <TextInput id='run-image-dialog-command'
+ value={dialogValues.command || ''}
+ onChange={(_, value) => this.onValueChanged('command', value)} />
+ </FormGroup>
+
+ <FormGroup fieldId="run=image-dialog-tty">
+ <Checkbox id="run-image-dialog-tty"
+ isChecked={this.state.hasTTY}
+ label={_("With terminal")}
+ onChange={(_event, checked) => this.onValueChanged('hasTTY', checked)} />
+ </FormGroup>
+
+ <FormGroup fieldId='run-image-dialog-memory' label={_("Memory limit")}>
+ <Flex alignItems={{ default: 'alignItemsCenter' }} className="ct-input-group-spacer-sm modal-run-limiter" id="run-image-dialog-memory-limit">
+ <Checkbox id="run-image-dialog-memory-limit-checkbox"
+ isChecked={this.state.memoryConfigure}
+ onChange={(_event, checked) => this.onValueChanged('memoryConfigure', checked)} />
+ <NumberInput
+ value={dialogValues.memory}
+ id="run-image-dialog-memory"
+ min={0}
+ isDisabled={!this.state.memoryConfigure}
+ onClick={() => !this.state.memoryConfigure && this.onValueChanged('memoryConfigure', true)}
+ onPlus={() => this.onPlusOne('memory')}
+ onMinus={() => this.onMinusOne('memory')}
+ minusBtnAriaLabel={_("Decrease memory")}
+ plusBtnAriaLabel={_("Increase memory")}
+ onChange={ev => this.onValueChanged('memory', parseInt(ev.target.value) < 0 ? 0 : ev.target.value)} />
+ <FormSelect id='memory-unit-select'
+ aria-label={_("Memory unit")}
+ value={this.state.memoryUnit}
+ isDisabled={!this.state.memoryConfigure}
+ className="dialog-run-form-select"
+ onChange={(_event, value) => this.onValueChanged('memoryUnit', value)}>
+ <FormSelectOption value={units.KB.name} key={units.KB.name} label={_("KB")} />
+ <FormSelectOption value={units.MB.name} key={units.MB.name} label={_("MB")} />
+ <FormSelectOption value={units.GB.name} key={units.GB.name} label={_("GB")} />
+ </FormSelect>
+ </Flex>
+ </FormGroup>
+
+ {this.isSystem() &&
+ <FormGroup
+ fieldId='run-image-cpu-priority'
+ label={_("CPU shares")}
+ labelIcon={
+ <Popover aria-label={_("CPU Shares help")}
+ enableFlip
+ bodyContent={_("CPU shares determine the priority of running containers. Default priority is 1024. A higher number prioritizes this container. A lower number decreases priority.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <Flex alignItems={{ default: 'alignItemsCenter' }} className="ct-input-group-spacer-sm modal-run-limiter" id="run-image-dialog-cpu-priority">
+ <Checkbox id="run-image-dialog-cpu-priority-checkbox"
+ isChecked={this.state.cpuSharesConfigure}
+ onChange={(_event, checked) => this.onValueChanged('cpuSharesConfigure', checked)} />
+ <NumberInput
+ id="run-image-cpu-priority"
+ value={dialogValues.cpuShares}
+ onClick={() => !this.state.cpuSharesConfigure && this.onValueChanged('cpuSharesConfigure', true)}
+ min={2}
+ max={262144}
+ isDisabled={!this.state.cpuSharesConfigure}
+ onPlus={() => this.onPlusOne('cpuShares')}
+ onMinus={() => this.onMinusOne('cpuShares')}
+ minusBtnAriaLabel={_("Decrease CPU shares")}
+ plusBtnAriaLabel={_("Increase CPU shares")}
+ onChange={ev => this.onValueChanged('cpuShares', parseInt(ev.target.value) < 2 ? 2 : ev.target.value)} />
+ </Flex>
+ </FormGroup>
+ }
+ {((userLingeringEnabled && userPodmanRestartAvailable) || (this.isSystem() && podmanRestartAvailable)) &&
+ <Grid hasGutter md={6} sm={3}>
+ <GridItem>
+ <FormGroup fieldId='run-image-dialog-restart-policy' label={_("Restart policy")}
+ labelIcon={
+ <Popover aria-label={_("Restart policy help")}
+ enableFlip
+ bodyContent={userLingeringEnabled ? _("Restart policy to follow when containers exit. Using linger for auto-starting containers may not work in some circumstances, such as when ecryptfs, systemd-homed, NFS, or 2FA are used on a user account.") : _("Restart policy to follow when containers exit.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }
+ >
+ <FormSelect id="run-image-dialog-restart-policy"
+ aria-label={_("Restart policy help")}
+ value={dialogValues.restartPolicy}
+ onChange={(_event, value) => this.onValueChanged('restartPolicy', value)}>
+ <FormSelectOption value='no' key='no' label={_("No")} />
+ <FormSelectOption value='on-failure' key='on-failure' label={_("On failure")} />
+ <FormSelectOption value='always' key='always' label={_("Always")} />
+ </FormSelect>
+ </FormGroup>
+ </GridItem>
+ {dialogValues.restartPolicy === "on-failure" &&
+ <FormGroup fieldId='run-image-dialog-restart-retries'
+ label={_("Maximum retries")}>
+ <NumberInput
+ id="run-image-dialog-restart-retries"
+ value={dialogValues.restartTries}
+ min={1}
+ max={65535}
+ widthChars={5}
+ minusBtnAriaLabel={_("Decrease maximum retries")}
+ plusBtnAriaLabel={_("Increase maximum retries")}
+ onMinus={() => this.onMinusOne('restartTries')}
+ onPlus={() => this.onPlusOne('restartTries')}
+ onChange={ev => this.onValueChanged('restartTries', parseInt(ev.target.value) < 1 ? 1 : ev.target.value)}
+ />
+ </FormGroup>
+ }
+ </Grid>
+ }
+ </Tab>
+ <Tab eventKey={1} title={<TabTitleText>{_("Integration")}</TabTitleText>} id="create-image-dialog-tab-integration" className="pf-v5-c-form">
+
+ <DynamicListForm id='run-image-dialog-publish'
+ emptyStateString={_("No ports exposed")}
+ formclass='publish-port-form'
+ label={_("Port mapping")}
+ actionLabel={_("Add port mapping")}
+ validationFailed={dialogValues.validationFailed.publish}
+ onValidationChange={value => this.dynamicListOnValidationChange('publish', value)}
+ onChange={value => this.onValueChanged('publish', value)}
+ default={{ IP: null, containerPort: null, hostPort: null, protocol: 'tcp' }}
+ itemcomponent={ <PublishPort />} />
+ <DynamicListForm id='run-image-dialog-volume'
+ emptyStateString={_("No volumes specified")}
+ formclass='volume-form'
+ label={_("Volumes")}
+ actionLabel={_("Add volume")}
+ validationFailed={dialogValues.validationFailed.volumes}
+ onValidationChange={value => this.dynamicListOnValidationChange('volumes', value)}
+ onChange={value => this.onValueChanged('volumes', value)}
+ default={{ containerPath: null, hostPath: null, mode: 'rw' }}
+ options={{ selinuxAvailable }}
+ itemcomponent={ <Volume />} />
+
+ <DynamicListForm id='run-image-dialog-env'
+ emptyStateString={_("No environment variables specified")}
+ formclass='env-form'
+ label={_("Environment variables")}
+ actionLabel={_("Add variable")}
+ validationFailed={dialogValues.validationFailed.env}
+ onValidationChange={value => this.dynamicListOnValidationChange('env', value)}
+ onChange={value => this.onValueChanged('env', value)}
+ default={{ envKey: null, envValue: null }}
+ helperText={_("Paste one or more lines of key=value pairs into any field for bulk import")}
+ itemcomponent={ <EnvVar />} />
+ </Tab>
+ <Tab eventKey={2} title={<TabTitleText>{_("Health check")}</TabTitleText>} id="create-image-dialog-tab-healthcheck" className="pf-v5-c-form pf-m-horizontal">
+ <FormGroup fieldId='run-image-dialog-healthcheck-command' label={_("Command")}>
+ <TextInput id='run-image-dialog-healthcheck-command'
+ value={dialogValues.healthcheck_command || ''}
+ onChange={(_, value) => this.onValueChanged('healthcheck_command', value)} />
+ </FormGroup>
+
+ <FormGroup fieldId='run-image-healthcheck-interval' label={_("Interval")}
+ labelIcon={
+ <Popover aria-label={_("Health check interval help")}
+ enableFlip
+ bodyContent={_("Interval how often health check is run.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <InputGroup>
+ <NumberInput
+ id="run-image-healthcheck-interval"
+ value={dialogValues.healthcheck_interval}
+ min={0}
+ max={262144}
+ widthChars={6}
+ minusBtnAriaLabel={_("Decrease interval")}
+ plusBtnAriaLabel={_("Increase interval")}
+ onMinus={() => this.onMinusOne('healthcheck_interval')}
+ onPlus={() => this.onPlusOne('healthcheck_interval')}
+ onChange={ev => this.onValueChanged('healthcheck_interval', parseInt(ev.target.value) < 0 ? 0 : ev.target.value)} />
+ <InputGroupText isPlain>{_("seconds")}</InputGroupText>
+ </InputGroup>
+ </FormGroup>
+ <FormGroup fieldId='run-image-healthcheck-timeout' label={_("Timeout")}
+ labelIcon={
+ <Popover aria-label={_("Health check timeout help")}
+ enableFlip
+ bodyContent={_("The maximum time allowed to complete the health check before an interval is considered failed.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <InputGroup>
+ <NumberInput
+ id="run-image-healthcheck-timeout"
+ value={dialogValues.healthcheck_timeout}
+ min={0}
+ max={262144}
+ widthChars={6}
+ minusBtnAriaLabel={_("Decrease timeout")}
+ plusBtnAriaLabel={_("Increase timeout")}
+ onMinus={() => this.onMinusOne('healthcheck_timeout')}
+ onPlus={() => this.onPlusOne('healthcheck_timeout')}
+ onChange={ev => this.onValueChanged('healthcheck_timeout', parseInt(ev.target.value) < 0 ? 0 : ev.target.value)} />
+ <InputGroupText isPlain>{_("seconds")}</InputGroupText>
+ </InputGroup>
+ </FormGroup>
+ <FormGroup fieldId='run-image-healthcheck-start-period' label={_("Start period")}
+ labelIcon={
+ <Popover aria-label={_("Health check start period help")}
+ enableFlip
+ bodyContent={_("The initialization time needed for a container to bootstrap.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <InputGroup>
+ <NumberInput
+ id="run-image-healthcheck-start-period"
+ value={dialogValues.healthcheck_start_period}
+ min={0}
+ max={262144}
+ widthChars={6}
+ minusBtnAriaLabel={_("Decrease start period")}
+ plusBtnAriaLabel={_("Increase start period")}
+ onMinus={() => this.onMinusOne('healthcheck_start_period')}
+ onPlus={() => this.onPlusOne('healthcheck_start_period')}
+ onChange={ev => this.onValueChanged('healthcheck_start_period', parseInt(ev.target.value) < 0 ? 0 : ev.target.value)} />
+ <InputGroupText isPlain>{_("seconds")}</InputGroupText>
+ </InputGroup>
+ </FormGroup>
+ <FormGroup fieldId='run-image-healthcheck-retries' label={_("Retries")}
+ labelIcon={
+ <Popover aria-label={_("Health check retries help")}
+ enableFlip
+ bodyContent={_("The number of retries allowed before a healthcheck is considered to be unhealthy.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <NumberInput
+ id="run-image-healthcheck-retries"
+ value={dialogValues.healthcheck_retries}
+ min={0}
+ max={999}
+ widthChars={3}
+ minusBtnAriaLabel={_("Decrease retries")}
+ plusBtnAriaLabel={_("Increase retries")}
+ onMinus={() => this.onMinusOne('healthcheck_retries')}
+ onPlus={() => this.onPlusOne('healthcheck_retries')}
+ onChange={ev => this.onValueChanged('healthcheck_retries', parseInt(ev.target.value) < 0 ? 0 : ev.target.value)} />
+ </FormGroup>
+ {version.localeCompare("4.3", undefined, { numeric: true, sensitivity: 'base' }) >= 0 &&
+ <FormGroup isInline hasNoPaddingTop fieldId='run-image-healthcheck-action' label={_("When unhealthy") }
+ labelIcon={
+ <Popover aria-label={_("Health failure check action help")}
+ enableFlip
+ bodyContent={_("Action to take once the container transitions to an unhealthy state.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ {HealthCheckOnFailureActionOrder.map(item =>
+ <Radio value={item.value}
+ key={item.value}
+ label={item.label}
+ id={`run-image-healthcheck-action-${item.value}`}
+ isChecked={dialogValues.healthcheck_action === item.value}
+ onChange={() => this.onValueChanged('healthcheck_action', item.value)} />
+ )}
+ </FormGroup>
+ }
+ </Tab>
+ </Tabs>
+ </Form>
+ );
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ // TODO: still not ideal on chromium https://github.com/patternfly/patternfly-react/issues/6471
+ onEscapePress={() => {
+ if (this.state.isImageSelectOpen) {
+ this.onImageSelectToggle(!this.state.isImageSelectOpen);
+ } else {
+ Dialogs.close();
+ }
+ }}
+ title={this.props.pod ? cockpit.format(_("Create container in $0"), this.props.pod.Name) : _("Create container")}
+ footer={<>
+ <Button variant='primary' id="create-image-create-run-btn" onClick={() => this.onCreateClicked(true)} isDisabled={(!image && selectedImage === "") || this.isFormInvalid(dialogValues.validationFailed)}>
+ {_("Create and run")}
+ </Button>
+ <Button variant='secondary' id="create-image-create-btn" onClick={() => this.onCreateClicked(false)} isDisabled={(!image && selectedImage === "") || this.isFormInvalid(dialogValues.validationFailed)}>
+ {_("Create")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ {defaultBody}
+ </Modal>
+ );
+ }
+}
diff --git a/src/ImageRunModal.scss b/src/ImageRunModal.scss
new file mode 100644
index 0000000..6a5399d
--- /dev/null
+++ b/src/ImageRunModal.scss
@@ -0,0 +1,47 @@
+@import "global-variables";
+
+// Ensure the width fits within the screen boundaries (with padding on the sides)
+.pf-v5-c-select__menu {
+ // 3xl is the left+right padding for an iPhone SE;
+ // this works on other screen sizes as well
+ max-inline-size: calc(100vw - var(--pf-v5-global--spacer--3xl));
+}
+
+// Make sure the footer is visible with more then 5 results.
+.pf-c-select__menu-list {
+ // 35% viewport height is for 1280x720;
+ // since it picks the min of the two, it works everywhere
+ max-block-size: min(20rem, 35vh);
+ overflow: hidden scroll;
+}
+
+// Fix the dot next to spinner: https://github.com/patternfly/patternfly-react/issues/6383
+.pf-v5-c-select__list-item.pf-m-loading {
+ list-style-type: none;
+}
+
+.image-search-footer {
+ flex-wrap: wrap;
+
+ .pf-v5-c-toggle-group__text {
+ word-wrap: break-word;
+ }
+}
+
+ // PF4 does not yet support multiple form fields for the same label
+.ct-input-group-spacer-sm.pf-v5-l-flex {
+ // Limit width for select entries and inputs in the input groups otherwise they take up the whole space
+ > .pf-v5-c-select, .pf-v5-c-form-control:not(.pf-v5-c-select__toggle-typeahead) {
+ max-inline-size: 8ch;
+ }
+}
+
+// HACK: A local copy of pf-m-horizontal (as ct-m-horizontal),
+// but applied at the FormGroup level instead of Form
+@media (min-width: $pf-v5-global--breakpoint--md) {
+ .pf-v5-c-form__group.ct-m-horizontal {
+ display: grid;
+ grid-column-gap: var(--pf-v5-c-form--m-horizontal__group-label--md--GridColumnGap);
+ grid-template-columns: var(--pf-v5-c-form--m-horizontal__group-label--md--GridColumnWidth) var(--pf-v5-c-form--m-horizontal__group-control--md--GridColumnWidth);
+ }
+}
diff --git a/src/ImageSearchModal.css b/src/ImageSearchModal.css
new file mode 100644
index 0000000..256c2b0
--- /dev/null
+++ b/src/ImageSearchModal.css
@@ -0,0 +1,59 @@
+.podman-search .pf-v5-c-modal-box__body {
+ display: grid;
+ grid-auto-flow: row;
+ overflow: hidden;
+ grid-template-rows: auto auto 1fr;
+ grid-gap: var(--pf-v5-global--spacer--sm);
+}
+
+.image-list-item {
+ display: grid;
+ grid-gap: 0 1rem;
+}
+
+.image-list-item + .image-list-item {
+ border-block-start: 1px solid var(--pf-v5-global--BorderColor--200);
+}
+
+.image-list-item > .image-name {
+ color: var(--pf-v5-global--Color--100);
+}
+
+@media (min-width: 768px) {
+ .image-list-item {
+ grid-template-columns: 1fr max-content;
+ }
+}
+
+.podman-search .image-search-modal-footer-grid {
+ display: grid;
+ grid-template-columns: 1fr auto auto;
+ grid-gap: 0.25rem;
+}
+
+.image-tag-entry {
+ max-inline-size: 15rem;
+}
+
+@media (max-width: 340px) {
+ /* Shrink buttons to accommodate iPhone 5/SE */
+ .podman-search .modal-footer > .btn {
+ padding-inline: 0.25rem;
+ }
+}
+
+.image-search-tag-form {
+ margin-block-end: var(--pf-v5-global--spacer--md);
+}
+
+.podman-search .pf-v5-c-modal-box__footer {
+ display: initial;
+}
+
+.podman-search .pf-v5-c-data-list {
+ overflow-y: auto;
+}
+
+.podman-search .pf-v5-l-flex .pf-v5-c-form__group:nth-child(2) {
+ grid-template-columns: 2rem var(--pf-v5-c-form--m-horizontal__group-control--md--GridColumnWidth);
+}
diff --git a/src/ImageSearchModal.jsx b/src/ImageSearchModal.jsx
new file mode 100644
index 0000000..82657a0
--- /dev/null
+++ b/src/ImageSearchModal.jsx
@@ -0,0 +1,218 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { DataList, DataListCell, DataListItem, DataListItemCells, DataListItemRow } from "@patternfly/react-core/dist/esm/components/DataList";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+
+import { EmptyStatePanel } from "cockpit-components-empty-state.jsx";
+import { ErrorNotification } from './Notification.jsx';
+import cockpit from 'cockpit';
+import rest from './rest.js';
+import * as client from './client.js';
+import { fallbackRegistries, usePodmanInfo } from './util.js';
+import { useDialogs } from "dialogs.jsx";
+
+import './ImageSearchModal.css';
+
+const _ = cockpit.gettext;
+
+export const ImageSearchModal = ({ downloadImage, user, userServiceAvailable, systemServiceAvailable }) => {
+ const [searchInProgress, setSearchInProgress] = useState(false);
+ const [searchFinished, setSearchFinished] = useState(false);
+ const [imageIdentifier, setImageIdentifier] = useState('');
+ const [imageList, setImageList] = useState([]);
+ const [imageTag, setImageTag] = useState("");
+ const [isSystem, setIsSystem] = useState(systemServiceAvailable);
+ const [selectedRegistry, setSelectedRegistry] = useState("");
+ const [selected, setSelected] = useState("");
+ const [dialogError, setDialogError] = useState("");
+ const [dialogErrorDetail, setDialogErrorDetail] = useState("");
+ const [typingTimeout, setTypingTimeout] = useState(null);
+
+ let activeConnection = null;
+ const { registries } = usePodmanInfo();
+ const Dialogs = useDialogs();
+ // Registries to use for searching
+ const searchRegistries = registries.search && registries.length !== 0 ? registries.search : fallbackRegistries;
+
+ // Don't use on selectedRegistry state variable for finding out the
+ // registry to search in as with useState we can only call something after a
+ // state update with useEffect but as onSearchTriggered also changes state we
+ // can't use that so instead we pass the selected registry.
+ const onSearchTriggered = (searchRegistry = "", forceSearch = false) => {
+ // When search re-triggers close any existing active connection
+ activeConnection = rest.connect(client.getAddress(isSystem), isSystem);
+ if (activeConnection)
+ activeConnection.close();
+ setSearchFinished(false);
+
+ // Do not call the SearchImage API if the input string is not at least 2 chars,
+ // unless Enter is pressed, which should force start the search.
+ // The comparison was done considering the fact that we miss always one letter due to delayed setState
+ if (imageIdentifier.length < 2 && !forceSearch)
+ return;
+
+ setSearchInProgress(true);
+
+ let queryRegistries = searchRegistries;
+ if (searchRegistry !== "") {
+ queryRegistries = [searchRegistry];
+ }
+ // if a user searches for `docker.io/cockpit` let podman search in the user specified registry.
+ if (imageIdentifier.includes('/')) {
+ queryRegistries = [""];
+ }
+
+ const searches = queryRegistries.map(rr => {
+ const registry = rr.length < 1 || rr[rr.length - 1] === "/" ? rr : rr + "/";
+ return activeConnection.call({
+ method: "GET",
+ path: client.VERSION + "libpod/images/search",
+ body: "",
+ params: {
+ term: registry + imageIdentifier
+ }
+ });
+ });
+
+ Promise.allSettled(searches)
+ .then(reply => {
+ if (reply) {
+ let results = [];
+
+ for (const result of reply) {
+ if (result.status === "fulfilled") {
+ results = results.concat(JSON.parse(result.value));
+ } else {
+ setDialogError(_("Failed to search for new images"));
+ setDialogErrorDetail(result.reason ? cockpit.format(_("Failed to search for images: $0"), result.reason.message) : _("Failed to search for images."));
+ }
+ }
+
+ setImageList(results || []);
+ setSearchInProgress(false);
+ setSearchFinished(true);
+ }
+ });
+ };
+
+ const onKeyDown = (e) => {
+ if (e.key != ' ') { // Space should not trigger search
+ const forceSearch = e.key == 'Enter';
+ if (forceSearch) {
+ e.preventDefault();
+ }
+
+ // Reset the timer, to make the http call after 250MS
+ clearTimeout(typingTimeout);
+ setTypingTimeout(setTimeout(() => onSearchTriggered(selectedRegistry, forceSearch), 250));
+ }
+ };
+
+ const onToggleUser = ev => setIsSystem(ev.currentTarget.value === "system");
+ const onDownloadClicked = () => {
+ const selectedImageName = imageList[selected].Name;
+ if (activeConnection)
+ activeConnection.close();
+ Dialogs.close();
+ downloadImage(selectedImageName, imageTag, isSystem);
+ };
+
+ const handleClose = () => {
+ if (activeConnection)
+ activeConnection.close();
+ Dialogs.close();
+ };
+
+ return (
+ <Modal isOpen className="podman-search"
+ position="top" variant="large"
+ onClose={handleClose}
+ title={_("Search for an image")}
+ footer={<>
+ <Form isHorizontal className="image-search-tag-form">
+ <FormGroup fieldId="image-search-tag" label={_("Tag")}>
+ <TextInput className="image-tag-entry"
+ id="image-search-tag"
+ type='text'
+ placeholder="latest"
+ value={imageTag || ''}
+ onChange={(_event, value) => setImageTag(value)} />
+ </FormGroup>
+ </Form>
+ <Button variant='primary' isDisabled={selected === ""} onClick={onDownloadClicked}>
+ {_("Download")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={handleClose}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ <Form isHorizontal>
+ {dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} />}
+ { userServiceAvailable && systemServiceAvailable &&
+ <FormGroup id="as-user" label={_("Owner")} isInline>
+ <Radio name="user" value="system" id="system" onChange={onToggleUser} isChecked={isSystem} label={_("system")} />
+ <Radio name="user" value="user" id="user" onChange={onToggleUser} isChecked={!isSystem} label={user} />
+ </FormGroup>}
+ <Flex spaceItems={{ default: 'inlineFlex', modifier: 'spaceItemsXl' }}>
+ <FormGroup fieldId="search-image-dialog-name" label={_("Search for")}>
+ <TextInput id='search-image-dialog-name'
+ type='text'
+ placeholder={_("Search by name or description")}
+ value={imageIdentifier}
+ onKeyDown={onKeyDown}
+ onChange={(_event, value) => setImageIdentifier(value)} />
+ </FormGroup>
+ <FormGroup fieldId="registry-select" label={_("in")}>
+ <FormSelect id='registry-select'
+ value={selectedRegistry}
+ onChange={(_ev, value) => { setSelectedRegistry(value); clearTimeout(typingTimeout); onSearchTriggered(value, false) }}>
+ <FormSelectOption value="" key="all" label={_("All registries")} />
+ {(searchRegistries || []).map(r => <FormSelectOption value={r} key={r} label={r} />)}
+ </FormSelect>
+ </FormGroup>
+ </Flex>
+ </Form>
+
+ {searchInProgress && <EmptyStatePanel loading title={_("Searching...")} /> }
+
+ {((!searchInProgress && !searchFinished) || imageIdentifier == "") && <EmptyStatePanel title={_("No images found")} paragraph={_("Start typing to look for images.")} /> }
+
+ {searchFinished && imageIdentifier !== '' && <>
+ {imageList.length == 0 && <EmptyStatePanel icon={ExclamationCircleIcon}
+ title={cockpit.format(_("No results for $0"), imageIdentifier)}
+ paragraph={_("Retry another term.")}
+ />}
+ {imageList.length > 0 &&
+ <DataList isCompact
+ selectedDataListItemId={"image-list-item-" + selected}
+ onSelectDataListItem={(_, key) => setSelected(key.split('-').slice(-1)[0])}>
+ {imageList.map((image, iter) => {
+ return (
+ <DataListItem id={"image-list-item-" + iter} key={iter}>
+ <DataListItemRow>
+ <DataListItemCells
+ dataListCells={[
+ <DataListCell key="primary content">
+ <span className='image-name'>{image.Name}</span>
+ </DataListCell>,
+ <DataListCell key="secondary content" wrapModifier="truncate">
+ <span className='image-description'>{image.Description}</span>
+ </DataListCell>
+ ]}
+ />
+ </DataListItemRow>
+ </DataListItem>
+ );
+ })}
+ </DataList>}
+ </>}
+ </Modal>
+ );
+};
diff --git a/src/ImageUsedBy.jsx b/src/ImageUsedBy.jsx
new file mode 100644
index 0000000..6db307b
--- /dev/null
+++ b/src/ImageUsedBy.jsx
@@ -0,0 +1,44 @@
+import React from 'react';
+import cockpit from 'cockpit';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Badge } from "@patternfly/react-core/dist/esm/components/Badge";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List";
+
+const _ = cockpit.gettext;
+
+const ImageUsedBy = ({ containers, showAll }) => {
+ if (containers === null)
+ return _("Loading...");
+ if (containers === undefined)
+ return _("No containers are using this image");
+
+ return (
+ <List isPlain>
+ {containers.map(c => {
+ const container = c.container;
+ const isRunning = container.State?.Status === "running";
+ return (
+ <ListItem key={container.Id}>
+ <Flex>
+ <Button variant="link"
+ isInline
+ onClick={() => {
+ const loc = document.location.toString().split('#')[0];
+ document.location = loc + '#' + container.Id;
+
+ if (!isRunning)
+ showAll();
+ }}>
+ {container.Name}
+ </Button>
+ {isRunning && <Badge className="ct-badge-container-running">{_("Running")}</Badge>}
+ </Flex>
+ </ListItem>
+ );
+ })}
+ </List>
+ );
+};
+
+export default ImageUsedBy;
diff --git a/src/Images.css b/src/Images.css
new file mode 100644
index 0000000..b93b2ac
--- /dev/null
+++ b/src/Images.css
@@ -0,0 +1,23 @@
+#containers-images div.download-in-progress {
+ color: grey;
+ font-weight: bold;
+}
+
+/* Danger dropdown items should be red */
+.pf-v5-c-dropdown__menu-item.pf-m-danger:not(.pf-m-disabled, .pf-m-aria-disabled) {
+ color: var(--pf-v5-global--danger-color--200);
+}
+
+#containers-images .pf-v5-c-table.pf-m-compact .pf-v5-c-table__action {
+ --pf-v5-c-table__action--PaddingTop: 0.5rem;
+ --pf-v5-c-table__action--PaddingBottom: 0.5rem;
+}
+
+.containers-images .pf-v5-c-expandable-section__content {
+ margin-block-start: 0;
+}
+
+/* Override font-size due to h2 being wrapped in a Flex */
+.containers-images-title {
+ font-size: var(--pf-v5-global--FontSize--2xl);
+}
diff --git a/src/Images.jsx b/src/Images.jsx
new file mode 100644
index 0000000..c559caa
--- /dev/null
+++ b/src/Images.jsx
@@ -0,0 +1,429 @@
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Card, CardBody, CardFooter, CardHeader, CardTitle } from "@patternfly/react-core/dist/esm/components/Card";
+import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+import { Flex, FlexItem } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { ExpandableSection } from "@patternfly/react-core/dist/esm/components/ExpandableSection";
+import { Text, TextVariants } from "@patternfly/react-core/dist/esm/components/Text";
+import { cellWidth } from '@patternfly/react-table';
+
+import cockpit from 'cockpit';
+import { ListingTable } from "cockpit-components-table.jsx";
+import { ListingPanel } from 'cockpit-components-listing-panel.jsx';
+import ImageDetails from './ImageDetails.jsx';
+import ImageHistory from './ImageHistory.jsx';
+import { ImageRunModal } from './ImageRunModal.jsx';
+import { ImageSearchModal } from './ImageSearchModal.jsx';
+import { ImageDeleteModal } from './ImageDeleteModal.jsx';
+import PruneUnusedImagesModal from './PruneUnusedImagesModal.jsx';
+import * as client from './client.js';
+import * as utils from './util.js';
+import { useDialogs, DialogsContext } from "dialogs.jsx";
+
+import './Images.css';
+import '@patternfly/react-styles/css/utilities/Sizing/sizing.css';
+
+import { KebabDropdown } from "cockpit-components-dropdown.jsx";
+
+const _ = cockpit.gettext;
+
+class Images extends React.Component {
+ static contextType = DialogsContext;
+
+ constructor(props) {
+ super(props);
+ this.state = {
+ intermediateOpened: false,
+ isExpanded: false,
+ };
+
+ this.downloadImage = this.downloadImage.bind(this);
+ this.renderRow = this.renderRow.bind(this);
+ }
+
+ downloadImage(imageName, imageTag, system) {
+ let pullImageId = imageName;
+ if (imageTag)
+ pullImageId += ":" + imageTag;
+
+ this.setState({ imageDownloadInProgress: imageName });
+ client.pullImage(system, pullImageId)
+ .then(() => {
+ this.setState({ imageDownloadInProgress: undefined });
+ })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to download image $0:$1"), imageName, imageTag || "latest");
+ const errorDetail = (
+ <>
+ <p> {_("Error message")}:
+ <samp>{cockpit.format("$0 $1", ex.message, ex.reason)}</samp>
+ </p>
+ </>
+ );
+ this.setState({ imageDownloadInProgress: undefined });
+ this.props.onAddNotification({ type: 'danger', error, errorDetail });
+ });
+ }
+
+ onOpenNewImagesDialog = () => {
+ const Dialogs = this.context;
+ Dialogs.show(
+ <ImageSearchModal downloadImage={this.downloadImage}
+ user={this.props.user}
+ userServiceAvailable={this.props.userServiceAvailable}
+ systemServiceAvailable={this.props.systemServiceAvailable} />
+ );
+ };
+
+ onOpenPruneUnusedImagesDialog = () => {
+ this.setState({ showPruneUnusedImagesModal: true });
+ };
+
+ getUsedByText(image) {
+ const { imageContainerList } = this.props;
+ if (imageContainerList === null) {
+ return { title: _("unused"), count: 0 };
+ }
+ const containers = imageContainerList[image.Id + image.isSystem.toString()];
+ if (containers !== undefined) {
+ const title = cockpit.format(cockpit.ngettext("$0 container", "$0 containers", containers.length), containers.length);
+ return { title, count: containers.length };
+ } else {
+ return { title: _("unused"), count: 0 };
+ }
+ }
+
+ calculateStats = () => {
+ const { images, imageContainerList } = this.props;
+ const unusedImages = [];
+ const imageStats = {
+ imagesTotal: 0,
+ imagesSize: 0,
+ unusedTotal: 0,
+ unusedSize: 0,
+ };
+
+ if (imageContainerList === null) {
+ return { imageStats, unusedImages };
+ }
+
+ if (images !== null) {
+ Object.keys(images).forEach(id => {
+ const image = images[id];
+ imageStats.imagesTotal += 1;
+ imageStats.imagesSize += image.Size;
+
+ const usedBy = imageContainerList[image.Id + image.isSystem.toString()];
+ if (usedBy === undefined) {
+ imageStats.unusedTotal += 1;
+ imageStats.unusedSize += image.Size;
+ unusedImages.push(image);
+ }
+ });
+ }
+
+ return { imageStats, unusedImages };
+ };
+
+ renderRow(image) {
+ const tabs = [];
+ const { title: usedByText, count: usedByCount } = this.getUsedByText(image);
+
+ const columns = [
+ { title: utils.image_name(image), header: true, props: { modifier: "breakWord" } },
+ { title: image.isSystem ? _("system") : <div><span className="ct-grey-text">{_("user:")} </span>{this.props.user}</div>, props: { className: "ignore-pixels", modifier: "nowrap" } },
+ { title: utils.localize_time(image.Created), props: { className: "ignore-pixels" } },
+ { title: utils.truncate_id(image.Id), props: { className: "ignore-pixels" } },
+ { title: cockpit.format_bytes(image.Size, 1000), props: { className: "ignore-pixels", modifier: "nowrap" } },
+ { title: <span className={usedByCount === 0 ? "ct-grey-text" : ""}>{usedByText}</span>, props: { className: "ignore-pixels", modifier: "nowrap" } },
+ {
+ title: <ImageActions image={image} onAddNotification={this.props.onAddNotification}
+ user={this.props.user}
+ userServiceAvailable={this.props.userServiceAvailable}
+ systemServiceAvailable={this.props.systemServiceAvailable} />,
+ props: { className: 'pf-v5-c-table__action content-action' }
+ },
+ ];
+
+ tabs.push({
+ name: _("Details"),
+ renderer: ImageDetails,
+ data: {
+ image,
+ containers: this.props.imageContainerList !== null ? this.props.imageContainerList[image.Id + image.isSystem.toString()] : null,
+ showAll: this.props.showAll,
+ }
+ });
+ tabs.push({
+ name: _("History"),
+ renderer: ImageHistory,
+ data: {
+ image,
+ }
+ });
+ return {
+ expandedContent: <ListingPanel
+ colSpan='8'
+ tabRenderers={tabs} />,
+ columns,
+ props: {
+ key: image.Id + image.isSystem.toString(),
+ "data-row-id": image.Id + image.isSystem.toString(),
+ },
+ };
+ }
+
+ render() {
+ const columnTitles = [
+ { title: _("Image"), transforms: [cellWidth(20)] },
+ { title: _("Owner"), props: { className: "ignore-pixels" } },
+ { title: _("Created"), props: { className: "ignore-pixels", width: 15 } },
+ { title: _("ID"), props: { className: "ignore-pixels" } },
+ { title: _("Disk space"), props: { className: "ignore-pixels" } },
+ { title: _("Used by"), props: { className: "ignore-pixels" } },
+ ];
+ let emptyCaption = _("No images");
+ if (this.props.images === null)
+ emptyCaption = "Loading...";
+ else if (this.props.textFilter.length > 0)
+ emptyCaption = _("No images that match the current filter");
+
+ const intermediateOpened = this.state.intermediateOpened;
+
+ let filtered = [];
+ if (this.props.images !== null) {
+ filtered = Object.keys(this.props.images).filter(id => {
+ if (this.props.userServiceAvailable && this.props.systemServiceAvailable && this.props.ownerFilter !== "all") {
+ if (this.props.ownerFilter === "system" && !this.props.images[id].isSystem)
+ return false;
+ if (this.props.ownerFilter !== "system" && this.props.images[id].isSystem)
+ return false;
+ }
+
+ const tags = this.props.images[id].RepoTags || [];
+ if (!intermediateOpened && tags.length < 1)
+ return false;
+ if (this.props.textFilter.length > 0)
+ return tags.some(tag => tag.toLowerCase().indexOf(this.props.textFilter.toLowerCase()) >= 0);
+ return true;
+ });
+ }
+
+ filtered.sort((a, b) => {
+ // User images are in front of system ones
+ if (this.props.images[a].isSystem !== this.props.images[b].isSystem)
+ return this.props.images[a].isSystem ? 1 : -1;
+ const name_a = this.props.images[a].RepoTags ? this.props.images[a].RepoTags[0] : "";
+ const name_b = this.props.images[b].RepoTags ? this.props.images[b].RepoTags[0] : "";
+ if (name_a === "")
+ return 1;
+ if (name_b === "")
+ return -1;
+ return name_a > name_b ? 1 : -1;
+ });
+
+ const imageRows = filtered.map(id => this.renderRow(this.props.images[id]));
+
+ const interim = this.props.images && Object.keys(this.props.images).some(id => {
+ // Intermediate image does not have any tags
+ if (this.props.images[id].RepoTags && this.props.images[id].RepoTags.length > 0)
+ return false;
+
+ // Only filter by selected user
+ if (this.props.userServiceAvailable && this.props.systemServiceAvailable && this.props.ownerFilter !== "all") {
+ if (this.props.ownerFilter === "system" && !this.props.images[id].isSystem)
+ return false;
+ if (this.props.ownerFilter !== "system" && this.props.images[id].isSystem)
+ return false;
+ }
+
+ // Any text filter hides all images
+ if (this.props.textFilter.length > 0)
+ return false;
+
+ return true;
+ });
+
+ let toggleIntermediate = "";
+ if (interim) {
+ toggleIntermediate = (
+ <span className="listing-action">
+ <Button variant="link" onClick={() => this.setState({ intermediateOpened: !intermediateOpened, isExpanded: true })}>
+ {intermediateOpened ? _("Hide intermediate images") : _("Show intermediate images")}</Button>
+ </span>
+ );
+ }
+ const cardBody = (
+ <>
+ <ListingTable aria-label={_("Images")}
+ variant='compact'
+ emptyCaption={emptyCaption}
+ columns={columnTitles}
+ rows={imageRows} />
+ {toggleIntermediate}
+ </>
+ );
+
+ const { imageStats, unusedImages } = this.calculateStats();
+ const imageTitleStats = (
+ <>
+ <Text component={TextVariants.h5}>
+ {cockpit.format(cockpit.ngettext("$0 image total, $1", "$0 images total, $1", imageStats.imagesTotal), imageStats.imagesTotal, cockpit.format_bytes(imageStats.imagesSize, 1000))}
+ </Text>
+ {imageStats.unusedTotal !== 0 &&
+ <Text component={TextVariants.h5}>
+ {cockpit.format(cockpit.ngettext("$0 unused image, $1", "$0 unused images, $1", imageStats.unusedTotal), imageStats.unusedTotal, cockpit.format_bytes(imageStats.unusedSize, 1000))}
+ </Text>
+ }
+ </>
+ );
+
+ return (
+ <Card id="containers-images" key="images" className="containers-images">
+ <CardHeader>
+ <Flex flexWrap={{ default: 'nowrap' }} className="pf-v5-u-w-100">
+ <FlexItem grow={{ default: 'grow' }}>
+ <Flex>
+ <CardTitle>
+ <Text component={TextVariants.h2} className="containers-images-title">{_("Images")}</Text>
+ </CardTitle>
+ <Flex className="ignore-pixels" style={{ rowGap: "var(--pf-v5-global--spacer--xs)" }}>{imageTitleStats}</Flex>
+ </Flex>
+ </FlexItem>
+ <FlexItem>
+ <ImageOverActions handleDownloadNewImage={this.onOpenNewImagesDialog}
+ handlePruneUsedImages={this.onOpenPruneUnusedImagesDialog}
+ unusedImages={unusedImages} />
+ </FlexItem>
+ </Flex>
+ </CardHeader>
+ <CardBody>
+ {this.props.images && Object.keys(this.props.images).length
+ ? <ExpandableSection toggleText={this.state.isExpanded ? _("Hide images") : _("Show images")}
+ onToggle={() => this.setState(prevState => ({ isExpanded: !prevState.isExpanded }))}
+ isExpanded={this.state.isExpanded}>
+ {cardBody}
+ </ExpandableSection>
+ : cardBody}
+ </CardBody>
+ {/* The PruneUnusedImagesModal dialog needs to keep
+ * its list of unused images in sync with reality at
+ * all times since the API call will delete whatever
+ * is unused at the exact time of call, and the
+ * dialog better be showing the correct list of
+ * unused images at that time. Thus, we can't use
+ * Dialog.show for it but include it here in the
+ * DOM. */}
+ {this.state.showPruneUnusedImagesModal &&
+ <PruneUnusedImagesModal
+ close={() => this.setState({ showPruneUnusedImagesModal: false })}
+ unusedImages={unusedImages}
+ onAddNotification={this.props.onAddNotification}
+ userServiceAvailable={this.props.userServiceAvailable}
+ systemServiceAvailable={this.props.systemServiceAvailable} /> }
+ {this.state.imageDownloadInProgress && <CardFooter>
+ <div className='download-in-progress'> {_("Pulling")} {this.state.imageDownloadInProgress}... </div>
+ </CardFooter>}
+ </Card>
+ );
+ }
+}
+
+const ImageOverActions = ({ handleDownloadNewImage, handlePruneUsedImages, unusedImages }) => {
+ const actions = [
+ <DropdownItem
+ key="download-new-image"
+ component="button"
+ onClick={() => handleDownloadNewImage()}
+ >
+ {_("Download new image")}
+ </DropdownItem>,
+ <DropdownItem
+ key="prune-unused-images"
+ id="prune-unused-images-button"
+ component="button"
+ className="pf-m-danger btn-delete"
+ onClick={() => handlePruneUsedImages()}
+ isDisabled={unusedImages.length === 0}
+ isAriaDisabled={unusedImages.length === 0}
+ >
+ {_("Prune unused images")}
+ </DropdownItem>
+ ];
+
+ return (
+ <KebabDropdown
+ toggleButtonId="image-actions-dropdown"
+ position="right"
+ dropdownItems={actions}
+ />
+ );
+};
+
+const ImageActions = ({ image, onAddNotification, user, systemServiceAvailable, userServiceAvailable }) => {
+ const Dialogs = useDialogs();
+
+ const runImage = () => {
+ Dialogs.show(
+ <utils.PodmanInfoContext.Consumer>
+ {(podmanInfo) => (
+ <DialogsContext.Consumer>
+ {(Dialogs) => (
+ <ImageRunModal
+ systemServiceAvailable={systemServiceAvailable}
+ userServiceAvailable={userServiceAvailable}
+ user={user}
+ image={image}
+ onAddNotification={onAddNotification}
+ podmanInfo={podmanInfo}
+ dialogs={Dialogs}
+ />
+ )}
+ </DialogsContext.Consumer>
+ )}
+ </utils.PodmanInfoContext.Consumer>);
+ };
+
+ const removeImage = () => {
+ Dialogs.show(<ImageDeleteModal imageWillDelete={image}
+ onAddNotification={onAddNotification} />);
+ };
+
+ const runImageAction = (
+ <Button key={image.Id + "create"}
+ className="ct-container-create show-only-when-wide"
+ variant='secondary'
+ onClick={ e => {
+ e.stopPropagation();
+ runImage();
+ }}
+ size="sm"
+ data-image={image.Id}>
+ {_("Create container")}
+ </Button>
+ );
+
+ const dropdownActions = [
+ <DropdownItem key={image.Id + "create-menu"}
+ component="button"
+ className="show-only-when-narrow"
+ onClick={runImage}>
+ {_("Create container")}
+ </DropdownItem>,
+ <DropdownItem key={image.Id + "delete"}
+ component="button"
+ className="pf-m-danger btn-delete"
+ onClick={removeImage}>
+ {_("Delete")}
+ </DropdownItem>
+ ];
+
+ return (
+ <>
+ {runImageAction}
+ <KebabDropdown position="right" dropdownItems={dropdownActions} />
+ </>
+ );
+};
+
+export default Images;
diff --git a/src/Notification.jsx b/src/Notification.jsx
new file mode 100644
index 0000000..3fc3ff2
--- /dev/null
+++ b/src/Notification.jsx
@@ -0,0 +1,45 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+import React from 'react';
+import { Alert, AlertActionCloseButton } from "@patternfly/react-core/dist/esm/components/Alert";
+
+import cockpit from 'cockpit';
+
+const _ = cockpit.gettext;
+
+let last_error = "";
+
+function log_error_if_changed(error) {
+ // Put the error in the browser log, for easier debugging and
+ // matching of known issues in the integration tests.
+ if (error != last_error) {
+ last_error = error;
+ console.error(error);
+ }
+}
+
+export const ErrorNotification = ({ errorMessage, errorDetail, onDismiss }) => {
+ log_error_if_changed(errorMessage + (errorDetail ? ": " + errorDetail : ""));
+ return (
+ <Alert isInline variant='danger' title={errorMessage}
+ actionClose={onDismiss ? <AlertActionCloseButton onClose={onDismiss} /> : null}>
+ { errorDetail && <p> {_("Error message")}: <samp>{errorDetail}</samp> </p> }
+ </Alert>
+ );
+};
diff --git a/src/PodActions.jsx b/src/PodActions.jsx
new file mode 100644
index 0000000..c01ad77
--- /dev/null
+++ b/src/PodActions.jsx
@@ -0,0 +1,195 @@
+import React, { useState } from 'react';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Alert } from "@patternfly/react-core/dist/esm/components/Alert";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Divider } from '@patternfly/react-core/dist/esm/components/Divider/index.js';
+import { DropdownItem } from '@patternfly/react-core/dist/esm/components/Dropdown/index.js';
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack";
+
+import cockpit from 'cockpit';
+import { useDialogs } from "dialogs.jsx";
+import { KebabDropdown } from "cockpit-components-dropdown.jsx";
+
+import * as client from './client.js';
+
+const _ = cockpit.gettext;
+
+const PodDeleteModal = ({ pod }) => {
+ const Dialogs = useDialogs();
+ const [deleteError, setDeleteError] = useState(null);
+ const [force, setForce] = useState(false);
+
+ const containers = (pod.Containers || []).filter(ct => ct.Id !== pod.InfraId);
+
+ function handlePodDelete() {
+ client.delPod(pod.isSystem, pod.Id, force)
+ .then(() => {
+ Dialogs.close();
+ })
+ .catch(ex => {
+ setDeleteError(ex.message);
+ setForce(true);
+ });
+ }
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ titleIconVariant="warning"
+ title={force
+ ? cockpit.format(_("Force delete pod $0?"), pod.Name)
+ : cockpit.format(_("Delete pod $0?"), pod.Name)}
+ onClose={Dialogs.close}
+ footer={<>
+ <Button variant="danger"
+ onClick={handlePodDelete}>
+ {force ? _("Force delete") : _("Delete")}
+ </Button>
+ {' '}
+ <Button variant="link" onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ {deleteError &&
+ <Alert variant="danger" isInline title={_("An error occurred")}>{deleteError}</Alert>}
+ {containers.length === 0 &&
+ <p>{cockpit.format(_("Empty pod $0 will be permanently removed."), pod.Name)}</p>
+ }
+ {containers.length > 0 &&
+ <Stack hasGutter>
+ <p>{_("Deleting this pod will remove the following containers:")}</p>
+ <List>
+ {containers.map(container => <ListItem key={container.Names}>{container.Names}</ListItem>)}
+ </List>
+ </Stack>}
+ </Modal>
+ );
+};
+
+export const PodActions = ({ onAddNotification, pod }) => {
+ const Dialogs = useDialogs();
+
+ const dropdownItems = [];
+ // Possible Pod Statuses can be found here https://github.com/containers/podman/blob/main/libpod/define/podstate.go
+ if (pod.Status == "Running" || pod.Status == "Paused") {
+ dropdownItems.push(
+ <DropdownItem key="action-stop"
+ className="pod-action-stop"
+ onClick={() =>
+ client.postPod(pod.isSystem, "stop", pod.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to stop pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Stop")}
+ </DropdownItem>,
+ <DropdownItem key="action-force-stop"
+ className="pod-action-force-stop"
+ onClick={() =>
+ client.postPod(pod.isSystem, "stop", pod.Id, { t: 0 })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to force stop pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Force stop")}
+ </DropdownItem>,
+ <DropdownItem key="action-restart"
+ className="pod-action-restart"
+ onClick={() =>
+ client.postPod(pod.isSystem, "restart", pod.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to restart pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Restart")}
+ </DropdownItem>,
+ <DropdownItem key="action-force-restart"
+ className="pod-action-force-restart"
+ onClick={() =>
+ client.postPod(pod.isSystem, "restart", pod.Id, { t: 0 })
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to force restart pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Force restart")}
+ </DropdownItem>,
+ );
+ }
+ if (pod.Status == "Created" || pod.Status == "Exited" || pod.Status == "Stopped") {
+ dropdownItems.push(
+ <DropdownItem key="action-start"
+ className="pod-action-start"
+ onClick={() =>
+ client.postPod(pod.isSystem, "start", pod.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to start pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Start")}
+ </DropdownItem>,
+ );
+ }
+ if (pod.Status == "Paused") {
+ dropdownItems.push(
+ <DropdownItem key="action-unpause"
+ className="pod-action-unpause"
+ onClick={() =>
+ client.postPod(pod.isSystem, "unpause", pod.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to resume pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Resume")}
+ </DropdownItem>,
+ );
+ }
+ if (pod.Status == "Running") {
+ dropdownItems.push(
+ <DropdownItem key="action-pause"
+ className="pod-action-pause"
+ onClick={() =>
+ client.postPod(pod.isSystem, "pause", pod.Id, {})
+ .catch(ex => {
+ const error = cockpit.format(_("Failed to pause pod $0"), pod.Name);
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ })}
+ component="button">
+ {_("Pause")}
+ </DropdownItem>,
+ );
+ }
+
+ if (dropdownItems.length > 1) {
+ dropdownItems.push(<Divider key="separator-1" />);
+ }
+ dropdownItems.push(
+ <DropdownItem key="action-delete"
+ className="pod-action-delete pf-m-danger"
+ onClick={() => {
+ Dialogs.show(<PodDeleteModal pod={pod} />);
+ }}
+ component="button">
+ {_("Delete")}
+ </DropdownItem>,
+ );
+
+ if (!dropdownItems.length)
+ return null;
+
+ return (
+ <KebabDropdown
+ toggleButtonId={"pod-" + pod.Name + (pod.isSystem ? "-system" : "-user") + "-action-toggle"}
+ position="right"
+ dropdownItems={dropdownItems}
+ />
+ );
+};
diff --git a/src/PodCreateModal.jsx b/src/PodCreateModal.jsx
new file mode 100644
index 0000000..fe34294
--- /dev/null
+++ b/src/PodCreateModal.jsx
@@ -0,0 +1,220 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Form, FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { Radio } from "@patternfly/react-core/dist/esm/components/Radio";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import * as dockerNames from 'docker-names';
+
+import { FormHelper } from 'cockpit-components-form-helper.jsx';
+import { DynamicListForm } from 'cockpit-components-dynamic-list.jsx';
+import { ErrorNotification } from './Notification.jsx';
+import { PublishPort, validatePublishPort } from './PublishPort.jsx';
+import { Volume } from './Volume.jsx';
+import * as client from './client.js';
+import * as utils from './util.js';
+import cockpit from 'cockpit';
+import { useDialogs } from "dialogs.jsx";
+
+const _ = cockpit.gettext;
+
+const systemOwner = "system";
+
+export const PodCreateModal = ({ user, systemServiceAvailable, userServiceAvailable }) => {
+ const { version, selinuxAvailable } = utils.usePodmanInfo();
+ const [podName, setPodName] = useState(dockerNames.getRandomName());
+ const [publish, setPublish] = useState([]);
+ const [volumes, setVolumes] = useState([]);
+ const [owner, setOwner] = useState(systemServiceAvailable ? systemOwner : user);
+ const [dialogError, setDialogError] = useState(null);
+ const [dialogErrorDetail, setDialogErrorDetail] = useState(null);
+ const [validationFailed, setValidationFailed] = useState({});
+ const Dialogs = useDialogs();
+
+ const getCreateConfig = () => {
+ const createConfig = {};
+
+ if (podName)
+ createConfig.name = podName;
+
+ if (publish.length > 0)
+ createConfig.portmappings = publish
+ .filter(port => port?.containerPort)
+ .map(port => {
+ const pm = { container_port: parseInt(port.containerPort), protocol: port.protocol };
+ if (port.hostPort !== null)
+ pm.host_port = parseInt(port.hostPort);
+ if (port.IP !== null)
+ pm.host_ip = port.IP;
+ return pm;
+ });
+
+ if (volumes.length > 0) {
+ createConfig.mounts = volumes
+ .filter(volume => volume?.hostPath && volume?.containerPath)
+ .map(volume => {
+ const record = { source: volume.hostPath, destination: volume.containerPath, type: "bind" };
+ record.options = [];
+ if (volume.mode)
+ record.options.push(volume.mode);
+ if (volume.selinux)
+ record.options.push(volume.selinux);
+ return record;
+ });
+ }
+
+ return createConfig;
+ };
+
+ /* Updates a validation object of the whole dynamic list's form (e.g. the whole port-mapping form)
+ *
+ * Arguments
+ * - key: [publish/volumes/env] - Specifies the validation of which dynamic form of the Image run dialog is being updated
+ * - value: An array of validation errors of the form. Each item of the array represents a row of the dynamic list.
+ * Index needs to corellate with a row number
+ */
+ const dynamicListOnValidationChange = (key, value) => {
+ setValidationFailed(prevState => {
+ prevState[key] = value;
+ if (prevState[key].every(a => a === undefined))
+ delete prevState[key];
+ return prevState;
+ });
+ };
+
+ const createPod = (isSystem, createConfig) => {
+ client.createPod(isSystem, createConfig)
+ .then(() => Dialogs.close())
+ .catch(ex => {
+ setDialogError(_("Pod failed to be created"));
+ setDialogErrorDetail(cockpit.format("$0: $1", ex.reason, ex.message));
+ });
+ };
+
+ const onCreateClicked = () => {
+ if (!validateForm())
+ return;
+ const createConfig = getCreateConfig();
+ createPod(owner === systemOwner, createConfig);
+ };
+
+ const isFormInvalid = validationFailed => {
+ const groupHasError = row => row && Object.values(row)
+ .filter(val => val) // Filter out empty/undefined properties
+ .length > 0; // If one field has error, the whole group (dynamicList) is invalid
+
+ // If at least one group is invalid, then the whole form is invalid
+ return validationFailed.publish?.some(groupHasError) ||
+ !!validationFailed.podName;
+ };
+
+ const validatePodName = value => {
+ if (!utils.is_valid_container_name(value))
+ return _("Invalid characters. Name can only contain letters, numbers, and certain punctuation (_ . -).");
+ };
+
+ const validateForm = () => {
+ const newValidationFailed = { };
+
+ const publishValidation = publish.map(a => {
+ if (a === undefined)
+ return undefined;
+
+ return {
+ IP: validatePublishPort(a.IP, "IP"),
+ hostPort: validatePublishPort(a.hostPort, "hostPort"),
+ containerPort: validatePublishPort(a.containerPort, "containerPort"),
+ };
+ });
+ if (publishValidation.some(entry => entry && Object.keys(entry).length > 0))
+ newValidationFailed.publish = publishValidation.filter(entry => entry !== undefined);
+
+ const podNameValidation = validatePodName(podName);
+
+ if (podNameValidation)
+ newValidationFailed.containerName = podNameValidation;
+
+ setValidationFailed(newValidationFailed);
+ return !isFormInvalid(newValidationFailed);
+ };
+
+ const defaultBody = (
+ <Form>
+ {dialogError && <ErrorNotification errorMessage={dialogError} errorDetail={dialogErrorDetail} />}
+ <FormGroup id="pod-name-group" fieldId='create-pod-dialog-name' label={_("Name")} className="ct-m-horizontal">
+ <TextInput id='create-pod-dialog-name'
+ className="pod-name"
+ placeholder={_("Pod name")}
+ value={podName}
+ validated={validationFailed.podName ? "error" : "default"}
+ onChange={(_, value) => {
+ utils.validationClear(validationFailed, "podName", (value) => setValidationFailed(value));
+ utils.validationDebounce(() => {
+ const delta = validatePodName(value);
+ if (delta)
+ setValidationFailed(prevState => { return { ...prevState, podName: delta } });
+ });
+ setPodName(value);
+ }} />
+ <FormHelper fieldId="create-pod-dialog-name" helperTextInvalid={validationFailed?.podName} />
+ </FormGroup>
+ { userServiceAvailable && systemServiceAvailable &&
+ <FormGroup isInline hasNoPaddingTop fieldId='create-pod-dialog-owner' label={_("Owner")} className="ct-m-horizontal">
+ <Radio value={systemOwner}
+ label={_("System")}
+ id="create-pod-dialog-owner-system"
+ isChecked={owner === systemOwner}
+ onChange={() => setOwner(systemOwner)} />
+ <Radio value={user}
+ label={cockpit.format("$0 $1", _("User:"), user)}
+ id="create-pod-dialog-owner-user"
+ isChecked={owner === user}
+ onChange={() => setOwner(user)} />
+ </FormGroup>
+ }
+ <DynamicListForm id='create-pod-dialog-publish'
+ emptyStateString={_("No ports exposed")}
+ formclass='publish-port-form'
+ label={_("Port mapping")}
+ actionLabel={_("Add port mapping")}
+ validationFailed={validationFailed.publish}
+ onValidationChange={value => dynamicListOnValidationChange('publish', value)}
+ onChange={value => setPublish(value)}
+ default={{ IP: null, containerPort: null, hostPort: null, protocol: 'tcp' }}
+ itemcomponent={ <PublishPort />} />
+
+ {version.localeCompare("4", undefined, { numeric: true, sensitivity: 'base' }) >= 0 &&
+ <DynamicListForm id='create-pod-dialog-volume'
+ emptyStateString={_("No volumes specified")}
+ formclass='volume-form'
+ label={_("Volumes")}
+ actionLabel={_("Add volume")}
+ onChange={value => setVolumes(value)}
+ default={{ containerPath: null, hostPath: null, mode: 'rw' }}
+ options={{ selinuxAvailable }}
+ itemcomponent={ <Volume />} />
+ }
+
+ </Form>
+ );
+
+ return (
+ <Modal isOpen
+ position="top" variant="medium"
+ onClose={Dialogs.close}
+ onEscapePress={Dialogs.close}
+ title={_("Create pod")}
+ footer={<>
+ <Button variant='primary' id="create-pod-create-btn" onClick={() => onCreateClicked()}
+ isDisabled={isFormInvalid(validationFailed)}>
+ {_("Create")}
+ </Button>
+ <Button variant='link' className='btn-cancel' onClick={Dialogs.close}>
+ {_("Cancel")}
+ </Button>
+ </>}
+ >
+ {defaultBody}
+ </Modal>
+ );
+};
diff --git a/src/PruneUnusedContainersModal.jsx b/src/PruneUnusedContainersModal.jsx
new file mode 100644
index 0000000..5f09c87
--- /dev/null
+++ b/src/PruneUnusedContainersModal.jsx
@@ -0,0 +1,110 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import { SortByDirection } from "@patternfly/react-table";
+import cockpit from 'cockpit';
+import { ListingTable } from 'cockpit-components-table.jsx';
+
+import * as client from './client.js';
+import * as utils from './util.js';
+
+const _ = cockpit.gettext;
+
+const getContainerRow = (container, userSystemServiceAvailable, user, selected) => {
+ const columns = [
+ {
+ title: container.name,
+ sortKey: container.name,
+ props: { width: 25, },
+ },
+ {
+ title: utils.localize_time(Date.parse(container.created) / 1000),
+ props: { width: 20, },
+ },
+ ];
+
+ if (userSystemServiceAvailable)
+ columns.push({
+ title: container.system ? _("system") : <div><span className="ct-grey-text">{_("user:")} </span>{user}</div>,
+ sortKey: container.system.toString(),
+ props: {
+ className: "ignore-pixels",
+ modifier: "nowrap"
+ }
+ });
+
+ return { columns, selected, props: { key: container.id } };
+};
+
+const PruneUnusedContainersModal = ({ close, unusedContainers, onAddNotification, userSystemServiceAvailable, user }) => {
+ const [isPruning, setPruning] = useState(false);
+ const [selectedContainerIds, setSelectedContainerIds] = React.useState(unusedContainers.map(u => u.id));
+
+ const handlePruneUnusedContainers = () => {
+ setPruning(true);
+
+ const actions = [];
+
+ for (const id of selectedContainerIds) {
+ if (id.endsWith("true")) {
+ actions.push(client.delContainer(true, id.replace(/true$/, ""), true));
+ } else {
+ actions.push(client.delContainer(false, id.replace(/false$/, ""), true));
+ }
+ }
+ Promise.all(actions).then(close)
+ .catch(ex => {
+ const error = _("Failed to prune unused containers");
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ close();
+ });
+ };
+
+ const columns = [
+ { title: _("Name"), sortable: true },
+ { title: _("Created"), sortable: true },
+ ];
+
+ if (userSystemServiceAvailable)
+ columns.push({ title: _("Owner"), sortable: true });
+
+ const selectAllContainers = isSelecting => setSelectedContainerIds(isSelecting ? unusedContainers.map(c => c.id) : []);
+ const isContainerSelected = container => selectedContainerIds.includes(container.id);
+ const setContainerSelected = (container, isSelecting) => setSelectedContainerIds(prevSelected => {
+ const otherSelectedContainerNames = prevSelected.filter(r => r !== container.id);
+ return isSelecting ? [...otherSelectedContainerNames, container.id] : otherSelectedContainerNames;
+ });
+
+ const onSelectContainer = (id, _rowIndex, isSelecting) => {
+ const container = unusedContainers.filter(u => u.id === id)[0];
+ setContainerSelected(container, isSelecting);
+ };
+
+ return (
+ <Modal isOpen
+ onClose={close}
+ position="top" variant="medium"
+ title={cockpit.format(_("Prune unused containers"))}
+ footer={<>
+ <Button id="btn-img-delete" variant="danger"
+ spinnerAriaValueText={isPruning ? _("Pruning containers") : undefined}
+ isLoading={isPruning}
+ isDisabled={isPruning || selectedContainerIds.length === 0}
+ onClick={handlePruneUnusedContainers}>
+ {isPruning ? _("Pruning containers") : _("Prune")}
+ </Button>
+ <Button variant="link" onClick={() => close()}>{_("Cancel")}</Button>
+ </>}
+ >
+ <p>{_("Removes selected non-running containers")}</p>
+ <ListingTable columns={columns}
+ onSelect={(_event, isSelecting, rowIndex, rowData) => onSelectContainer(rowData.props.id, rowIndex, isSelecting)}
+ onHeaderSelect={(_event, isSelecting) => selectAllContainers(isSelecting)}
+ id="unused-container-list"
+ rows={unusedContainers.map(container => getContainerRow(container, userSystemServiceAvailable, user, isContainerSelected(container))) }
+ variant="compact" sortBy={{ index: 0, direction: SortByDirection.asc }} />
+ </Modal>
+ );
+};
+
+export default PruneUnusedContainersModal;
diff --git a/src/PruneUnusedImagesModal.jsx b/src/PruneUnusedImagesModal.jsx
new file mode 100644
index 0000000..a2e1919
--- /dev/null
+++ b/src/PruneUnusedImagesModal.jsx
@@ -0,0 +1,123 @@
+import React, { useState } from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { Flex } from "@patternfly/react-core/dist/esm/layouts/Flex";
+import { List, ListItem } from "@patternfly/react-core/dist/esm/components/List";
+import { Modal } from "@patternfly/react-core/dist/esm/components/Modal";
+import cockpit from 'cockpit';
+
+import * as client from './client.js';
+import * as utils from './util.js';
+
+import "@patternfly/patternfly/utilities/Spacing/spacing.css";
+
+const _ = cockpit.gettext;
+
+function ImageOptions({ images, checked, isSystem, handleChange, name, showCheckbox }) {
+ const [isExpanded, onToggle] = useState(false);
+ let shownImages = images;
+ if (!isExpanded) {
+ shownImages = shownImages.slice(0, 5);
+ }
+
+ if (shownImages.length === 0) {
+ return null;
+ }
+ const listNameId = "list-" + name;
+
+ return (
+ <Flex flex={{ default: 'column' }}>
+ {showCheckbox &&
+ <Checkbox
+ label={isSystem ? _("Delete unused system images:") : _("Delete unused user images:")}
+ isChecked={checked}
+ id={name}
+ name={name}
+ onChange={(_, val) => handleChange(val)}
+ aria-owns={listNameId}
+ />
+ }
+ <List id={listNameId}>
+ {shownImages.map((image, index) =>
+ <ListItem className="pf-v5-u-ml-md" key={index}>
+ {utils.image_name(image)}
+ </ListItem>
+ )}
+ {!isExpanded && images.length > 5 &&
+ <Button onClick={onToggle} variant="link" isInline>
+ {_("Show more")}
+ </Button>
+ }
+ </List>
+ </Flex>
+ );
+}
+
+const PruneUnusedImagesModal = ({ close, unusedImages, onAddNotification, userServiceAvailable, systemServiceAvailable }) => {
+ const [isPruning, setPruning] = useState(false);
+ const [deleteUserImages, setDeleteUserImages] = useState(userServiceAvailable !== null && userServiceAvailable);
+ const [deleteSystemImages, setDeleteSystemImages] = useState(systemServiceAvailable);
+
+ const handlePruneUnusedImages = () => {
+ setPruning(true);
+
+ const actions = [];
+ if (deleteUserImages) {
+ actions.push(client.pruneUnusedImages(false));
+ }
+ if (deleteSystemImages) {
+ actions.push(client.pruneUnusedImages(true));
+ }
+ Promise.all(actions).then(close)
+ .catch(ex => {
+ const error = _("Failed to prune unused images");
+ onAddNotification({ type: 'danger', error, errorDetail: ex.message });
+ close();
+ });
+ };
+
+ const isSystem = systemServiceAvailable;
+ const userImages = unusedImages.filter(image => !image.isSystem);
+ const systemImages = unusedImages.filter(image => image.isSystem);
+ const showCheckboxes = userImages.length > 0 && systemImages.length > 0;
+
+ return (
+ <Modal isOpen
+ onClose={close}
+ position="top" variant="medium"
+ title={cockpit.format(_("Prune unused images"))}
+ footer={<>
+ <Button id="btn-img-delete" variant="danger"
+ spinnerAriaValueText={isPruning ? _("Pruning images") : undefined}
+ isLoading={isPruning}
+ isDisabled={!deleteUserImages && !deleteSystemImages}
+ onClick={handlePruneUnusedImages}>
+ {isPruning ? _("Pruning images") : _("Prune")}
+ </Button>
+ <Button variant="link" onClick={() => close()}>{_("Cancel")}</Button>
+ </>}
+ >
+ <Flex flex={{ default: 'column' }}>
+ {isSystem && <ImageOptions
+ images={systemImages}
+ name="deleteSystemImages"
+ checked={deleteSystemImages}
+ handleChange={setDeleteSystemImages}
+ showCheckbox={showCheckboxes}
+ isSystem
+ />
+ }
+ <ImageOptions
+ images={userImages}
+ name="deleteUserImages"
+ checked={deleteUserImages}
+ handleChange={setDeleteUserImages}
+ showCheckbox={showCheckboxes}
+ isSystem={false}
+ />
+ </Flex>
+ </Modal>
+ );
+};
+
+export default PruneUnusedImagesModal;
diff --git a/src/PublishPort.jsx b/src/PublishPort.jsx
new file mode 100644
index 0000000..92a36cd
--- /dev/null
+++ b/src/PublishPort.jsx
@@ -0,0 +1,142 @@
+import React from 'react';
+
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Grid } from "@patternfly/react-core/dist/esm/layouts/Grid";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { Popover } from "@patternfly/react-core/dist/esm/components/Popover";
+import { OutlinedQuestionCircleIcon, TrashIcon } from '@patternfly/react-icons';
+import cockpit from 'cockpit';
+import ipaddr from "ipaddr.js";
+
+import { FormHelper } from "cockpit-components-form-helper.jsx";
+import * as utils from './util.js';
+
+const _ = cockpit.gettext;
+
+const MAX_PORT = 65535;
+
+export function validatePublishPort(value, key) {
+ switch (key) {
+ case "IP":
+ if (value && !ipaddr.isValid(value))
+ return _("Must be a valid IP address");
+ break;
+ case "hostPort": {
+ if (value) {
+ const hostPort = parseInt(value);
+ if (hostPort < 1 || hostPort > MAX_PORT)
+ return _("1 to 65535");
+ }
+
+ break;
+ }
+ case "containerPort": {
+ if (!value)
+ return _("Container port must not be empty");
+
+ const containerPort = parseInt(value);
+ if (containerPort < 1 || containerPort > MAX_PORT)
+ return _("1 to 65535");
+
+ break;
+ }
+ default:
+ console.error(`Unknown key "${key}"`); // not-covered: unreachable assertion
+ }
+}
+
+export const PublishPort = ({ id, item, onChange, idx, removeitem, itemCount, validationFailed, onValidationChange }) =>
+ (
+ <Grid hasGutter id={id}>
+ <FormGroup className="pf-m-6-col-on-md"
+ id={id + "-ip-address-group"}
+ label={_("IP address")}
+ fieldId={id + "-ip-address"}
+ labelIcon={
+ <Popover aria-label={_("IP address help")}
+ enableFlip
+ bodyContent={_("If host IP is set to 0.0.0.0 or not set at all, the port will be bound on all IPs on the host.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <TextInput id={id + "-ip-address"}
+ value={item.IP || ''}
+ validated={validationFailed?.IP ? "error" : "default"}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "IP", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, IP: validatePublishPort(value, "IP") }));
+ onChange(idx, 'IP', value);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.IP} />
+ </FormGroup>
+ <FormGroup className="pf-m-2-col-on-md"
+ id={id + "-host-port-group"}
+ label={_("Host port")}
+ fieldId={id + "-host-port"}
+ labelIcon={
+ <Popover aria-label={_("Host port help")}
+ enableFlip
+ bodyContent={_("If the host port is not set the container port will be randomly assigned a port on the host.")}>
+ <button onClick={e => e.preventDefault()} className="pf-v5-c-form__group-label-help">
+ <OutlinedQuestionCircleIcon />
+ </button>
+ </Popover>
+ }>
+ <TextInput id={id + "-host-port"}
+ type='number'
+ step={1}
+ min={1}
+ max={MAX_PORT}
+ value={item.hostPort || ''}
+ validated={validationFailed?.hostPort ? "error" : "default"}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "hostPort", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, hostPort: validatePublishPort(value, "hostPort") }));
+ onChange(idx, 'hostPort', value);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.hostPort} />
+ </FormGroup>
+ <FormGroup className="pf-m-2-col-on-md"
+ id={id + "-container-port-group"}
+ label={_("Container port")}
+ fieldId={id + "-container-port"} isRequired>
+ <TextInput id={id + "-container-port"}
+ type='number'
+ step={1}
+ min={1}
+ max={MAX_PORT}
+ validated={validationFailed?.containerPort ? "error" : "default"}
+ value={item.containerPort || ''}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "containerPort", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, containerPort: validatePublishPort(value, "containerPort") }));
+ onChange(idx, 'containerPort', value);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.containerPort} />
+ </FormGroup>
+ <FormGroup className="pf-m-2-col-on-md"
+ label={_("Protocol")}
+ fieldId={id + "-protocol"}>
+ <FormSelect className='pf-v5-c-form-control container-port-protocol'
+ id={id + "-protocol"}
+ value={item.protocol}
+ onChange={(_event, value) => onChange(idx, 'protocol', value)}>
+ <FormSelectOption value='tcp' key='tcp' label={_("TCP")} />
+ <FormSelectOption value='udp' key='udp' label={_("UDP")} />
+ </FormSelect>
+ </FormGroup>
+ <FormGroup className="pf-m-1-col-on-md remove-button-group">
+ <Button variant='plain'
+ className="btn-close"
+ id={id + "-btn-close"}
+ size="sm"
+ aria-label={_("Remove item")}
+ icon={<TrashIcon />}
+ onClick={() => removeitem(idx)} />
+ </FormGroup>
+ </Grid>
+ );
diff --git a/src/Volume.jsx b/src/Volume.jsx
new file mode 100644
index 0000000..988ea42
--- /dev/null
+++ b/src/Volume.jsx
@@ -0,0 +1,90 @@
+import React from 'react';
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { FormGroup } from "@patternfly/react-core/dist/esm/components/Form";
+import { FormHelper } from "cockpit-components-form-helper.jsx";
+import { FormSelect, FormSelectOption } from "@patternfly/react-core/dist/esm/components/FormSelect";
+import { Grid } from "@patternfly/react-core/dist/esm/layouts/Grid";
+import { TextInput } from "@patternfly/react-core/dist/esm/components/TextInput";
+import { TrashIcon } from '@patternfly/react-icons';
+import { FileAutoComplete } from 'cockpit-components-file-autocomplete.jsx';
+import cockpit from 'cockpit';
+
+import * as utils from './util.js';
+
+const _ = cockpit.gettext;
+
+export function validateVolume(value, key) {
+ switch (key) {
+ case "hostPath":
+ break;
+ case "containerPath":
+ if (!value)
+ return _("Container path must not be empty");
+
+ break;
+ default:
+ console.error(`Unknown key "${key}"`); // not-covered: unreachable assertion
+ }
+}
+
+export const Volume = ({ id, item, onChange, idx, removeitem, additem, options, itemCount, validationFailed, onValidationChange }) =>
+ (
+ <Grid hasGutter id={id}>
+ <FormGroup className="pf-m-4-col-on-md"
+ id={id + "-host-path-group"}
+ label={_("Host path")}
+ fieldId={id + "-host-path"}
+ >
+ <FileAutoComplete id={id + "-host-path"}
+ value={item.hostPath || ''}
+ onChange={value => {
+ utils.validationClear(validationFailed, "hostPath", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, hostPath: validateVolume(value, "hostPath") }));
+ onChange(idx, 'hostPath', value);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.hostPath} />
+ </FormGroup>
+ <FormGroup className="pf-m-3-col-on-md"
+ id={id + "-container-path-group"}
+ label={_("Container path")}
+ fieldId={id + "-container-path"}
+ isRequired
+ >
+ <TextInput id={id + "-container-path"}
+ value={item.containerPath || ''}
+ validated={validationFailed?.containerPath ? "error" : "default"}
+ onChange={(_event, value) => {
+ utils.validationClear(validationFailed, "containerPath", onValidationChange);
+ utils.validationDebounce(() => onValidationChange({ ...validationFailed, containerPath: validateVolume(value, "containerPath") }));
+ onChange(idx, 'containerPath', value);
+ }} />
+ <FormHelper helperTextInvalid={validationFailed?.containerPath} />
+ </FormGroup>
+ <FormGroup className="pf-m-2-col-on-md" label={_("Mode")} fieldId={id + "-mode"}>
+ <Checkbox id={id + "-mode"}
+ label={_("Writable")}
+ isChecked={item.mode == "rw"}
+ onChange={(_event, value) => onChange(idx, 'mode', value ? "rw" : "ro")} />
+ </FormGroup>
+ { options && options.selinuxAvailable &&
+ <FormGroup className="pf-m-3-col-on-md" label={_("SELinux")} fieldId={id + "-selinux"}>
+ <FormSelect id={id + "-selinux"} className='pf-v5-c-form-control'
+ value={item.selinux}
+ onChange={(_event, value) => onChange(idx, 'selinux', value)}>
+ <FormSelectOption value='' key='' label={_("No label")} />
+ <FormSelectOption value='z' key='z' label={_("Shared")} />
+ <FormSelectOption value='Z' key='Z' label={_("Private")} />
+ </FormSelect>
+ </FormGroup> }
+ <FormGroup className="pf-m-1-col-on-md remove-button-group">
+ <Button variant='plain'
+ className="btn-close"
+ id={id + "-btn-close"}
+ aria-label={_("Remove item")}
+ size="sm"
+ icon={<TrashIcon />}
+ onClick={() => removeitem(idx)} />
+ </FormGroup>
+ </Grid>
+ );
diff --git a/src/app.jsx b/src/app.jsx
new file mode 100644
index 0000000..63496d8
--- /dev/null
+++ b/src/app.jsx
@@ -0,0 +1,791 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import React from 'react';
+import { Page, PageSection, PageSectionVariants } from "@patternfly/react-core/dist/esm/components/Page";
+import { Alert, AlertActionCloseButton, AlertActionLink, AlertGroup } from "@patternfly/react-core/dist/esm/components/Alert";
+import { Button } from "@patternfly/react-core/dist/esm/components/Button";
+import { Checkbox } from "@patternfly/react-core/dist/esm/components/Checkbox";
+import { EmptyState, EmptyStateHeader, EmptyStateFooter, EmptyStateIcon, EmptyStateActions, EmptyStateVariant } from "@patternfly/react-core/dist/esm/components/EmptyState";
+import { Stack } from "@patternfly/react-core/dist/esm/layouts/Stack";
+import { ExclamationCircleIcon } from '@patternfly/react-icons';
+import { WithDialogs } from "dialogs.jsx";
+
+import cockpit from 'cockpit';
+import { superuser } from "superuser";
+import ContainerHeader from './ContainerHeader.jsx';
+import Containers from './Containers.jsx';
+import Images from './Images.jsx';
+import * as client from './client.js';
+import { WithPodmanInfo } from './util.js';
+
+const _ = cockpit.gettext;
+
+class Application extends React.Component {
+ constructor(props) {
+ super(props);
+ this.state = {
+ systemServiceAvailable: null,
+ userServiceAvailable: null,
+ enableService: true,
+ images: null,
+ userImagesLoaded: false,
+ systemImagesLoaded: false,
+ containers: null,
+ containersFilter: "all",
+ containersStats: {},
+ userContainersLoaded: null,
+ systemContainersLoaded: null,
+ userPodsLoaded: null,
+ systemPodsLoaded: null,
+ userServiceExists: false,
+ textFilter: "",
+ ownerFilter: "all",
+ dropDownValue: 'Everything',
+ notifications: [],
+ showStartService: true,
+ version: '1.3.0',
+ selinuxAvailable: false,
+ podmanRestartAvailable: false,
+ userPodmanRestartAvailable: false,
+ currentUser: _("User"),
+ userLingeringEnabled: null,
+ privileged: false,
+ location: {},
+ };
+ this.onAddNotification = this.onAddNotification.bind(this);
+ this.onDismissNotification = this.onDismissNotification.bind(this);
+ this.onFilterChanged = this.onFilterChanged.bind(this);
+ this.onOwnerChanged = this.onOwnerChanged.bind(this);
+ this.onContainerFilterChanged = this.onContainerFilterChanged.bind(this);
+ this.updateContainer = this.updateContainer.bind(this);
+ this.startService = this.startService.bind(this);
+ this.goToServicePage = this.goToServicePage.bind(this);
+ this.checkUserService = this.checkUserService.bind(this);
+ this.onNavigate = this.onNavigate.bind(this);
+
+ this.pendingUpdateContainer = {}; // id+system → promise
+ }
+
+ onAddNotification(notification) {
+ notification.index = this.state.notifications.length;
+
+ this.setState(prevState => ({
+ notifications: [
+ ...prevState.notifications,
+ notification
+ ]
+ }));
+ }
+
+ onDismissNotification(notificationIndex) {
+ const notificationsArray = this.state.notifications.concat();
+ const index = notificationsArray.findIndex(current => current.index == notificationIndex);
+
+ if (index !== -1) {
+ notificationsArray.splice(index, 1);
+ this.setState({ notifications: notificationsArray });
+ }
+ }
+
+ updateUrl(options) {
+ cockpit.location.go([], options);
+ }
+
+ onFilterChanged(value) {
+ this.setState({
+ textFilter: value
+ });
+
+ const options = this.state.location;
+ if (value === "") {
+ delete options.name;
+ this.updateUrl(Object.assign(options));
+ } else {
+ this.updateUrl(Object.assign(this.state.location, { name: value }));
+ }
+ }
+
+ onOwnerChanged(value) {
+ this.setState({
+ ownerFilter: value
+ });
+
+ const options = this.state.location;
+ if (value == "all") {
+ delete options.owner;
+ this.updateUrl(Object.assign(options));
+ } else {
+ this.updateUrl(Object.assign(options, { owner: value }));
+ }
+ }
+
+ onContainerFilterChanged(value) {
+ this.setState({
+ containersFilter: value
+ });
+
+ const options = this.state.location;
+ if (value == "running") {
+ delete options.container;
+ this.updateUrl(Object.assign(options));
+ } else {
+ this.updateUrl(Object.assign(options, { container: value }));
+ }
+ }
+
+ updateState(state, id, newValue) {
+ this.setState(prevState => {
+ return {
+ [state]: { ...prevState[state], [id]: newValue }
+ };
+ });
+ }
+
+ updateContainerStats(system) {
+ client.streamContainerStats(system, reply => {
+ if (reply.Error != null) // executed when container stop
+ console.warn("Failed to update container stats:", JSON.stringify(reply.message));
+ else {
+ reply.Stats.forEach(stat => this.updateState("containersStats", stat.ContainerID + system.toString(), stat));
+ }
+ }).catch(ex => {
+ if (ex.cause == "no support for CGroups V1 in rootless environments" || ex.cause == "Container stats resource only available for cgroup v2") {
+ console.log("This OS does not support CgroupsV2. Some information may be missing.");
+ } else
+ console.warn("Failed to update container stats:", JSON.stringify(ex.message));
+ });
+ }
+
+ initContainers(system) {
+ return client.getContainers(system)
+ .then(containerList => Promise.all(
+ containerList.map(container => client.inspectContainer(system, container.Id))
+ ))
+ .then(containerDetails => {
+ this.setState(prevState => {
+ // keep/copy the containers for !system
+ const copyContainers = {};
+ Object.entries(prevState.containers || {}).forEach(([id, container]) => {
+ if (container.isSystem !== system)
+ copyContainers[id] = container;
+ });
+ for (const detail of containerDetails) {
+ detail.isSystem = system;
+ copyContainers[detail.Id + system.toString()] = detail;
+ }
+
+ return {
+ containers: copyContainers,
+ [system ? "systemContainersLoaded" : "userContainersLoaded"]: true,
+ };
+ });
+ this.updateContainerStats(system);
+ })
+ .catch(console.log);
+ }
+
+ updateImages(system) {
+ client.getImages(system)
+ .then(reply => {
+ this.setState(prevState => {
+ // Copy only images that could not be deleted with this event
+ // So when event from system come, only copy user images and vice versa
+ const copyImages = {};
+ Object.entries(prevState.images || {}).forEach(([Id, image]) => {
+ if (image.isSystem !== system)
+ copyImages[Id] = image;
+ });
+ Object.entries(reply).forEach(([Id, image]) => {
+ image.isSystem = system;
+ copyImages[Id + system.toString()] = image;
+ });
+
+ return {
+ images: copyImages,
+ [system ? "systemImagesLoaded" : "userImagesLoaded"]: true
+ };
+ });
+ })
+ .catch(ex => {
+ console.warn("Failed to do Update Images:", JSON.stringify(ex));
+ });
+ }
+
+ updatePods(system) {
+ return client.getPods(system)
+ .then(reply => {
+ this.setState(prevState => {
+ // Copy only pods that could not be deleted with this event
+ // So when event from system come, only copy user pods and vice versa
+ const copyPods = {};
+ Object.entries(prevState.pods || {}).forEach(([id, pod]) => {
+ if (pod.isSystem !== system)
+ copyPods[id] = pod;
+ });
+ for (const pod of reply || []) {
+ pod.isSystem = system;
+ copyPods[pod.Id + system.toString()] = pod;
+ }
+
+ return {
+ pods: copyPods,
+ [system ? "systemPodsLoaded" : "userPodsLoaded"]: true,
+ };
+ });
+ })
+ .catch(ex => {
+ console.warn("Failed to do Update Pods:", JSON.stringify(ex));
+ });
+ }
+
+ updateContainer(id, system, event) {
+ /* when firing off multiple calls in parallel, podman can return them in a random order.
+ * This messes up the state. So we need to serialize them for a particular container. */
+ const idx = id + system.toString();
+ const wait = this.pendingUpdateContainer[idx] ?? Promise.resolve();
+
+ const new_wait = wait.then(() => client.inspectContainer(system, id))
+ .then(details => {
+ details.isSystem = system;
+ // HACK: during restart State never changes from "running"
+ // override it to reconnect console after restart
+ if (event?.Action === "restart")
+ details.State.Status = "restarting";
+ this.updateState("containers", idx, details);
+ })
+ .catch(console.log);
+ this.pendingUpdateContainer[idx] = new_wait;
+ new_wait.finally(() => { delete this.pendingUpdateContainer[idx] });
+
+ return new_wait;
+ }
+
+ updateImage(id, system) {
+ client.getImages(system, id)
+ .then(reply => {
+ const immage = reply[id];
+ immage.isSystem = system;
+ this.updateState("images", id + system.toString(), immage);
+ })
+ .catch(ex => {
+ console.warn("Failed to do Update Image:", JSON.stringify(ex));
+ });
+ }
+
+ updatePod(id, system) {
+ return client.getPods(system, id)
+ .then(reply => {
+ if (reply && reply.length > 0) {
+ reply = reply[0];
+
+ reply.isSystem = system;
+ this.updateState("pods", reply.Id + system.toString(), reply);
+ }
+ })
+ .catch(ex => {
+ console.warn("Failed to do Update Pod:", JSON.stringify(ex));
+ });
+ }
+
+ // see https://docs.podman.io/en/latest/markdown/podman-events.1.html
+
+ handleImageEvent(event, system) {
+ switch (event.Action) {
+ case 'push':
+ case 'save':
+ case 'tag':
+ this.updateImage(event.Actor.ID, system);
+ break;
+ case 'pull': // Pull event has not event.id
+ case 'untag':
+ case 'remove':
+ case 'prune':
+ case 'build':
+ this.updateImages(system);
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type, event.Action);
+ }
+ }
+
+ handleContainerEvent(event, system) {
+ const id = event.Actor.ID;
+
+ switch (event.Action) {
+ /* The following events do not need to trigger any state updates */
+ case 'attach':
+ case 'exec':
+ case 'export':
+ case 'import':
+ case 'init':
+ case 'kill':
+ case 'mount':
+ case 'prune':
+ case 'restart':
+ case 'sync':
+ case 'unmount':
+ case 'wait':
+ break;
+ /* The following events need only to update the Container list
+ * We do get the container affected in the event object but for
+ * now we 'll do a batch update
+ */
+ case 'start':
+ // HACK: We don't get 'started' event for pods got started by the first container which was added to them
+ // https://github.com/containers/podman/issues/7213
+ (event.Actor.Attributes.podId
+ ? this.updatePod(event.Actor.Attributes.podId, system)
+ : this.updatePods(system)
+ ).then(() => this.updateContainer(id, system, event));
+ break;
+ case 'checkpoint':
+ case 'cleanup':
+ case 'create':
+ case 'died':
+ case 'exec_died': // HACK: pick up health check runs with older podman versions, see https://github.com/containers/podman/issues/19237
+ case 'health_status':
+ case 'pause':
+ case 'restore':
+ case 'stop':
+ case 'unpause':
+ case 'rename': // rename event is available starting podman v4.1; until then the container does not get refreshed after renaming
+ this.updateContainer(id, system, event);
+ break;
+
+ case 'remove':
+ this.setState(prevState => {
+ const containers = { ...prevState.containers };
+ delete containers[id + system.toString()];
+ let pods;
+
+ if (event.Actor.Attributes.podId) {
+ const podIdx = event.Actor.Attributes.podId + system.toString();
+ const newPod = { ...prevState.pods[podIdx] };
+ newPod.Containers = newPod.Containers.filter(container => container.Id !== id);
+ pods = { ...prevState.pods, [podIdx]: newPod };
+ } else {
+ // HACK: with podman < 4.3.0 we don't get a pod event when a container in a pod is removed
+ // https://github.com/containers/podman/issues/15408
+ pods = prevState.pods;
+ this.updatePods(system);
+ }
+
+ return { containers, pods };
+ });
+ break;
+
+ // only needs to update the Image list, this ought to be an image event
+ case 'commit':
+ this.updateImages(system);
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type, event.Action);
+ }
+ }
+
+ handlePodEvent(event, system) {
+ switch (event.Action) {
+ case 'create':
+ case 'kill':
+ case 'pause':
+ case 'start':
+ case 'stop':
+ case 'unpause':
+ this.updatePod(event.Actor.ID, system);
+ break;
+ case 'remove':
+ this.setState(prevState => {
+ const pods = { ...prevState.pods };
+ delete pods[event.Actor.ID + system.toString()];
+ return { pods };
+ });
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type, event.Action);
+ }
+ }
+
+ handleEvent(event, system) {
+ switch (event.Type) {
+ case 'container':
+ this.handleContainerEvent(event, system);
+ break;
+ case 'image':
+ this.handleImageEvent(event, system);
+ break;
+ case 'pod':
+ this.handlePodEvent(event, system);
+ break;
+ default:
+ console.warn('Unhandled event type ', event.Type);
+ }
+ }
+
+ cleanupAfterService(system, key) {
+ ["images", "containers", "pods"].forEach(t => {
+ if (this.state[t])
+ this.setState(prevState => {
+ const copy = {};
+ Object.entries(prevState[t] || {}).forEach(([id, v]) => {
+ if (v.isSystem !== system)
+ copy[id] = v;
+ });
+ return { [t]: copy };
+ });
+ });
+ }
+
+ init(system) {
+ client.getInfo(system)
+ .then(reply => {
+ this.setState({
+ [system ? "systemServiceAvailable" : "userServiceAvailable"]: true,
+ version: reply.version.Version,
+ registries: reply.registries,
+ cgroupVersion: reply.host.cgroupVersion,
+ });
+ this.updateImages(system);
+ this.initContainers(system);
+ this.updatePods(system);
+ client.streamEvents(system,
+ message => this.handleEvent(message, system))
+ .then(() => {
+ this.setState({ [system ? "systemServiceAvailable" : "userServiceAvailable"]: false });
+ this.cleanupAfterService(system);
+ })
+ .catch(e => {
+ console.log(e);
+ this.setState({ [system ? "systemServiceAvailable" : "userServiceAvailable"]: false });
+ this.cleanupAfterService(system);
+ });
+
+ // Listen if podman is still running
+ const ch = cockpit.channel({ superuser: system ? "require" : null, payload: "stream", unix: client.getAddress(system) });
+ ch.addEventListener("close", () => {
+ this.setState({ [system ? "systemServiceAvailable" : "userServiceAvailable"]: false });
+ this.cleanupAfterService(system);
+ });
+
+ ch.send("GET " + client.VERSION + "libpod/events HTTP/1.0\r\nContent-Length: 0\r\n\r\n");
+ })
+ .catch(() => {
+ this.setState({
+ [system ? "systemServiceAvailable" : "userServiceAvailable"]: false,
+ [system ? "systemContainersLoaded" : "userContainersLoaded"]: true,
+ [system ? "systemImagesLoaded" : "userImagesLoaded"]: true,
+ [system ? "systemPodsLoaded" : "userPodsLoaded"]: true
+ });
+ });
+ }
+
+ componentDidMount() {
+ this.init(true);
+ cockpit.script("[ `id -u` -eq 0 ] || echo $XDG_RUNTIME_DIR")
+ .done(xrd => {
+ const isRoot = !xrd || xrd.split("/").pop() == "root";
+ if (!isRoot) {
+ sessionStorage.setItem('XDG_RUNTIME_DIR', xrd.trim());
+ this.init(false);
+ this.checkUserService();
+ } else {
+ this.setState({
+ userImagesLoaded: true,
+ userContainersLoaded: true,
+ userPodsLoaded: true,
+ userServiceExists: false
+ });
+ }
+ })
+ .fail(e => console.log("Could not read $XDG_RUNTIME_DIR: ", e.message));
+ cockpit.spawn("selinuxenabled", { error: "ignore" })
+ .then(() => this.setState({ selinuxAvailable: true }))
+ .catch(() => this.setState({ selinuxAvailable: false }));
+
+ cockpit.spawn(["systemctl", "show", "--value", "-p", "LoadState", "podman-restart"], { environ: ["LC_ALL=C"], error: "ignore" })
+ .then(out => this.setState({ podmanRestartAvailable: out.trim() === "loaded" }));
+
+ superuser.addEventListener("changed", () => this.setState({ privileged: !!superuser.allowed }));
+ this.setState({ privileged: superuser.allowed });
+
+ cockpit.user().then(user => {
+ this.setState({ currentUser: user.name || _("User") });
+ // HACK: https://github.com/systemd/systemd/issues/22244#issuecomment-1210357701
+ cockpit.file(`/var/lib/systemd/linger/${user.name}`).watch((content, tag) => {
+ if (content == null && tag === '-') {
+ this.setState({ userLingeringEnabled: false });
+ } else {
+ this.setState({ userLingeringEnabled: true });
+ }
+ });
+ });
+
+ cockpit.addEventListener("locationchanged", this.onNavigate);
+ this.onNavigate();
+ }
+
+ componentWillUnmount() {
+ cockpit.removeEventListener("locationchanged", this.onNavigate);
+ }
+
+ onNavigate() {
+ // HACK: Use usePageLocation when this is rewritten into a functional component
+ const { options, path } = cockpit.location;
+ this.setState({ location: options }, () => {
+ // only use the root path
+ if (path.length === 0) {
+ if (options.name) {
+ this.onFilterChanged(options.name);
+ }
+ if (options.container) {
+ this.onContainerFilterChanged(options.container);
+ }
+ const owners = ["user", "system", "all"];
+ if (owners.indexOf(options.owner) !== -1) {
+ this.onOwnerChanged(options.owner);
+ }
+ }
+ });
+ }
+
+ checkUserService() {
+ const argv = ["systemctl", "--user", "is-enabled", "podman.socket"];
+
+ cockpit.spawn(["systemctl", "--user", "show", "--value", "-p", "LoadState", "podman-restart"], { environ: ["LC_ALL=C"], error: "ignore" })
+ .then(out => this.setState({ userPodmanRestartAvailable: out.trim() === "loaded" }));
+
+ cockpit.spawn(argv, { environ: ["LC_ALL=C"], err: "out" })
+ .then(() => this.setState({ userServiceExists: true }))
+ .catch((_, response) => {
+ if (response.trim() !== "disabled")
+ this.setState({ userServiceExists: false });
+ else
+ this.setState({ userServiceExists: true });
+ });
+ }
+
+ startService(e) {
+ if (!e || e.button !== 0)
+ return;
+
+ let argv;
+ if (this.state.enableService)
+ argv = ["systemctl", "enable", "--now", "podman.socket"];
+ else
+ argv = ["systemctl", "start", "podman.socket"];
+
+ cockpit.spawn(argv, { superuser: "require", err: "message" })
+ .then(() => this.init(true))
+ .catch(err => {
+ this.setState({
+ systemServiceAvailable: false,
+ systemContainersLoaded: true,
+ systemImagesLoaded: true
+ });
+ console.warn("Failed to start system podman.socket:", JSON.stringify(err));
+ });
+
+ if (this.state.enableService)
+ argv = ["systemctl", "--user", "enable", "--now", "podman.socket"];
+ else
+ argv = ["systemctl", "--user", "start", "podman.socket"];
+
+ cockpit.spawn(argv, { err: "message" })
+ .then(() => this.init(false))
+ .catch(err => {
+ this.setState({
+ userServiceAvailable: false,
+ userContainersLoaded: true,
+ userPodsLoaded: true,
+ userImagesLoaded: true
+ });
+ console.warn("Failed to start user podman.socket:", JSON.stringify(err));
+ });
+ }
+
+ goToServicePage(e) {
+ if (!e || e.button !== 0)
+ return;
+ cockpit.jump("/system/services#/podman.socket");
+ }
+
+ render() {
+ if (this.state.systemServiceAvailable === null && this.state.userServiceAvailable === null) // not detected yet
+ return null;
+
+ if (!this.state.systemServiceAvailable && !this.state.userServiceAvailable) {
+ return (
+ <Page>
+ <PageSection variant={PageSectionVariants.light}>
+ <EmptyState variant={EmptyStateVariant.full}>
+ <EmptyStateHeader titleText={_("Podman service is not active")} icon={<EmptyStateIcon icon={ExclamationCircleIcon} />} headingLevel="h2" />
+ <EmptyStateFooter>
+ <Checkbox isChecked={this.state.enableService}
+ id="enable"
+ label={_("Automatically start podman on boot")}
+ onChange={ (_event, checked) => this.setState({ enableService: checked }) } />
+ <Button onClick={this.startService}>
+ {_("Start podman")}
+ </Button>
+ { cockpit.manifests.system &&
+ <EmptyStateActions>
+ <Button variant="link" onClick={this.goToServicePage}>
+ {_("Troubleshoot")}
+ </Button>
+ </EmptyStateActions>
+ }
+ </EmptyStateFooter>
+ </EmptyState>
+ </PageSection>
+ </Page>
+ );
+ }
+
+ let imageContainerList = {};
+ if (this.state.containers !== null) {
+ Object.keys(this.state.containers).forEach(c => {
+ const container = this.state.containers[c];
+ const image = container.Image + container.isSystem.toString();
+ if (imageContainerList[image]) {
+ imageContainerList[image].push({
+ container,
+ stats: this.state.containersStats[container.Id + container.isSystem.toString()],
+ });
+ } else {
+ imageContainerList[image] = [{
+ container,
+ stats: this.state.containersStats[container.Id + container.isSystem.toString()]
+ }];
+ }
+ });
+ } else
+ imageContainerList = null;
+
+ let startService = "";
+ const action = (
+ <>
+ <AlertActionLink variant='secondary' onClick={this.startService}>{_("Start")}</AlertActionLink>
+ <AlertActionCloseButton onClose={() => this.setState({ showStartService: false })} />
+ </>
+ );
+ if (!this.state.systemServiceAvailable && this.state.privileged) {
+ startService = (
+ <Alert
+ title={_("System Podman service is also available")}
+ actionClose={action} />
+ );
+ }
+ if (!this.state.userServiceAvailable && this.state.userServiceExists) {
+ startService = (
+ <Alert
+ title={_("User Podman service is also available")}
+ actionClose={action} />
+ );
+ }
+
+ const imageList = (
+ <Images
+ key="imageList"
+ images={this.state.systemImagesLoaded && this.state.userImagesLoaded ? this.state.images : null}
+ imageContainerList={imageContainerList}
+ onAddNotification={this.onAddNotification}
+ textFilter={this.state.textFilter}
+ ownerFilter={this.state.ownerFilter}
+ showAll={ () => this.setState({ containersFilter: "all" }) }
+ user={this.state.currentUser}
+ userServiceAvailable={this.state.userServiceAvailable}
+ systemServiceAvailable={this.state.systemServiceAvailable}
+ />
+ );
+ const containerList = (
+ <Containers
+ key="containerList"
+ version={this.state.version}
+ images={this.state.systemImagesLoaded && this.state.userImagesLoaded ? this.state.images : null}
+ containers={this.state.systemContainersLoaded && this.state.userContainersLoaded ? this.state.containers : null}
+ pods={this.state.systemPodsLoaded && this.state.userPodsLoaded ? this.state.pods : null}
+ containersStats={this.state.containersStats}
+ filter={this.state.containersFilter}
+ handleFilterChange={this.onContainerFilterChanged}
+ textFilter={this.state.textFilter}
+ ownerFilter={this.state.ownerFilter}
+ user={this.state.currentUser}
+ onAddNotification={this.onAddNotification}
+ userServiceAvailable={this.state.userServiceAvailable}
+ systemServiceAvailable={this.state.systemServiceAvailable}
+ cgroupVersion={this.state.cgroupVersion}
+ updateContainer={this.updateContainer}
+ />
+ );
+
+ const notificationList = (
+ <AlertGroup isToast>
+ {this.state.notifications.map((notification, index) => {
+ return (
+ <Alert key={index} title={notification.error} variant={notification.type}
+ isLiveRegion
+ actionClose={<AlertActionCloseButton onClose={() => this.onDismissNotification(notification.index)} />}>
+ {notification.errorDetail}
+ </Alert>
+ );
+ })}
+ </AlertGroup>
+ );
+
+ const contextInfo = {
+ cgroupVersion: this.state.cgroupVersion,
+ registries: this.state.registries,
+ selinuxAvailable: this.state.selinuxAvailable,
+ podmanRestartAvailable: this.state.podmanRestartAvailable,
+ userPodmanRestartAvailable: this.state.userPodmanRestartAvailable,
+ userLingeringEnabled: this.state.userLingeringEnabled,
+ version: this.state.version,
+ };
+
+ return (
+ <WithPodmanInfo value={contextInfo}>
+ <WithDialogs>
+ <Page id="overview" key="overview">
+ {notificationList}
+ <PageSection className="content-filter" padding={{ default: 'noPadding' }}
+ variant={PageSectionVariants.light}>
+ <ContainerHeader
+ handleFilterChanged={this.onFilterChanged}
+ handleOwnerChanged={this.onOwnerChanged}
+ ownerFilter={this.state.ownerFilter}
+ textFilter={this.state.textFilter}
+ twoOwners={this.state.systemServiceAvailable && this.state.userServiceAvailable}
+ user={this.state.currentUser}
+ />
+ </PageSection>
+ <PageSection className='ct-pagesection-mobile'>
+ <Stack hasGutter>
+ { this.state.showStartService ? startService : null }
+ {imageList}
+ {containerList}
+ </Stack>
+ </PageSection>
+ </Page>
+ </WithDialogs>
+ </WithPodmanInfo>
+ );
+ }
+}
+
+export default Application;
diff --git a/src/client.js b/src/client.js
new file mode 100644
index 0000000..41df29d
--- /dev/null
+++ b/src/client.js
@@ -0,0 +1,183 @@
+import rest from './rest.js';
+
+const PODMAN_SYSTEM_ADDRESS = "/run/podman/podman.sock";
+export const VERSION = "/v1.12/";
+
+export function getAddress(system) {
+ if (system)
+ return PODMAN_SYSTEM_ADDRESS;
+ const xrd = sessionStorage.getItem('XDG_RUNTIME_DIR');
+ if (xrd)
+ return (xrd + "/podman/podman.sock");
+ console.warn("$XDG_RUNTIME_DIR is not present. Cannot use user service.");
+ return "";
+}
+
+function podmanCall(name, method, args, system, body) {
+ const options = {
+ method,
+ path: VERSION + name,
+ body: body || "",
+ params: args,
+ };
+
+ return rest.call(getAddress(system), system, options);
+}
+
+const podmanJson = (name, method, args, system, body) => podmanCall(name, method, args, system, body)
+ .then(reply => JSON.parse(reply));
+
+function podmanMonitor(name, method, args, callback, system) {
+ const options = {
+ method,
+ path: VERSION + name,
+ body: "",
+ params: args,
+ };
+
+ const connection = rest.connect(getAddress(system), system);
+ return connection.monitor(options, callback, system);
+}
+
+export const streamEvents = (system, callback) => podmanMonitor("libpod/events", "GET", {}, callback, system);
+
+export function getInfo(system) {
+ return new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => reject(new Error("timeout")), 5000);
+ podmanJson("libpod/info", "GET", {}, system)
+ .then(reply => resolve(reply))
+ .catch(reject)
+ .finally(() => clearTimeout(timeout));
+ });
+}
+
+export const getContainers = system => podmanJson("libpod/containers/json", "GET", { all: true }, system);
+
+export const streamContainerStats = (system, callback) => podmanMonitor("libpod/containers/stats", "GET", { stream: true }, callback, system);
+
+export function inspectContainer(system, id) {
+ const options = {
+ size: false // set true to display filesystem usage
+ };
+ return podmanJson("libpod/containers/" + id + "/json", "GET", options, system);
+}
+
+export const delContainer = (system, id, force) => podmanCall("libpod/containers/" + id, "DELETE", { force }, system);
+
+export const renameContainer = (system, id, config) => podmanCall("libpod/containers/" + id + "/rename", "POST", config, system);
+
+export const createContainer = (system, config) => podmanJson("libpod/containers/create", "POST", {}, system, JSON.stringify(config));
+
+export const commitContainer = (system, commitData) => podmanCall("libpod/commit", "POST", commitData, system);
+
+export const postContainer = (system, action, id, args) => podmanCall("libpod/containers/" + id + "/" + action, "POST", args, system);
+
+export const runHealthcheck = (system, id) => podmanCall("libpod/containers/" + id + "/healthcheck", "GET", {}, system);
+
+export const postPod = (system, action, id, args) => podmanCall("libpod/pods/" + id + "/" + action, "POST", args, system);
+
+export const delPod = (system, id, force) => podmanCall("libpod/pods/" + id, "DELETE", { force }, system);
+
+export const createPod = (system, config) => podmanCall("libpod/pods/create", "POST", {}, system, JSON.stringify(config));
+
+export function execContainer(system, id) {
+ const args = {
+ AttachStderr: true,
+ AttachStdout: true,
+ AttachStdin: true,
+ Tty: true,
+ Cmd: ["/bin/sh"],
+ };
+
+ return podmanJson("libpod/containers/" + id + "/exec", "POST", {}, system, JSON.stringify(args));
+}
+
+export function resizeContainersTTY(system, id, exec, width, height) {
+ const args = {
+ h: height,
+ w: width,
+ };
+
+ let point = "containers/";
+ if (!exec)
+ point = "exec/";
+
+ return podmanCall("libpod/" + point + id + "/resize", "POST", args, system);
+}
+
+function parseImageInfo(info) {
+ const image = {};
+
+ if (info.Config) {
+ image.Entrypoint = info.Config.Entrypoint;
+ image.Command = info.Config.Cmd;
+ image.Ports = Object.keys(info.Config.ExposedPorts || {});
+ image.Env = info.Config.Env;
+ }
+ image.Author = info.Author;
+
+ return image;
+}
+
+export function getImages(system, id) {
+ const options = {};
+ if (id)
+ options.filters = JSON.stringify({ id: [id] });
+ return podmanJson("libpod/images/json", "GET", options, system)
+ .then(reply => {
+ const images = {};
+ const promises = [];
+
+ for (const image of reply) {
+ images[image.Id] = image;
+ promises.push(podmanJson("libpod/images/" + image.Id + "/json", "GET", {}, system));
+ }
+
+ return Promise.all(promises)
+ .then(replies => {
+ for (const info of replies) {
+ images[info.Id] = Object.assign(images[info.Id], parseImageInfo(info));
+ images[info.Id].isSystem = system;
+ }
+ return images;
+ });
+ });
+}
+
+export function getPods(system, id) {
+ const options = {};
+ if (id)
+ options.filters = JSON.stringify({ id: [id] });
+ return podmanJson("libpod/pods/json", "GET", options, system);
+}
+
+export const delImage = (system, id, force) => podmanJson("libpod/images/" + id, "DELETE", { force }, system);
+
+export const untagImage = (system, id, repo, tag) => podmanCall("libpod/images/" + id + "/untag", "POST", { repo, tag }, system);
+
+export function pullImage(system, reference) {
+ return new Promise((resolve, reject) => {
+ podmanCall("libpod/images/pull", "POST", { reference }, system)
+ .then(r => {
+ // Need to check the last response if it contains error
+ const responses = r.trim().split("\n");
+ const response = JSON.parse(responses[responses.length - 1]);
+ if (response.error) {
+ response.message = response.error;
+ reject(response);
+ } else if (response.cause) // present for 400 and 500 errors
+ reject(response);
+ else
+ resolve();
+ })
+ .catch(reject);
+ });
+}
+
+export const pruneUnusedImages = system => podmanJson("libpod/images/prune?all=true", "POST", {}, system);
+
+export const imageHistory = (system, id) => podmanJson(`libpod/images/${id}/history`, "GET", {}, system);
+
+export const imageExists = (system, id) => podmanCall("libpod/images/" + id + "/exists", "GET", {}, system);
+
+export const containerExists = (system, id) => podmanCall("libpod/containers/" + id + "/exists", "GET", {}, system);
diff --git a/src/index.html b/src/index.html
new file mode 100644
index 0000000..c4e1856
--- /dev/null
+++ b/src/index.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2017 Red Hat, Inc.
+
+Cockpit is free software; you can redistribute it and/or modify it
+under the terms of the GNU Lesser General Public License as published by
+the Free Software Foundation; either version 2.1 of the License, or
+(at your option) any later version.
+
+Cockpit is distributed in the hope that it will be useful, but
+WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+Lesser General Public License for more details.
+
+You should have received a copy of the GNU Lesser General Public License
+along with this package; If not, see <http://www.gnu.org/licenses/>.
+-->
+<html id="podman-page" class="pf-theme-dark" lang="en">
+<head>
+ <title translatable="yes">Podman containers</title>
+ <meta charset="utf-8">
+ <meta name="description" content="">
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="stylesheet" href="index.css">
+
+ <script type="text/javascript" src="index.js"></script>
+ <script type="text/javascript" src="po.js"></script>
+ <script type="text/javascript" src="../manifests.js"></script>
+</head>
+
+<body class="pf-m-redhat-font pf-v5-m-tabular-nums">
+ <div class="ct-page-fill" id="app">
+ </div>
+</body>
+</html>
diff --git a/src/index.js b/src/index.js
new file mode 100644
index 0000000..90c7eb3
--- /dev/null
+++ b/src/index.js
@@ -0,0 +1,30 @@
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+import "cockpit-dark-theme";
+import React from 'react';
+import { createRoot } from 'react-dom/client';
+import 'patternfly/patternfly-5-cockpit.scss';
+import Application from './app.jsx';
+import './podman.scss';
+
+document.addEventListener("DOMContentLoaded", function () {
+ const root = createRoot(document.getElementById('app'));
+ root.render(<Application />);
+});
diff --git a/src/manifest.json b/src/manifest.json
new file mode 100644
index 0000000..530aa6d
--- /dev/null
+++ b/src/manifest.json
@@ -0,0 +1,16 @@
+{
+ "conditions": [
+ {"path-exists": "/lib/systemd/system/podman.socket"}
+ ],
+ "menu": {
+ "index": {
+ "label": "Podman containers",
+ "order": 50,
+ "keywords": [
+ {
+ "matches": ["podman", "container", "image"]
+ }
+ ]
+ }
+ }
+}
diff --git a/src/podman.scss b/src/podman.scss
new file mode 100644
index 0000000..6db864c
--- /dev/null
+++ b/src/podman.scss
@@ -0,0 +1,149 @@
+@use "ct-card.scss";
+@use "page.scss";
+@import "global-variables";
+// For pf-v5-line-clamp
+@import "@patternfly/patternfly/sass-utilities/mixins.scss";
+// For pf-u-disabled-color-100
+@import "@patternfly/patternfly/utilities/Text/text.css";
+
+#app .pf-v5-c-card.containers-containers, #app .pf-v5-c-card.containers-images {
+ @extend .ct-card;
+}
+
+.pf-v5-c-modal-box__title-text {
+ white-space: break-spaces;
+}
+
+#containers-images, #containers-containers {
+ // Decrease padding for the image/container toggle button list
+ .pf-v5-c-table.pf-m-compact .pf-v5-c-table__toggle {
+ padding-inline-start: 0;
+ }
+}
+
+@media screen and (max-width: 768px) {
+ // Badges should not stretch in mobile mode
+ .pf-v5-c-table [data-label] > .pf-v5-c-badge {
+ justify-self: start;
+ }
+}
+
+.container-block {
+ display: flex;
+ flex-direction: column;
+ word-break: break-all;
+}
+
+.container-block small {
+ @include pf-v5-line-clamp("1");
+ color: var(--pf-v5-global--Color--200);
+}
+
+.container-name {
+ font-size: var(--pf-v5-global--FontSize--lg);
+ font-weight: 400;
+}
+
+.containers-run-onbuildvarclaim input {
+ max-inline-size: 15em;
+}
+
+.pf-v5-c-alert__description {
+ overflow-wrap: anywhere;
+}
+
+.listing-action {
+ inline-size: 100%;
+ display: flex;
+ justify-content: space-around;
+}
+
+.ct-badge-container-running, .ct-badge-pod-running {
+ background-color: var(--pf-v5-global--info-color--100);
+ color: white;
+}
+
+.ct-badge-container-healthy {
+ background-color: var(--pf-v5-global--success-color--100);
+ color: white;
+}
+
+.ct-badge-container-unhealthy {
+ background-color: var(--pf-v5-global--danger-color--100);
+ color: white;
+}
+
+.ct-badge-toolbox {
+ background-color: var(--pf-v5-global--palette--purple-100);
+ color: var(--pf-v5-global--palette--purple-600);
+
+ .pf-v5-theme-dark & {
+ background-color: var(--pf-v5-global--palette--purple-500);
+ color: white;
+ }
+}
+
+.ct-badge-distrobox {
+ background-color: var(--pf-v5-global--palette--gold-100);
+ color: var(--pf-v5-global--palette--gold-600);
+
+ .pf-v5-theme-dark & {
+ background-color: var(--pf-v5-global--palette--gold-500);
+ color: white;
+ }
+}
+
+.green {
+ color: var(--pf-v5-global--success-color--100);
+}
+
+.red {
+ color: var(--pf-v5-global--danger-color--100);
+}
+
+// Hide the header nav from the expandable rows - this should be better done with JS but the current cockpit-listing-panel implementation does not support this variant
+#containers-images .ct-listing-panel-head {
+ display: none;
+}
+
+.ct-grey-text {
+ color: var(--pf-v5-global--Color--200);
+}
+
+.content-action {
+ text-align: end;
+ white-space: nowrap !important;
+}
+
+// Remove doubled-up padding and borders on nested tables in mobile
+.ct-listing-panel-body .ct-table tr {
+ --pf-v5-c-table-tr--responsive--PaddingTop: 0;
+ --pf-v5-c-table-tr--responsive--PaddingRight: 0;
+ --pf-v5-c-table-tr--responsive--PaddingBottom: 0;
+ --pf-v5-c-table-tr--responsive--PaddingLeft: 0;
+}
+
+@media (max-width: $pf-v5-global--breakpoint--md - 1) {
+ .show-only-when-wide {
+ display: none;
+ }
+}
+
+@media (min-width: $pf-v5-global--breakpoint--md) {
+ .show-only-when-narrow {
+ display: none;
+ }
+
+ // Add borders to no pod containers list and images list
+ .container-pod.pf-m-plain tbody,
+ .containers-images tbody {
+ border: var(--pf-v5-c-card--m-flat--BorderWidth) solid var(--pf-v5-c-card--m-flat--BorderColor);
+ }
+}
+
+// Override table padding on mobile
+@media (max-width: $pf-v5-global--breakpoint--md) {
+ .health-logs.pf-m-grid-md.pf-v5-c-table tr:where(.pf-v5-c-table__tr):not(.pf-v5-c-table__expandable-row) {
+ padding: 0;
+ }
+}
diff --git a/src/rest.js b/src/rest.js
new file mode 100644
index 0000000..30a5dfa
--- /dev/null
+++ b/src/rest.js
@@ -0,0 +1,89 @@
+import cockpit from "cockpit";
+import { debug } from "./util.js";
+
+function manage_error(reject, error, content) {
+ let content_o = {};
+ if (content) {
+ try {
+ content_o = JSON.parse(content);
+ } catch {
+ content_o.message = content;
+ }
+ }
+ const c = { ...error, ...content_o };
+ reject(c);
+}
+
+// calls are async, so keep track of a call counter to associate a result with a call
+let call_id = 0;
+
+function connect(address, system) {
+ /* This doesn't create a channel until a request */
+ const http = cockpit.http(address, { superuser: system ? "require" : null });
+ const connection = {};
+
+ connection.monitor = function(options, callback, system, return_raw) {
+ return new Promise((resolve, reject) => {
+ let buffer = "";
+
+ http.request(options)
+ .stream(data => {
+ if (return_raw)
+ callback(data);
+ else {
+ buffer += data;
+ const chunks = buffer.split("\n");
+ buffer = chunks.pop();
+
+ chunks.forEach(chunk => {
+ debug(system, "monitor", chunk);
+ callback(JSON.parse(chunk));
+ });
+ }
+ })
+ .catch((error, content) => {
+ manage_error(reject, error, content);
+ })
+ .then(resolve);
+ });
+ };
+
+ connection.call = function (options) {
+ const id = call_id++;
+ debug(system, `call ${id}:`, JSON.stringify(options));
+ return new Promise((resolve, reject) => {
+ options = options || {};
+ http.request(options)
+ .then(result => {
+ debug(system, `call ${id} result:`, JSON.stringify(result));
+ resolve(result);
+ })
+ .catch((error, content) => {
+ debug(system, `call ${id} error:`, JSON.stringify(error), "content", JSON.stringify(content));
+ manage_error(reject, error, content);
+ });
+ });
+ };
+
+ connection.close = function () {
+ http.close();
+ };
+
+ return connection;
+}
+
+/*
+ * Connects to the podman service, performs a single call, and closes the
+ * connection.
+ */
+async function call (address, system, parameters) {
+ const connection = connect(address, system);
+ const result = await connection.call(parameters);
+ connection.close();
+ return result;
+}
+
+export default {
+ connect,
+ call
+};
diff --git a/src/util.js b/src/util.js
new file mode 100644
index 0000000..688194a
--- /dev/null
+++ b/src/util.js
@@ -0,0 +1,185 @@
+import React, { useContext } from "react";
+
+import cockpit from 'cockpit';
+
+import { debounce } from 'throttle-debounce';
+import * as dfnlocales from 'date-fns/locale';
+import { formatRelative } from 'date-fns';
+const _ = cockpit.gettext;
+
+export const PodmanInfoContext = React.createContext();
+export const usePodmanInfo = () => useContext(PodmanInfoContext);
+
+export const WithPodmanInfo = ({ value, children }) => {
+ return (
+ <PodmanInfoContext.Provider value={value}>
+ {children}
+ </PodmanInfoContext.Provider>
+ );
+};
+
+// https://github.com/containers/podman/blob/main/libpod/define/containerstate.go
+// "Restarting" comes from special handling of restart case in Application.updateContainer()
+export const states = [_("Exited"), _("Paused"), _("Stopped"), _("Removing"), _("Configured"), _("Created"), _("Restart"), _("Running")];
+
+// https://github.com/containers/podman/blob/main/libpod/define/podstate.go
+export const podStates = [_("Created"), _("Running"), _("Stopped"), _("Paused"), _("Exited"), _("Error")];
+
+export const fallbackRegistries = ["docker.io", "quay.io"];
+
+export function debug(system, ...args) {
+ if (window.debugging === "all" || window.debugging?.includes("podman"))
+ console.debug("podman", system ? "system" : "user", ...args);
+}
+
+export function truncate_id(id) {
+ if (!id) {
+ return "";
+ }
+ return id.substr(0, 12);
+}
+
+export function localize_time(unix_timestamp) {
+ const locale = (cockpit.language == "en") ? dfnlocales.enUS : dfnlocales[cockpit.language.replace('_', '')];
+ return formatRelative(unix_timestamp * 1000, Date.now(), { locale });
+}
+
+export function format_memory_and_limit(usage, limit) {
+ if (usage === undefined || isNaN(usage))
+ return "";
+
+ let mtext = "";
+ let units = 1000;
+ let parts;
+ if (limit) {
+ parts = cockpit.format_bytes(limit, units, true);
+ mtext = " / " + parts.join(" ");
+ units = parts[1];
+ }
+
+ if (usage) {
+ parts = cockpit.format_bytes(usage, units, true);
+ if (mtext)
+ return _(parts[0] + mtext);
+ else
+ return _(parts.join(" "));
+ } else {
+ return "";
+ }
+}
+
+/*
+ * The functions quote_cmdline and unquote_cmdline implement
+ * a simple shell-like quoting syntax. They are used when letting the
+ * user edit a sequence of words as a single string.
+ *
+ * When parsing, words are separated by whitespace. Single and double
+ * quotes can be used to protect a sequence of characters that
+ * contains whitespace or the other quote character. A backslash can
+ * be used to protect any character. Quotes can appear in the middle
+ * of a word.
+ */
+
+export function quote_cmdline(words) {
+ words = words || [];
+
+ function is_whitespace(c) {
+ return c == ' ';
+ }
+
+ function quote(word) {
+ let text = "";
+ let quote_char = "";
+ let i;
+ for (i = 0; i < word.length; i++) {
+ if (word[i] == '\\' || word[i] == quote_char)
+ text += '\\';
+ else if (quote_char === "") {
+ if (word[i] == "'" || is_whitespace(word[i]))
+ quote_char = '"';
+ else if (word[i] == '"')
+ quote_char = "'";
+ }
+ text += word[i];
+ }
+
+ return quote_char + text + quote_char;
+ }
+
+ return words.map(quote).join(' ');
+}
+
+export function unquote_cmdline(text) {
+ const words = [];
+ let next;
+
+ function is_whitespace(c) {
+ return c == ' ';
+ }
+
+ function skip_whitespace() {
+ while (next < text.length && is_whitespace(text[next]))
+ next++;
+ }
+
+ function parse_word() {
+ let word = "";
+ let quote_char = null;
+
+ while (next < text.length) {
+ if (text[next] == '\\') {
+ next++;
+ if (next < text.length) {
+ word += text[next];
+ }
+ } else if (text[next] == quote_char) {
+ quote_char = null;
+ } else if (quote_char) {
+ word += text[next];
+ } else if (text[next] == '"' || text[next] == "'") {
+ quote_char = text[next];
+ } else if (is_whitespace(text[next])) {
+ break;
+ } else
+ word += text[next];
+ next++;
+ }
+ return word;
+ }
+
+ next = 0;
+ skip_whitespace();
+ while (next < text.length) {
+ words.push(parse_word());
+ skip_whitespace();
+ }
+
+ return words;
+}
+
+export function image_name(image) {
+ return image.RepoTags ? image.RepoTags[0] : "<none>:<none>";
+}
+
+export function is_valid_container_name(name) {
+ return /^[a-zA-Z0-9][a-zA-Z0-9_\\.-]*$/.test(name);
+}
+
+/* Clears a single field in validationFailed object.
+ *
+ * Arguments:
+ * - validationFailed (object): Object containing list of fields with validation error
+ * - key (string): Specified which field from validationFailed object is clear
+ * - onValidationChange (func)
+ */
+export const validationClear = (validationFailed, key, onValidationChange) => {
+ if (!validationFailed)
+ return;
+
+ const delta = { ...validationFailed };
+ delete delta[key];
+ onValidationChange(delta);
+};
+
+// This method needs to be outside of component as re-render would create a new instance of debounce
+export const validationDebounce = debounce(500, (validationHandler) => validationHandler());
diff --git a/test/browser/browser.sh b/test/browser/browser.sh
new file mode 100755
index 0000000..0d5c581
--- /dev/null
+++ b/test/browser/browser.sh
@@ -0,0 +1,88 @@
+#!/bin/sh
+set -eux
+
+# test plan name, passed on to run-test.sh
+PLAN="$1"
+
+export TEST_BROWSER=${TEST_BROWSER:-firefox}
+
+TESTS="$(realpath $(dirname "$0"))"
+export SOURCE="$(realpath $TESTS/../..)"
+
+# https://tmt.readthedocs.io/en/stable/overview.html#variables
+export LOGS="${TMT_TEST_DATA:-$(pwd)/logs}"
+mkdir -p "$LOGS"
+chmod a+w "$LOGS"
+
+# install firefox (available everywhere in Fedora and RHEL)
+# we don't need the H.264 codec, and it is sometimes not available (rhbz#2005760)
+dnf install --disablerepo=fedora-cisco-openh264 -y --setopt=install_weak_deps=False firefox
+
+# nodejs 10 is too old for current Cockpit test API
+if grep -q platform:el8 /etc/os-release; then
+ dnf module switch-to -y nodejs:16
+fi
+
+# HACK: ensure that critical components are up to date: https://github.com/psss/tmt/issues/682
+dnf update -y podman crun conmon criu
+
+# if we run during cross-project testing against our main-builds COPR, then let that win
+# even if Fedora has a newer revision
+main_builds_repo="$(ls /etc/yum.repos.d/*cockpit*main-builds* 2>/dev/null || true)"
+if [ -n "$main_builds_repo" ]; then
+ echo 'priority=0' >> "$main_builds_repo"
+ dnf distro-sync -y --repo 'copr*' cockpit-podman
+fi
+
+# Show critical package versions
+rpm -q runc crun podman criu kernel-core selinux-policy cockpit-podman cockpit-bridge || true
+
+# create user account for logging in
+if ! id admin 2>/dev/null; then
+ useradd -c Administrator -G wheel admin
+ echo admin:foobar | chpasswd
+fi
+
+# set root's password
+echo root:foobar | chpasswd
+
+# avoid sudo lecture during tests
+su -c 'echo foobar | sudo --stdin whoami' - admin
+
+# create user account for running the test
+if ! id runtest 2>/dev/null; then
+ useradd -c 'Test runner' runtest
+ # allow test to set up things on the machine
+ mkdir -p /root/.ssh
+ curl https://raw.githubusercontent.com/cockpit-project/bots/main/machine/identity.pub >> /root/.ssh/authorized_keys
+ chmod 600 /root/.ssh/authorized_keys
+fi
+chown -R runtest "$SOURCE"
+
+# disable core dumps, we rather investigate them upstream where test VMs are accessible
+echo core > /proc/sys/kernel/core_pattern
+
+# grab a few images to play with; tests run offline, so they cannot download images
+podman rmi --all
+
+# set up our expected images, in the same way that we do for upstream CI
+# this sometimes runs into network issues, so retry a few times
+for retry in $(seq 5); do
+ if curl https://raw.githubusercontent.com/cockpit-project/bots/main/images/scripts/lib/podman-images.setup | sh -eux; then
+ break
+ fi
+ sleep $((5 * retry * retry))
+done
+
+# image setup, shared with upstream tests
+$TESTS/../vm.install
+
+systemctl enable --now cockpit.socket podman.socket
+
+# Run tests as unprivileged user
+# once we drop support for RHEL 8, use this:
+# runuser -u runtest --whitelist-environment=TEST_BROWSER,TEST_ALLOW_JOURNAL_MESSAGES,TEST_AUDIT_NO_SELINUX,SOURCE,LOGS $TESTS/run-test.sh $PLAN
+runuser -u runtest --preserve-environment env USER=runtest HOME=$(getent passwd runtest | cut -f6 -d:) $TESTS/run-test.sh $PLAN
+
+RC=$(cat $LOGS/exitcode)
+exit ${RC:-1}
diff --git a/test/browser/main.fmf b/test/browser/main.fmf
new file mode 100644
index 0000000..dfad2c3
--- /dev/null
+++ b/test/browser/main.fmf
@@ -0,0 +1,24 @@
+require:
+ - cockpit-podman
+ - cockpit-ws
+ - cockpit-system
+ - bzip2
+ - criu
+ - git-core
+ - libvirt-python3
+ - make
+ - nodejs
+ - python3
+duration: 30m
+
+/system:
+ test: ./browser.sh system
+ summary: Run *System tests
+
+/user:
+ test: ./browser.sh user
+ summary: Run *User tests
+
+/other:
+ test: ./browser.sh other
+ summary: Run all other tests
diff --git a/test/browser/run-test.sh b/test/browser/run-test.sh
new file mode 100755
index 0000000..9b64cde
--- /dev/null
+++ b/test/browser/run-test.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+set -eux
+
+PLAN="$1"
+
+# tests need cockpit's bots/ libraries and test infrastructure
+cd $SOURCE
+rm -f bots # common local case: existing bots symlink
+make bots test/common
+
+if [ -e .git ]; then
+ tools/node-modules checkout
+ # disable detection of affected tests; testing takes too long as there is no parallelization
+ mv .git dot-git
+else
+ # upstream tarballs ship test dependencies; print version for debugging
+ grep '"version"' node_modules/chrome-remote-interface/package.json
+fi
+
+. /etc/os-release
+export TEST_OS="${ID}-${VERSION_ID/./-}"
+
+if [ "${TEST_OS#centos-}" != "$TEST_OS" ]; then
+ TEST_OS="${TEST_OS}-stream"
+fi
+
+# select subset of tests according to plan
+TESTS="$(test/common/run-tests -l)"
+case "$PLAN" in
+ system) TESTS="$(echo "$TESTS" | grep 'System$')" ;;
+ user) TESTS="$(echo "$TESTS" | grep 'User$')" ;;
+ other) TESTS="$(echo "$TESTS" | grep -vE '(System|User)$')" ;;
+ *) echo "Unknown test plan: $PLAN" >&2; exit 1 ;;
+esac
+
+EXCLUDES=""
+
+# make it easy to check in logs
+echo "TEST_ALLOW_JOURNAL_MESSAGES: ${TEST_ALLOW_JOURNAL_MESSAGES:-}"
+echo "TEST_AUDIT_NO_SELINUX: ${TEST_AUDIT_NO_SELINUX:-}"
+
+RC=0
+test/common/run-tests --nondestructive --machine 127.0.0.1:22 --browser 127.0.0.1:9090 $TESTS $EXCLUDES || RC=$?
+
+echo $RC > "$LOGS/exitcode"
+cp --verbose Test* "$LOGS" || true
+# deliver test result via exitcode file
+exit 0
diff --git a/test/check-application b/test/check-application
new file mode 100755
index 0000000..41109d4
--- /dev/null
+++ b/test/check-application
@@ -0,0 +1,2871 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/common/pywrap", sys.argv)
+# Run this with --help to see available options for tracing and debugging
+# See https://github.com/cockpit-project/cockpit/blob/main/test/common/testlib.py
+# "class Browser" and "class MachineCase" for the available API.
+
+import os
+import sys
+import time
+
+import testlib
+from machine_core import ssh_connection
+
+REGISTRIES_CONF = """
+[registries.search]
+registries = ['localhost:5000', 'localhost:6000']
+
+[registries.insecure]
+registries = ['localhost:5000', 'localhost:6000']
+"""
+
+NOT_RUNNING = ["Exited", "Stopped"]
+
+# image names used in tests
+IMG_ALPINE = "localhost/test-alpine"
+IMG_ALPINE_LATEST = IMG_ALPINE + ":latest"
+IMG_BUSYBOX = "localhost/test-busybox"
+IMG_BUSYBOX_LATEST = IMG_BUSYBOX + ":latest"
+IMG_REGISTRY = "localhost/test-registry"
+IMG_REGISTRY_LATEST = IMG_REGISTRY + ":latest"
+
+
+def podman_version(cls):
+ version = cls.execute(False, "podman -v").strip().split(' ')[-1]
+ # HACK: handle possible rc versions such as 4.4.0-rc2
+ return tuple(int(v.split('-')[0]) for v in version.split('.'))
+
+
+def showImages(browser):
+ if browser.attr("#containers-images button.pf-v5-c-expandable-section__toggle", "aria-expanded") == 'false':
+ browser.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+
+def checkImage(browser, name, owner):
+ showImages(browser)
+ browser.wait_visible("#containers-images table")
+ browser.wait_js_func("""(function (first, last) {
+ let items = ph_select("#containers-images table tbody");
+ for (i = 0; i < items.length; i++)
+ if (items[i].innerText.trim().startsWith(first) && items[i].innerText.trim().includes(last))
+ return true;
+ return false;
+ })""", name, owner)
+
+
+# HACK: temporary workaround till we bump testlib
+def become_superuser(browser, user=None, password=None, passwordless=False):
+ cur_frame = browser.cdp.cur_frame
+ browser.switch_to_top()
+
+ browser.open_superuser_dialog()
+
+ pf_prefix = "" if browser.machine.system_before(293) else "-v5"
+
+ if passwordless:
+ browser.wait_in_text(f".pf{pf_prefix}-c-modal-box:contains('Administrative access')",
+ "You now have administrative access.")
+ browser.click(f".pf{pf_prefix}-c-modal-box button:contains('Close')")
+ browser.wait_not_present(f".pf{pf_prefix}-c-modal-box:contains('You now have administrative access.')")
+ else:
+ browser.wait_in_text(f".pf{pf_prefix}-c-modal-box:contains('Switch to administrative access')",
+ f"Password for {user or 'admin'}:")
+ browser.set_input_text(f".pf{pf_prefix}-c-modal-box:contains('Switch to administrative access') input",
+ password or "foobar")
+ browser.click(f".pf{pf_prefix}-c-modal-box button:contains('Authenticate')")
+ browser.wait_not_present(f".pf{pf_prefix}-c-modal-box:contains('Switch to administrative access')")
+
+ browser.check_superuser_indicator("Administrative access")
+ browser.switch_to_frame(cur_frame)
+
+
+# HACK: temporary workaround till we bump testlib
+def drop_superuser(browser):
+ cur_frame = browser.cdp.cur_frame
+ browser.switch_to_top()
+
+ pf_prefix = "" if browser.machine.system_before(293) else "-v5"
+
+ browser.open_superuser_dialog()
+ browser.click(f".pf{pf_prefix}-c-modal-box:contains('Switch to limited access') button:contains('Limit access')")
+ browser.wait_not_present(f".pf{pf_prefix}-c-modal-box:contains('Switch to limited access')")
+ browser.check_superuser_indicator("Limited access")
+
+ browser.switch_to_frame(cur_frame)
+
+
+@testlib.nondestructive
+class TestApplication(testlib.MachineCase):
+
+ def setUp(self):
+ super().setUp()
+ m = self.machine
+ m.execute("""
+ systemctl stop podman.service; systemctl --now enable podman.socket
+ # Ensure podman is really stopped, otherwise it keeps the containers/ directory busy
+ pkill -e -9 podman || true
+ while pgrep podman; do sleep 0.1; done
+ pkill -e -9 conmon || true
+ while pgrep conmon; do sleep 0.1; done
+ findmnt --list -otarget | grep /var/lib/containers/. | xargs -r umount
+ sync
+ """)
+
+ # backup/restore pristine podman state, so that tests can run on existing testbeds
+ self.restore_dir("/var/lib/containers")
+
+ self.addCleanup(m.execute, """
+ systemctl stop podman.service podman.socket
+
+ # HACK: system reset has 10s timeout, make that faster with an extra `stop`
+ # https://github.com/containers/podman/issues/21874
+ podman stop --time 0 --all
+ podman pod stop --time 0 --all
+
+ systemctl reset-failed podman.service podman.socket
+ podman system reset --force
+ pkill -e -9 podman || true
+ while pgrep podman; do sleep 0.1; done
+ pkill -e -9 conmon || true
+ while pgrep conmon; do sleep 0.1; done
+
+ # HACK: sometimes podman leaks mounts
+ findmnt --list -otarget | grep /var/lib/containers/. | xargs -r umount
+ sync
+ """)
+
+ # Create admin session
+ m.execute("""
+ if [ ! -d /home/admin/.ssh ]; then
+ mkdir /home/admin/.ssh
+ cp /root/.ssh/* /home/admin/.ssh
+ chown -R admin:admin /home/admin/.ssh
+ chmod -R go-wx /home/admin/.ssh
+ fi
+ """)
+ self.admin_s = ssh_connection.SSHConnection(user="admin",
+ address=m.ssh_address,
+ ssh_port=m.ssh_port,
+ identity_file=m.identity_file)
+
+ # Enable user service as well; copy our images (except cockpit/ws) from system
+ self.admin_s.execute("""
+ systemctl --user stop podman.service
+ for img in $(ls /var/lib/test-images/*.tar | grep -v cockpitws); do podman load < "$img"; done
+ systemctl --now --user enable podman.socket
+ """)
+ self.addCleanup(self.admin_s.execute, """
+ systemctl --user stop podman.service podman.socket
+ podman system reset --force
+ """)
+ # HACK: system reset has 10s timeout, make that faster with an extra `stop`
+ # https://github.com/containers/podman/issues/21874
+ # Ubuntu 22.04 has old podman that does not know about rm --time
+ if m.image == 'ubuntu-2204':
+ self.addCleanup(self.admin_s.execute, "podman rm --force --all", timeout=300)
+ self.addCleanup(self.admin_s.execute, "podman pod rm --force --all", timeout=300)
+ else:
+ self.addCleanup(self.admin_s.execute, "podman rm --force --time 0 --all")
+ self.addCleanup(self.admin_s.execute, "podman pod rm --force --time 0 --all")
+
+ # But disable it globally so that "systemctl --user disable" does what we expect
+ m.execute("systemctl --global disable podman.socket")
+
+ self.allow_journal_messages("/run.*/podman/podman: couldn't connect.*")
+ self.allow_journal_messages(".*/run.*/podman/podman.*Connection reset by peer")
+
+ # https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=1008249
+ self.has_criu = "debian" not in m.image and "ubuntu" not in m.image
+ self.has_selinux = "arch" not in m.image and "debian" not in m.image and "ubuntu" not in m.image
+ self.has_cgroupsV2 = m.image not in ["centos-8-stream"] and not m.image.startswith('rhel-8')
+
+ self.system_images_count = int(self.execute(True, "podman images -n | wc -l").strip())
+ self.user_images_count = int(self.execute(False, "podman images -n | wc -l").strip())
+
+ # allow console.error
+ self.allow_browser_errors(
+ ".*couldn't search registry \".*\": pinging container registry .*",
+ ".*Error occurred while connecting console: cannot resize session: cannot resize container.*",
+ )
+
+ def tearDown(self):
+ if self.getError():
+ # dump container logs for debugging
+ for auth in [False, True]:
+ print(f"----- {auth and 'system' or 'user'} containers -----", file=sys.stderr)
+ self.execute(auth, "podman ps -a >&2")
+ self.execute(auth, 'for c in $(podman ps -aq); do echo "---- $c ----" >&2; podman logs $c >&2; done')
+
+ super().tearDown()
+
+ def getRestartPolicy(self, auth, container_name):
+ cmd = f"podman inspect --format '{{{{.HostConfig.RestartPolicy}}}}' {container_name}"
+ return self.execute(auth, cmd).strip()
+
+ def waitNumImages(self, expected):
+ self.browser.wait_js_func("ph_count_check", "#containers-images table[aria-label='Images'] > tbody", expected)
+
+ def waitNumContainers(self, expected, auth):
+ if auth and self.machine.ostree_image:
+ extra = 1 # cockpit/ws
+ else:
+ extra = 0
+
+ self.browser.wait_js_func("ph_count_check", "#containers-containers tbody", expected + extra)
+
+ def performContainerAction(self, container, cmd):
+ b = self.browser
+ b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-menu-toggle")
+ b.click(f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})")
+
+ def getContainerAction(self, container, cmd):
+ return f"#containers-containers tbody tr:contains('{container}') button.pf-v5-c-menu__item:contains({cmd})"
+
+ def toggleExpandedContainer(self, container):
+ b = self.browser
+ b.click(f"#containers-containers tbody tr:contains('{container}') .pf-v5-c-table__toggle button")
+
+ def getContainerAttr(self, container, key, selector=""):
+ b = self.browser
+ return b.text(f"#containers-containers tbody tr:contains('{container}') > td[data-label={key}] {selector}")
+
+ def execute(self, system, cmd):
+ if system:
+ return self.machine.execute(cmd)
+ else:
+ return self.admin_s.execute(cmd)
+
+ def login(self, system=True):
+ # HACK: The first rootless call often gets stuck or fails
+ # In such case we have alert banner to start the service (or just empty state)
+ # A real user would just hit the button so lets do the same as this is always getting
+ # back to us and we waste too much time reporting to podman with mixed results.
+ # Examples:
+ # https://github.com/containers/podman/issues/8762
+ # https://github.com/containers/podman/issues/9251
+ # https://github.com/containers/podman/issues/6660
+
+ b = self.browser
+
+ self.login_and_go("/podman", superuser=system)
+ b.wait_visible("#app")
+
+ with self.browser.wait_timeout(30):
+ try:
+ b.wait_visible("#containers-containers")
+ b.wait_not_in_text("#containers-containers", "Loading")
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+ except testlib.Error:
+ if system:
+ b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+ else:
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_not_present("#app .pf-v5-c-empty-state button")
+
+ def waitPodRow(self, podName, present=False):
+ if present:
+ self.browser.wait_visible("#table-" + podName)
+ else:
+ self.browser.wait_not_present("#table-" + podName)
+
+ def waitPodContainer(self, podName, containerList, system=True):
+ if len(containerList):
+ for container in containerList:
+ self.waitContainer(container["id"], system, name=container["name"], image=container["image"],
+ cmd=container["command"], state=container["state"], pod=podName)
+ else:
+ if self.browser.val("#containers-containers-filter") == "all":
+ self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state", "No containers in this pod")
+ else:
+ self.browser.wait_in_text("#table-" + podName + " .pf-v5-c-empty-state",
+ "No running containers in this pod")
+
+ def waitContainerRow(self, container, present=True):
+ b = self.browser
+ if present:
+ b.wait_visible(f'#containers-containers td[data-label="Container"]:contains("{container}")')
+ else:
+ b.wait_not_present(f'#containers-containers td[data-label="Container"]:contains("{container}")')
+
+ def performPodAction(self, podName, podOwner, action):
+ b = self.browser
+
+ b.click(f"#pod-{podName}-{podOwner}-action-toggle")
+ b.click(f"ul.pf-v5-c-menu__list li > button.pod-action-{action.lower()}")
+ b.wait_not_present("ul.pf-v5-c-menu__list")
+
+ def getStartTime(self, container: str, *, auth: bool) -> str:
+ # don't format the raw time strings from the API, force json format
+ out = self.execute(auth, "podman inspect --format '{{json .State.StartedAt}}' " + container)
+ return out.strip().replace('"', '')
+
+ def waitRestart(self, container: str, old_start: str, *, auth: bool) -> int:
+ for _ in range(10):
+ new_start = self.getStartTime(container, auth=auth)
+ if new_start > old_start:
+ return new_start
+ time.sleep(1)
+ else:
+ self.fail("Timed out waiting for StartedAt change")
+
+ def testPods(self):
+ b = self.browser
+
+ self.login()
+
+ self.filter_containers("running")
+ if not self.machine.ostree_image:
+ b.wait_in_text("#containers-containers", "No running containers")
+
+ # Run a pods as system
+ self.machine.execute("podman pod create --infra=false --name pod-1")
+
+ self.waitPodRow("pod-1", False)
+ self.filter_containers("all")
+ self.waitPodContainer("pod-1", [])
+
+ def get_pod_cpu_usage(pod_name):
+ cpu = self.browser.text(f"#table-{pod_name}-title .pod-cpu")
+ self.assertIn('%', cpu)
+ return float(cpu[:-1])
+
+ def get_pod_memory(pod_name):
+ memory = self.browser.text(f"#table-{pod_name}-title .pod-memory")
+ memory, unit = memory.split(' ')
+ self.assertIn(unit, ["GB", "MB", "KB"])
+ return float(memory)
+
+ run_cmd = f"podman run -d --pod pod-1 --name test-pod-1-system --stop-timeout 0 {IMG_ALPINE} sleep 100"
+ containerId = self.machine.execute(run_cmd).strip()
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Running", "id": containerId}])
+ cpu = get_pod_cpu_usage("pod-1")
+ b.wait(lambda: get_pod_memory("pod-1") > 0)
+
+ # Test that cpu usage increases
+ self.machine.execute("podman exec -di test-pod-1-system sh -c 'dd bs=1024 < /dev/urandom > /dev/null'")
+ b.wait(lambda: get_pod_cpu_usage("pod-1") > cpu)
+
+ self.machine.execute("podman pod stop -t0 pod-1") # disable timeout, so test doesn't wait endlessly
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
+ self.filter_containers("running")
+ self.waitPodRow("pod-1", False)
+
+ self.filter_containers("all")
+ b.set_input_text('#containers-filter', 'pod-1')
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
+ b.set_input_text('#containers-filter', 'test-pod-1-system')
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
+ # TODO add pixel test when this is again reachable - https://github.com/cockpit-project/bots/issues/2463
+
+ # Check Pod Actions
+ self.performPodAction("pod-1", "system", "Start")
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Running", "id": containerId}])
+
+ self.performPodAction("pod-1", "system", "Pause")
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Paused", "id": containerId}])
+
+ self.performPodAction("pod-1", "system", "Unpause")
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Running", "id": containerId}])
+
+ self.performPodAction("pod-1", "system", "Stop")
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": NOT_RUNNING, "id": containerId}])
+
+ self.machine.execute("podman pod start pod-1")
+ self.waitPodContainer("pod-1", [{"name": "test-pod-1-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Running", "id": containerId}])
+
+ old_start = self.getStartTime("test-pod-1-system", auth=True)
+ self.performPodAction("pod-1", "system", "Restart")
+ self.waitRestart("test-pod-1-system", old_start, auth=True)
+
+ self.performPodAction("pod-1", "system", "Delete")
+ b.click(".pf-v5-c-modal-box button:contains(Delete)")
+ # Alert should be shown, that running pods need to be force deleted.
+ b.wait_visible(".pf-v5-c-modal-box__body .pf-v5-c-alert")
+ b.wait_in_text(".pf-v5-c-modal-box__body .pf-v5-c-list", "test-pod-1-system")
+ b.click(".pf-v5-c-modal-box button:contains('Force delete')")
+ self.waitPodRow("pod-1", False)
+
+ # HACK: there is some race here which steals the focus from the filter input and selects the page text instead
+ for _ in range(3):
+ b.focus('#containers-filter')
+ time.sleep(1)
+ if b.eval_js('document.activeElement == document.querySelector("#containers-filter")'):
+ break
+ b.set_input_text('#containers-filter', '')
+ self.machine.execute("podman pod create --infra=false --name pod-2")
+ self.waitPodContainer("pod-2", [])
+ run_cmd = f"podman run -d --pod pod-2 --name test-pod-2-system --stop-timeout 0 {IMG_ALPINE} sleep 100"
+ containerId = self.machine.execute(run_cmd).strip()
+ self.waitPodContainer("pod-2", [{"name": "test-pod-2-system", "image": IMG_ALPINE,
+ "command": "sleep 100", "state": "Running", "id": containerId}])
+ self.machine.execute("podman rm --force -t0 test-pod-2-system")
+ self.waitPodContainer("pod-2", [])
+ self.performPodAction("pod-2", "system", "Delete")
+ b.wait_not_in_text(".pf-v5-c-modal-box__body", "test-pod-2-system")
+ b.click(".pf-v5-c-modal-box button:contains('Delete')")
+ self.waitPodRow("pod-2", False)
+
+ # Volumes / mounts
+ self.machine.execute("podman pod create -p 9999:9999 -v /tmp:/app --name pod-3")
+ self.machine.execute("podman pod start pod-3")
+
+ self.waitPodContainer("pod-3", [])
+ # Verify 1 port mapping
+ b.wait_in_text("#table-pod-3-title .pod-details-ports-btn", "1")
+ b.click("#table-pod-3-title .pod-details-ports-btn")
+ b.wait_in_text(".pf-v5-c-popover__content", "0.0.0.0:9999 → 9999/tcp")
+ # Verify 1 mount
+ b.wait_in_text("#table-pod-3-title .pod-details-volumes-btn", "1")
+ b.click("#table-pod-3-title .pod-details-volumes-btn")
+ b.wait_in_text(".pf-v5-c-popover__content", "/tmp ↔ /app")
+
+ def testBasicSystem(self):
+ self._testBasic(True)
+
+ b = self.browser
+
+ # Test dropping and gaining privileges
+ b.set_val("#containers-containers-owner", "all")
+ self.filter_containers("all")
+ self.execute(False, "podman pod create --infra=false --name pod_user")
+ self.execute(True, "podman pod create --infra=false --name pod_system")
+
+ checkImage(b, IMG_REGISTRY, "system")
+ checkImage(b, IMG_REGISTRY, "admin")
+ b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
+ b.wait_visible("#containers-containers .pod-name:contains('pod_system')")
+ b.wait_visible("#containers-containers .container-name:contains('a')")
+ b.wait_visible("#containers-containers .container-name:contains('b')")
+
+ # Drop privileges - all system things should disappear
+ if b.machine.system_before(293):
+ b.drop_superuser()
+ else:
+ drop_superuser(b)
+ b.wait_not_present("#containers-containers .pod-name:contains('pod_system')")
+ b.wait_not_present("#containers-containers .container-name:contains('a')")
+ b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
+ b.wait_visible("#containers-containers .container-name:contains('b')")
+ # Checking images is harder but if there would be more than one this would fail
+ b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")
+
+ # Owner select should disappear
+ b.wait_not_present("#containers-containers-owner")
+
+ # Also user selection in image download should not be visible
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Download new image)")
+
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
+ b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")
+ b.wait_not_present("#as-user")
+ b.click(".pf-v5-c-modal-box button:contains('Cancel')")
+ b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")')
+
+ # Gain privileges
+ if b.machine.system_before(293):
+ b.become_superuser(passwordless=self.machine.image == "rhel4edge")
+ else:
+ become_superuser(b, passwordless=self.machine.image == "rhel4edge")
+
+ # We are notified that we can also start the system one
+ b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available")
+ b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
+ b.wait_not_present("#overview div.pf-v5-c-alert .pf-v5-c-alert__title")
+
+ checkImage(b, IMG_REGISTRY, "system")
+ checkImage(b, IMG_REGISTRY, "admin")
+ b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
+ b.wait_visible("#containers-containers .pod-name:contains('pod_system')")
+ b.wait_visible("#containers-containers .container-name:contains('a')")
+ b.wait_visible("#containers-containers .container-name:contains('b')")
+
+ # Owner select should appear
+ b.wait_visible("#containers-containers-owner")
+
+ # Also user selection in image download should be visible
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Download new image)")
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
+ b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")
+ b.wait_visible("#as-user")
+ b.click(".pf-v5-c-modal-box button:contains('Cancel')")
+ b.wait_not_present('div.pf-v5-c-modal-box header:contains("Search for an image")')
+
+ # Check that when we filter only system stuff an then drop privileges that we show user stuff
+ b.set_val("#containers-containers-owner", "system")
+ b.wait_not_present("#containers-containers .pod-name:contains('pod_user')")
+ b.wait_not_present("#containers-containers .container-name:contains('b')")
+ # Checking images is harder but if there would be more than one this would fail
+ b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")
+
+ if b.machine.system_before(293):
+ b.drop_superuser()
+ else:
+ drop_superuser(b)
+
+ b.wait_visible("#containers-containers .pod-name:contains('pod_user')")
+ b.wait_visible("#containers-containers .container-name:contains('b')")
+ # Checking images is harder but if there would be more than one this would fail
+ b.wait_visible(f"#containers-images:contains('{IMG_REGISTRY}')")
+
+ # Check showing of entrypoint
+ b.click("#containers-containers-create-container-btn")
+ b.click("#create-image-image-select-typeahead")
+ b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")')
+ b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml')
+ b.wait_text("#run-image-dialog-entrypoint", '/entrypoint.sh')
+
+ # Deleting image will cleanup both command and entrypoint
+ b.click("button.pf-v5-c-select__toggle-clear")
+ b.wait_val("#run-image-dialog-command", '')
+ b.wait_not_present("#run-image-dialog-entrypoint")
+
+ # Edited command will not be cleared
+ b.click("#create-image-image-select-typeahead")
+ b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_REGISTRY}")')
+ b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yml')
+ b.set_input_text("#run-image-dialog-command", '/etc/docker/registry/config.yaml')
+ b.click("button.pf-v5-c-select__toggle-clear")
+ b.wait_not_present("#run-image-dialog-entrypoint")
+ b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml')
+
+ # Setting a new image will still keep the old command and not prefill it
+ b.click("#create-image-image-select-typeahead")
+ b.click(f'button.pf-v5-c-select__menu-item:contains({IMG_ALPINE})')
+ b.wait_visible("#run-image-dialog-pull-latest-image")
+ b.wait_val("#run-image-dialog-command", '/etc/docker/registry/config.yaml')
+
+ b.logout()
+
+ if self.machine.ostree_image:
+ self.machine.execute("echo foobar | passwd --stdin root")
+ self.write_file("/etc/ssh/sshd_config.d/99-root-password.conf", "PermitRootLogin yes",
+ post_restore_action="systemctl try-restart sshd")
+ self.machine.execute("systemctl try-restart sshd")
+
+ # Test that when root is logged in we don't present "user" and "system"
+ self.login_and_go("/podman", user="root", enable_root_login=True)
+ b.wait_visible("#app")
+
+ # `User Service is also available` banner should not be present
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+ # There should not be any duplicate images listed
+ # The "busybox" and "alpine" images have been deleted by _testBasic.
+ showImages(b)
+ self.waitNumImages(self.system_images_count - 2)
+ # There should not be 'owner' selector
+ b.wait_not_present("#containers-containers-owner")
+
+ # Test the isSystem boolean for searching
+ # https://github.com/cockpit-project/cockpit-podman/pull/891
+ b.click("#containers-containers-create-container-btn")
+ b.set_input_text("#create-image-image-select-typeahead", "registry")
+ b.wait_visible('button.pf-v5-c-select__menu-item:contains("registry")')
+
+ def testBasicUser(self):
+ self._testBasic(False)
+
+ def _testBasic(self, auth):
+ b = self.browser
+
+ def clickDeleteImage(image_sel):
+ b.click(f'{image_sel} .pf-v5-c-menu-toggle')
+ b.click(image_sel + " button.btn-delete")
+
+ if not auth:
+ self.allow_browser_errors("Failed to start system podman.socket.*")
+
+ expected_ws = ""
+ if auth and self.machine.ostree_image:
+ expected_ws += "ws"
+
+ self.login(auth)
+
+ # Check all containers
+ if auth:
+ checkImage(b, IMG_ALPINE, "system")
+ checkImage(b, IMG_BUSYBOX, "system")
+ checkImage(b, IMG_REGISTRY, "system")
+
+ checkImage(b, IMG_ALPINE, "admin")
+ checkImage(b, IMG_BUSYBOX, "admin")
+ checkImage(b, IMG_REGISTRY, "admin")
+
+ # Check order of images
+ text = b.text("#containers-images table")
+ if auth:
+ # all user images before all system images
+ self.assertRegex(text, ".*admin.*system.*")
+ self.assertNotRegex(text, ".*system.*admin.*")
+ else:
+ self.assertNotIn("system", text)
+ # images are sorted alphabetically
+ self.assertRegex(text, ".*/test-alpine.*/test-busybox.*/test-registry")
+
+ # build a dummy image so that the timestamp is "today" (for predictable pixel tests)
+ # ensure that CMD neither comes first (podman rmi leaves that layer otherwise)
+ # nor last (then the topmost layer does not match the image ID)
+ IMG_HELLO_LATEST = "localhost/test-hello:latest"
+ self.machine.execute(f"""set -eu; D={self.vm_tmpdir}/hello;
+ mkdir $D
+ printf 'FROM scratch\\nCOPY test.txt /\\nCMD ["/run.sh"]\\nCOPY test.txt /test2.txt\\n' > $D/Containerfile
+ echo hello > $D/test.txt""")
+ self.execute(auth, f"podman build -t {IMG_HELLO_LATEST} {self.vm_tmpdir}/hello")
+
+ # prepare image ids - much easier to pick a specific container
+ images = {}
+ for image in self.execute(auth, "podman images --noheading --no-trunc").strip().split("\n"):
+ # <name> <tag> sha256:<sha> <other things>
+ items = image.split()
+ images[f"{items[0]}:{items[1]}"] = items[2].split(":")[-1]
+
+ # show image listing toggle
+ hello_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_HELLO_LATEST]}{auth}\"]".lower()
+ b.wait_visible(hello_sel)
+ b.click(hello_sel + " td.pf-v5-c-table__toggle button")
+ b.click(hello_sel + " .pf-v5-c-menu-toggle")
+ b.wait_visible(hello_sel + " button.btn-delete")
+ b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details:first-child", "Command/run.sh")
+ # Show history
+ b.click("#containers-images tbody.pf-m-expanded .pf-v5-c-tabs__list li:nth-child(2) button")
+ first_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:first-of-type"
+ b.wait_in_text(f"{first_row_sel} td[data-label=\"ID\"]",
+ images[IMG_HELLO_LATEST][:12])
+ created_sel = f"{first_row_sel} td[data-label=\"Created\"]"
+ b.wait_in_text(f"{created_sel}", "today at")
+ # topmost (last) layer
+ created_sel = f"{first_row_sel} td[data-label=\"Created by\"]"
+ b.wait_in_text(f"{created_sel}", "COPY")
+ b.wait_in_text(f"{created_sel}", "in /test2.txt")
+ # initial (first) layer
+ last_row_sel = "#containers-images .pf-v5-c-table__expandable-row.pf-m-expanded tbody:last-of-type"
+ b.wait_in_text(f"{last_row_sel} td[data-label=\"Created by\"]", "COPY")
+
+ self.execute(auth, f"podman rmi {IMG_HELLO_LATEST}")
+ b.wait_not_present(hello_sel)
+
+ # make sure no running containers shown; on CoreOS there's the cockpit/ws container
+ self.filter_containers('running')
+ if auth and self.machine.ostree_image:
+ self.waitContainerRow("ws")
+ else:
+ b.wait_in_text("#containers-containers", "No running containers")
+
+ if auth:
+ # Run two containers as system (first exits immediately)
+ self.execute(auth, f"podman run -d --name test-sh-system --stop-timeout 0 {IMG_ALPINE} sh")
+ self.execute(auth, f"podman run -d --name swamped-crate-system --stop-timeout 0 {IMG_BUSYBOX} sleep 1000")
+
+ # Run two containers as admin (first exits immediately)
+ self.execute(False, f"podman run -d --name test-sh-user --stop-timeout 0 {IMG_ALPINE} sh")
+ self.execute(False, f"podman run -d --name swamped-crate-user --stop-timeout 0 {IMG_BUSYBOX} sleep 1000")
+
+ # Test owner filtering
+ if auth:
+ self.waitNumImages(self.user_images_count + self.system_images_count)
+ self.waitNumContainers(2, True)
+
+ def verify_system():
+ self.waitNumImages(self.system_images_count)
+ b.wait_in_text("#containers-images", "system")
+ self.waitNumContainers(1, True)
+ b.wait_in_text("#containers-containers", "system")
+
+ b.set_val("#containers-containers-owner", "system")
+ verify_system()
+ b.set_val("#containers-containers-owner", "all")
+ b.go("#/?owner=system")
+ verify_system()
+
+ def verify_user():
+ self.waitNumImages(self.user_images_count)
+ b.wait_in_text("#containers-images", "admin")
+ self.waitNumContainers(1, False)
+ b.wait_in_text("#containers-containers", "admin")
+
+ b.set_val("#containers-containers-owner", "user")
+ verify_user()
+ b.set_val("#containers-containers-owner", "all")
+ b.go("#/?owner=user")
+ verify_user()
+
+ b.set_val("#containers-containers-owner", "all")
+ self.waitNumImages(self.user_images_count + self.system_images_count)
+ self.waitNumContainers(2, True)
+ else: # No 'owner' selector when not privileged
+ b.wait_not_present("#containers-containers-owner")
+
+ user_containers = {}
+ system_containers = {}
+ for container in self.execute(True, "podman ps --all --no-trunc").strip().split("\n")[1:]:
+ # <sha> <other things> <name>
+ items = container.split()
+ system_containers[items[-1]] = items[0]
+ for container in self.execute(False, "podman ps --all --no-trunc").strip().split("\n")[1:]:
+ # <sha> <other things> <name>
+ items = container.split()
+ user_containers[items[-1]] = items[0]
+
+ # running busybox shown
+ if auth:
+ self.waitContainerRow("swamped-crate-system")
+ self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system',
+ image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')
+
+ self.waitContainerRow("swamped-crate-user")
+ self.waitContainer(user_containers["swamped-crate-user"], False, name='swamped-crate-user',
+ image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')
+
+ # exited alpine not shown
+ b.wait_not_in_text("#containers-containers", "alpine")
+
+ # show all containers and check status
+ b.go("#/?container=all")
+
+ # exited alpine under everything list
+ b.wait_visible("#containers-containers")
+ if auth:
+ self.waitContainer(system_containers["test-sh-system"], True, name='test-sh-system', image=IMG_ALPINE,
+ cmd='sh', state=NOT_RUNNING)
+
+ self.waitContainer(user_containers["test-sh-user"], False, name='test-sh-user', image=IMG_ALPINE,
+ cmd='sh', state=NOT_RUNNING)
+
+ self.performContainerAction("swamped-crate-user", "Delete")
+ self.confirm_modal("Cancel")
+
+ if auth:
+ self.performContainerAction("swamped-crate-system", "Delete")
+ self.confirm_modal("Cancel")
+
+ # Checked order of containers
+ expected = ["swamped-crate-user", "test-sh-user"]
+ if auth:
+ expected.extend(["swamped-crate-system", "test-sh-system"])
+ expected.extend([expected_ws])
+ b.wait_collected_text("#containers-containers .container-name", ''.join(sorted(expected)))
+
+ # show running container
+ self.filter_containers('running')
+ if auth:
+ self.waitContainer(system_containers["swamped-crate-system"], True, name='swamped-crate-system',
+ image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')
+ self.waitContainer(user_containers["swamped-crate-user"], False, name='swamped-crate-user',
+ image=IMG_BUSYBOX, cmd="sleep 1000", state='Running')
+ # check exited alpine not in running list
+ b.wait_not_in_text("#containers-containers", "alpine")
+
+ # delete running container busybox using force delete
+ if auth:
+ self.performContainerAction("swamped-crate-system", "Delete")
+ self.confirm_modal("Force delete")
+ self.waitContainerRow("swamped-crate-system", False)
+
+ self.filter_containers("all")
+
+ self.performContainerAction("swamped-crate-user", "Delete")
+ self.confirm_modal("Force delete")
+ self.waitContainerRow("swamped-crate-user", False)
+
+ self.waitContainerRow("test-sh-user")
+ self.performContainerAction("test-sh-user", "Delete")
+ self.confirm_modal("Delete")
+ b.wait_not_in_text("#containers-containers", "test-sh-user")
+
+ if auth:
+ self.waitContainerRow("test-sh-system")
+ self.performContainerAction("test-sh-system", "Delete")
+ self.confirm_modal("Delete")
+ b.wait_not_in_text("#containers-containers", "test-sh-system")
+
+ # delete image busybox that hasn't been used
+ # First try to just untag and then remove with more tags
+ self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:1")
+ self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:2")
+ self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:3")
+ self.execute(auth, f"podman tag {IMG_BUSYBOX} {IMG_BUSYBOX}:4")
+
+ busybox_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_BUSYBOX_LATEST]}{auth}\"]".lower()
+ b.click(busybox_sel + " td.pf-v5-c-table__toggle button")
+
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1")
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2")
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3")
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:4")
+
+ clickDeleteImage(busybox_sel)
+ self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']"))
+ b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:1']", True)
+ b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:3']", True)
+ b.set_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']", False)
+ self.confirm_modal("Delete")
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX_LATEST}")
+ b.wait_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:2")
+ b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:1")
+ b.wait_not_in_text(busybox_sel + " + tr", f"{IMG_BUSYBOX}:3")
+
+ clickDeleteImage(busybox_sel)
+ b.click("#delete-all")
+ self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX_LATEST}']"))
+ self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:2']"))
+ self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{IMG_BUSYBOX}:4']"))
+ self.confirm_modal("Delete")
+ self.confirm_modal("Force delete")
+ b.wait_not_present(busybox_sel)
+
+ # Check that we correctly show networking information
+ # Rootless don't have this info
+ if auth:
+ self.execute(auth, f"podman run -dt --name net_check --stop-timeout 0 {IMG_ALPINE}")
+ self.toggleExpandedContainer("net_check")
+ b.wait_in_text(".pf-m-expanded .container-details-networking",
+ self.execute(auth, """
+ podman inspect --format '{{.NetworkSettings.Gateway}}' net_check""").strip())
+ b.wait_in_text(".pf-m-expanded .container-details-networking",
+ self.execute(auth, """
+ podman inspect --format '{{.NetworkSettings.IPAddress}}' net_check""").strip())
+ b.wait_in_text(".pf-m-expanded .container-details-networking",
+ self.execute(auth, """
+ podman inspect --format '{{.NetworkSettings.MacAddress}}' net_check""").strip())
+ self.execute(auth, "podman stop net_check")
+ b.wait(lambda: self.execute(True, "podman ps --all | grep -e net_check -e Exited"))
+ self.toggleExpandedContainer("net_check")
+ sha = self.execute(auth, "podman inspect --format '{{.Id}}' net_check").strip()
+ self.waitContainer(sha, auth, state='Exited')
+
+ # delete image alpine that has been used by a container
+ self.execute(auth, f"podman run -d --name test-sh4 --stop-timeout 0 {IMG_ALPINE} sh")
+ # our pixel test expects both containers to be in state "Exited"
+ sha = self.execute(auth, "podman inspect --format '{{.Id}}' test-sh4").strip()
+ self.waitContainer(sha, auth, name="test-sh4", state='Exited')
+ if auth:
+ b.assert_pixels('#app', "overview", ignore=[".ignore-pixels"], skip_layouts=["rtl", "mobile"])
+ alpine_sel = f"#containers-images tbody tr[data-row-id=\"{images[IMG_ALPINE_LATEST]}{auth}\"]".lower()
+ b.wait_visible(alpine_sel)
+ b.click(alpine_sel + " td.pf-v5-c-table__toggle button")
+ clickDeleteImage(alpine_sel)
+ self.confirm_modal("Delete")
+ self.confirm_modal("Force delete")
+ b.wait_not_present(alpine_sel)
+
+ b.wait_collected_text("#containers-containers .container-name", expected_ws)
+ self.execute(auth, f"podman run -d --name c --stop-timeout 0 {IMG_REGISTRY} sh")
+ b.wait_collected_text("#containers-containers .container-name", "c" + expected_ws)
+ self.execute(auth, f"podman run -d --name a --stop-timeout 0 {IMG_REGISTRY} sh")
+ b.wait_collected_text("#containers-containers .container-name", "ac" + expected_ws)
+
+ self.execute(False, f"podman run -d --name b --stop-timeout 0 {IMG_REGISTRY} sh")
+ if auth:
+ b.wait_collected_text("#containers-containers .container-name", "abc" + expected_ws)
+ self.execute(False, f"podman run -d --name doremi --stop-timeout 0 {IMG_REGISTRY} sh")
+ b.wait_collected_text("#containers-containers .container-name", "abcdoremi" + expected_ws)
+ b.wait(lambda: self.getContainerAttr("doremi", "State") in NOT_RUNNING)
+ else:
+ b.wait_collected_text("#containers-containers .container-name", "abc")
+
+ # Test intermediate images
+ b.wait_not_present(".listing-action")
+ tmpdir = self.execute(auth, "mktemp -d").strip()
+ self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile")
+ self.execute(auth, f"podman build {tmpdir}")
+
+ b.wait_not_in_text("#containers-images", "<none>:<none>")
+ b.click(".listing-action button:contains('Show intermediate images')")
+ b.wait_in_text("#containers-images", "<none>:<none>")
+ b.wait_in_text("#containers-images tbody:last-child td[data-label=Created]", "today at")
+
+ b.click(".listing-action button:contains('Hide intermediate images')")
+ b.wait_not_in_text("#containers-images", "<none>:<none>")
+
+ # Intermediate images are not shown in create container dialog
+ b.click("#containers-containers-create-container-btn")
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+ b.click("#create-image-image-select-typeahead")
+ b.wait_visible(f".pf-v5-c-select__menu-item:contains('{IMG_REGISTRY}')")
+ b.wait_not_present(".pf-v5-c-select__menu-item:contains('none')")
+ b.click(".pf-v5-c-modal-box .btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ # Delete intermediate images
+ intermediate_image_sel = "#containers-images tbody:last-child:contains('<none>:<none>')"
+ b.click(".listing-action button:contains('Show intermediate images')")
+ clickDeleteImage(intermediate_image_sel)
+ self.confirm_modal("Delete")
+ b.wait_not_present(intermediate_image_sel)
+
+ # Create intermediate image and use it in a container
+ tmpdir = self.execute(auth, "mktemp -d").strip()
+ self.execute(auth, f"echo 'FROM {IMG_REGISTRY}\nRUN ls' > {tmpdir}/Dockerfile")
+ IMG_INTERMEDIATE = 'localhost/test-intermediate'
+ self.execute(auth, f"podman build -t {IMG_INTERMEDIATE} {tmpdir}")
+ b.click(f'#containers-images tbody tr:contains("{IMG_INTERMEDIATE}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+ b.click("#create-image-create-btn")
+ b.wait_not_present("div.pf-v5-c-modal-box")
+ self.waitContainerRow(IMG_INTERMEDIATE)
+
+ # Delete intermediate image which is in use
+ self.execute(auth, f"podman untag {IMG_INTERMEDIATE}")
+ clickDeleteImage(intermediate_image_sel)
+ self.confirm_modal("Delete")
+ self.confirm_modal("Force delete")
+ b.wait_not_in_text("#containers-images", "<none>:<none>")
+ b.wait_not_in_text("#containers-containers", IMG_INTERMEDIATE)
+
+ def testCommitUser(self):
+ self._testCommit(False)
+
+ def testCommitSystem(self):
+ self._testCommit(True)
+
+ def _testCommit(self, auth):
+ b = self.browser
+ self.allow_browser_errors("Failed to commit container .* repository name must be lowercase")
+
+ self.login(auth)
+
+ # run a container (will exit immediately) and test the display of commit modal
+ self.execute(auth, f"podman run -d --name test-sh0 --stop-timeout 0 {IMG_ALPINE} sh -c 'ls -a'")
+
+ self.filter_containers("all")
+ self.waitContainerRow("test-sh0")
+ self.toggleExpandedContainer("test-sh0")
+
+ self.performContainerAction("test-sh0", "Commit")
+ b.wait_visible(".pf-v5-c-modal-box")
+
+ b.wait_in_text(".pf-v5-c-modal-box__description", "state of the test-sh0 container")
+
+ # Empty name yields warning
+ b.click("button:contains(Commit)")
+ b.wait_text("#commit-dialog-image-name-helper", "Image name is required")
+ b.wait_visible("button:contains(Commit):disabled")
+ b.wait_visible("button:contains('Force commit')")
+ # Warning should be cleaned when updating name
+ b.set_input_text("#commit-dialog-image-name", "foobar")
+ b.wait_not_present("button:contains('Force commit')")
+ b.wait_not_present("#commit-dialog-image-name-helper")
+
+ # Existing name yields warning
+ b.set_input_text("#commit-dialog-image-name", IMG_ALPINE)
+ b.click("button:contains(Commit)")
+ b.wait_text("#commit-dialog-image-name-helper", "Image name is not unique")
+ b.wait_visible("button:contains(Commit):disabled")
+ b.wait_visible("button:contains('Force commit')")
+ # Warning should be cleaned when updating tag
+ b.set_input_text("#commit-dialog-image-tag", "foobar")
+ b.wait_not_present("button:contains('Force commit')")
+ b.wait_not_present("#commit-dialog-image-name-helper")
+
+ # Check failing commit
+ b.set_input_text("#commit-dialog-image-name", "TEST")
+ b.click("button:contains(Commit)")
+ b.wait_in_text(".pf-v5-c-alert", "Failed to commit container test-sh0")
+ b.wait_in_text(".pf-v5-c-alert", "repository name must be lowercase")
+
+ # Test cancel
+ self.confirm_modal("Cancel")
+
+ # Force commit empty container
+ self.performContainerAction("test-sh0", "Commit")
+ b.wait_visible(".pf-v5-c-modal-box")
+ # We prefill command
+ b.wait_val("#commit-dialog-command", 'sh -c "ls -a"')
+ # Test docker format
+ b.set_checked("#commit-dialog-docker", True)
+ b.click("button:contains(Commit)")
+ self.confirm_modal("Force commit")
+
+ # don't use waitNumImages() here, as we want to include anonymous images
+ def waitImageCount(expected):
+ if auth:
+ expected += self.system_images_count
+
+ b.wait_in_text("#containers-images", f"{expected} images")
+
+ waitImageCount(self.user_images_count + 1)
+ image_id = self.execute(auth, "podman images --sort created --format '{{.Id}}' | head -n 1").strip()
+ manifest_type = self.execute(auth, "podman inspect --format '{{.ManifestType}}' " + image_id).strip()
+ cmd = self.execute(auth, "podman inspect --format '{{.Config.Cmd}}' " + image_id).strip()
+ self.assertIn("docker.distribution.manifest", manifest_type)
+ self.assertEqual("[sh -c ls -a]", cmd)
+
+ # Commit with name, tag, author and edited command
+ self.performContainerAction("test-sh0", "Commit")
+ b.wait_visible(".pf-v5-c-modal-box")
+ b.set_input_text("#commit-dialog-image-name", "newname")
+ b.set_input_text("#commit-dialog-image-tag", "24")
+ b.set_input_text("#commit-dialog-author", "MM")
+ b.set_input_text("#commit-dialog-command", "sh -c 'ps'")
+
+ if auth:
+ b.assert_pixels(".pf-v5-c-modal-box", "commit", skip_layouts=["rtl"])
+
+ self.confirm_modal("Commit")
+
+ waitImageCount(self.user_images_count + 2)
+ self.assertEqual(self.execute(auth, "podman inspect --format '{{.Author}}' newname:24").strip(), "MM")
+ self.assertEqual(self.execute(auth, "podman inspect --format '{{.Config.Cmd}}' newname:24").strip(),
+ "[sh -c ps]")
+ self.assertIn("vnd.oci.image.manifest",
+ self.execute(auth, "podman inspect --format '{{.ManifestType}}' newname:24").strip())
+
+ # Test commit of running container
+ self.execute(auth, f"podman run -d --name test-sh2 --stop-timeout 0 {IMG_BUSYBOX} sleep 1000")
+ self.performContainerAction("test-sh2", "Commit")
+ b.wait_visible(".pf-v5-c-modal-box")
+ b.set_input_text("#commit-dialog-image-name", "newname")
+ self.confirm_modal("Commit")
+ waitImageCount(self.user_images_count + 3)
+ self.assertEqual(self.execute(auth,
+ "podman inspect --format '{{.Config.Cmd}}' newname:latest").strip(),
+ "[sleep 1000]")
+
+ # Test commit of running container with pause (also conflicting name through :latest)
+ # This only works on rootless with cgroupsv2
+ if auth or self.has_cgroupsV2:
+ self.performContainerAction("test-sh2", "Commit")
+ b.wait_visible(".pf-v5-c-modal-box")
+ b.set_input_text("#commit-dialog-image-name", "newname")
+ b.set_checked("#commit-dialog-pause", True)
+ b.click("button:contains(Commit)")
+ self.confirm_modal("Force commit")
+ waitImageCount(self.user_images_count + 4)
+
+ def testDownloadImage(self):
+ b = self.browser
+ execute = self.execute
+
+ def prepare():
+ # Create and start registry containers
+ self.execute(True, f"podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
+ self.execute(True, f"podman run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}")
+ # Add local insecure registry into registries conf
+ self.machine.write("/etc/containers/registries.conf", REGISTRIES_CONF)
+ self.execute(True, "systemctl stop podman.service")
+ # Push busybox image to the local registries
+ self.execute(True,
+ f"podman tag {IMG_BUSYBOX} localhost:5000/my-busybox; podman push localhost:5000/my-busybox")
+ self.execute(True,
+ f"podman tag {IMG_BUSYBOX} localhost:6000/my-busybox; podman push localhost:6000/my-busybox")
+ # Untag busybox image which duplicates the image we are about to download
+ self.execute(True, f"podman rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox")
+ self.execute(False, f"podman rmi -f {IMG_BUSYBOX}")
+
+ class DownloadImageDialog():
+ def __init__(self, test_obj, imageName, imageTag=None, user="system"):
+ self.imageName = imageName
+ self.imageTag = imageTag
+ self.user = user
+ self.imageSha = ""
+ self.assertTrue = test_obj.assertTrue
+
+ def openDialog(self):
+ # Open get new image modal
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Download new image)")
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
+ b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):disabled")
+
+ return self
+
+ def fillDialog(self):
+ # Search for image specified with self.imageName and self.imageTag
+ b.click(f"#{self.user}")
+ b.set_val('#registry-select', "localhost:5000")
+ # HACK: Sometimes the value is not shown fully. FIXME
+ b.set_input_text("#search-image-dialog-name", self.imageName, value_check=False)
+ if self.imageTag:
+ b.set_input_text(".image-tag-entry input", self.imageTag)
+
+ return self
+
+ def selectImageAndDownload(self):
+ # Select and download the self.imageName image
+ b.wait_visible(f".pf-v5-c-data-list .image-name:contains({self.imageName})")
+ b.click(f".pf-v5-c-data-list .image-name:contains({self.imageName})")
+ b.wait_visible("div.pf-v5-c-modal-box footer button:contains(Download):not([disabled])")
+ b.click("div.pf-v5-c-modal-box footer button:contains(Download)")
+ b.wait_not_present("div.pf-v5-c-modal-box")
+
+ return self
+
+ def expectDownloadErrorForNonExistingTag(self):
+ title = f"Danger alert:Failed to download image localhost:5000/{self.imageName}:{self.imageTag}"
+ b.wait_visible(f'h4.pf-v5-c-alert__title:contains("{title}")')
+
+ return self
+
+ def expectSearchErrorForNotExistingImage(self):
+ b.wait_visible(f".pf-v5-c-modal-box__body:contains(No results for {self.imageName})")
+ b.click(".pf-v5-c-modal-box button.btn-cancel")
+ b.wait_not_present(".pf-v5-c-modal-box__body")
+
+ return self
+
+ def expectDownloadSuccess(self):
+ # Confirm that the modal dialog is not open anymore
+ b.wait_not_present('div.pf-v5-c-modal-box')
+ # Confirm that the image got downloaded
+ checkImage(b,
+ f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}",
+ "system" if self.user == "system" else "admin")
+
+ # Confirm that no error has happened
+ b.wait_not_present('h4.pf-v5-c-alert__title:contains("Failed to download image")')
+
+ # Find out this image ID
+ container_name = f"localhost:5000/{self.imageName}:{self.imageTag or 'latest'}"
+ self.imageSha = execute(self.user == "system",
+ f"podman inspect --format '{{{{.Id}}}}' {container_name}").strip()
+
+ return self
+
+ def deleteImage(self, force=False, another=None):
+ imageTagSuffix = ":" + (self.imageTag or 'latest')
+
+ # Select the image row
+
+ # show image listing toggle
+ imageId = f"{self.imageSha}{'true' if self.user == 'system' else 'false'}"
+ sel = f"#containers-images tbody tr[data-row-id=\"{imageId}\"]"
+ b.wait_visible(sel)
+ b.click(sel + " td.pf-v5-c-table__toggle button")
+
+ # Click the delete icon on the image row
+ b.click(sel + " .pf-v5-c-menu-toggle")
+ b.click(sel + ' button.btn-delete')
+
+ if another:
+ b.click("#delete-all")
+ sel = f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']"
+ self.assertTrue(b.get_checked(sel))
+ self.assertTrue(b.get_checked(f".pf-v5-c-check__input[aria-label='{another}']"))
+ b.click("#delete-all")
+ b.wait_visible("#btn-img-delete:disabled")
+
+ b.set_checked(
+ f".pf-v5-c-check__input[aria-label='localhost:5000/{self.imageName}{imageTagSuffix}']", True)
+ b.set_checked(f".pf-v5-c-check__input[aria-label='{another}']", True)
+
+ # Confirm deletion in the delete dialog
+ b.click(".pf-v5-c-modal-box #btn-img-delete")
+
+ if force:
+ # Confirm force delete
+ b.click(".pf-v5-c-modal-box button:contains('Force delete')")
+
+ b.wait_not_present(sel)
+
+ return self
+
+ prepare()
+
+ self.login()
+
+ # Test registries
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Download new image)")
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Search for an image")')
+ # HACK: Sometimes the value is not shown fully. FIXME
+ b.set_input_text("#search-image-dialog-name", "my-busybox", value_check=False)
+
+ b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')")
+ b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')")
+ b.assert_pixels(".podman-search", "download", skip_layouts=["rtl"])
+
+ b.set_val('#registry-select', "localhost:6000")
+ b.wait_not_present(".pf-v5-c-data-list .image-name:contains('localhost:5000/my-busybox')")
+ b.wait_visible(".pf-v5-c-data-list .image-name:contains('localhost:6000/my-busybox')")
+ b.click(".pf-v5-c-modal-box button:contains('Cancel')")
+ b.wait_not_present('div.pf-v5-c-modal-box')
+
+ dialog0 = DownloadImageDialog(self, imageName='my-busybox', user="system")
+ dialog0.openDialog() \
+ .fillDialog() \
+ .selectImageAndDownload() \
+ .expectDownloadSuccess()
+ dialog0.deleteImage()
+
+ dialog1 = DownloadImageDialog(self, imageName='my-busybox', user="user")
+ dialog1.openDialog() \
+ .fillDialog() \
+ .selectImageAndDownload() \
+ .expectDownloadSuccess()
+ # test recognition/deletion of multiple image tags
+ second_tag = "localhost/copybox:latest"
+ self.execute(False, f"podman tag localhost:5000/my-busybox {second_tag}")
+ # expand details
+ b.click("#containers-images tr:contains('my-busybox') td.pf-v5-c-table__toggle button")
+ b.wait_in_text("#containers-images tbody.pf-m-expanded tr .image-details", second_tag)
+ dialog1.deleteImage(True, another=second_tag)
+
+ dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='latest', user="system")
+ dialog.openDialog() \
+ .fillDialog() \
+ .selectImageAndDownload() \
+ .expectDownloadSuccess() \
+ .deleteImage()
+
+ dialog = DownloadImageDialog(self, imageName='foobar')
+ dialog.openDialog() \
+ .fillDialog() \
+ .expectSearchErrorForNotExistingImage()
+
+ dialog = DownloadImageDialog(self, imageName='my-busybox', imageTag='foobar')
+ dialog.openDialog() \
+ .fillDialog() \
+ .selectImageAndDownload() \
+ .expectDownloadErrorForNonExistingTag()
+
+ def testLifecycleOperationsUser(self):
+ self._testLifecycleOperations(False)
+
+ def testLifecycleOperationsSystem(self):
+ self._testLifecycleOperations(True)
+
+ def _testLifecycleOperations(self, auth):
+ b = self.browser
+
+ if not auth:
+ self.allow_browser_errors("Failed to start system podman.socket.*")
+
+ self.login()
+ self.filter_containers('all')
+
+ # run a container
+ self.execute(auth, f"""
+ podman run -d --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh -c 'echo 123; sleep infinity';
+ podman stop swamped-crate""")
+ b.wait(lambda: self.execute(auth, "podman ps --all | grep -e swamped-crate -e Exited"))
+
+ b.wait_visible("#containers-containers")
+ container_sha = self.execute(auth, "podman inspect --format '{{.Id}}' swamped-crate").strip()
+ self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX,
+ state='Exited', owner="system" if auth else "admin")
+ b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle")
+
+ if not auth:
+ # Checkpoint/restore is not supported on user containers yet - the related buttons should not be shown
+ # Check that the restore option is not present
+ b.wait_not_present(self.getContainerAction('swamped-crate', 'Restore'))
+
+ # Health check is not set up
+ b.wait_not_present(self.getContainerAction('swamped-crate', 'Run health check'))
+
+ b.click("#containers-containers tbody tr:contains('swamped-crate') .pf-v5-c-menu-toggle")
+
+ # Start the container
+ self.performContainerAction(IMG_BUSYBOX, "Start")
+
+ self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX,
+ state='Running', owner="system" if auth else "admin")
+
+ def get_cpu_usage(sel):
+ cpu = self.getContainerAttr(sel, "CPU")
+ self.assertIn('%', cpu)
+ # If it not a number it will raise ValueError which is what we want to know
+ return float(cpu[:-1])
+
+ # Check we show usage
+ b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "CPU") != "")
+ b.wait(lambda: self.getContainerAttr(IMG_BUSYBOX, "Memory") != "")
+ memory = self.getContainerAttr(IMG_BUSYBOX, "Memory")
+ if auth or self.has_cgroupsV2:
+ cpu = get_cpu_usage(IMG_BUSYBOX)
+
+ self.assertIn('/', memory)
+ numbers = memory.split('/')
+ self.assertTrue(numbers[0].strip().replace('.', '', 1).isdigit())
+ full = numbers[1].strip().split()
+ self.assertTrue(full[0].replace('.', '', 1).isdigit())
+ self.assertIn(full[1], ["GB", "MB"])
+
+ # Test that the value is updated dynamically
+ self.execute(auth, "podman exec -i swamped-crate sh -c 'dd bs=1024 < /dev/urandom > /dev/null &'")
+ b.wait(lambda: get_cpu_usage(IMG_BUSYBOX) > cpu)
+ self.execute(auth, "podman exec swamped-crate sh -c 'pkill dd'")
+ else:
+ # No support for CGroupsV2
+ self.assertEqual(self.getContainerAttr(IMG_BUSYBOX, "CPU"), "n/a")
+ self.assertEqual(memory, "n/a")
+
+ # Restart the container; there is no steady state change in the visible UI, so look for
+ # a changed data-started-at attribute
+ old_start = self.getStartTime("swamped-crate", auth=auth)
+ b.wait_in_text(f'#containers-containers tr[data-started-at="{old_start}"]', "swamped-crate")
+ self.performContainerAction(IMG_BUSYBOX, "Force restart")
+ new_start = self.waitRestart("swamped-crate", old_start, auth=auth)
+ b.wait_in_text(f'#containers-containers tr[data-started-at="{new_start}"]', "swamped-crate")
+ self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state='Running')
+
+ self.waitContainerRow(IMG_BUSYBOX)
+ if not auth:
+ # Check that the checkpoint option is not present for rootless
+ b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
+ b.wait_visible(self.getContainerAction(IMG_BUSYBOX, 'Force stop'))
+ b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Checkpoint'))
+ b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
+ # Stop the container
+ self.performContainerAction(IMG_BUSYBOX, "Force stop")
+
+ self.waitContainer(container_sha, auth, name='swamped-crate', image=IMG_BUSYBOX)
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "CPU") == "")
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "Memory") == "")
+
+ # Check that container details are not lost when the container is stopped
+ self.toggleExpandedContainer("swamped-crate")
+ b.click(".pf-m-expanded button:contains('Integration')")
+ b.wait_visible(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables")')
+
+ # Check that console reconnects when container starts
+ b.click(".pf-m-expanded button:contains('Console')")
+ b.wait_text(".pf-m-expanded .pf-v5-c-empty-state", "Container is not running")
+ self.performContainerAction("swamped-crate", "Start")
+ b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ")
+ b.focus(".pf-m-expanded .xterm-helper-textarea")
+ b.key_press('clear\r')
+ b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
+ b.key_press('echo hello\r')
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ")
+ self.performContainerAction("swamped-crate", "Stop")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # disconnected ")
+ sha = self.execute(auth, "podman inspect --format '{{.Id}}' swamped-crate").strip()
+ self.waitContainer(sha, auth, name='swamped-crate', image=IMG_BUSYBOX, state=NOT_RUNNING)
+ self.performContainerAction("swamped-crate", "Start")
+ self.waitContainer(sha, auth, state='Running')
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
+ b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")
+
+ # Check that logs reconnect when container starts
+ b.click(".pf-m-expanded button:contains('Logs')")
+ self.performContainerAction("swamped-crate", "Stop")
+ self.waitContainer(sha, auth, state=NOT_RUNNING)
+ b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected")
+ self.performContainerAction("swamped-crate", "Start")
+ b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "Streaming disconnected123")
+
+ def testCheckpointRestore(self):
+ m = self.machine
+ b = self.browser
+
+ self.login()
+ self.filter_containers('all')
+
+ if not self.has_criu:
+ # On cgroupsv1 systems just check that we get expected error messages
+
+ # Run a container
+ self.execute(True, f"podman run -dit --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh")
+ b.wait(lambda: self.execute(True, "podman ps --all | grep -e swamped-crate"))
+
+ # Checkpoint the container
+ self.performContainerAction(IMG_BUSYBOX, "Checkpoint")
+ b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True)
+ b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True)
+ b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')
+ b.wait_not_present('.modal_dialog')
+
+ def criu_alert():
+ text = b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower()
+ return "checkpoint/restore requires at least criu" in text or "failed to check for criu" in text
+ b.wait(criu_alert)
+ return
+
+ # Run a container
+ mac_address = '92:d0:c6:0a:29:38'
+ self.execute(True, f"""
+ podman run -dit --mac-address {mac_address} --name swamped-crate --stop-timeout 0 {IMG_BUSYBOX} sh;
+ podman stop swamped-crate
+ """)
+ b.wait(lambda: self.execute(True, "podman ps --all | grep -e swamped-crate -e Exited"))
+
+ # Check that the restore option is not present (i.e. start is a regular button)
+ b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
+ b.wait_not_present(self.getContainerAction(IMG_BUSYBOX, 'Restore'))
+ b.click(f"#containers-containers tbody tr:contains('{IMG_BUSYBOX}') .pf-v5-c-menu-toggle")
+
+ # Start the container
+ self.performContainerAction("swamped-crate", "Start")
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')
+
+ self.toggleExpandedContainer("swamped-crate")
+ b.wait_visible(".pf-m-expanded button:contains('Details')")
+ b.wait_not_present(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint")')
+
+ # Checkpoint the container
+ self.performContainerAction("swamped-crate", "Checkpoint")
+ b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-keep', True)
+ b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-tcpEstablished', True)
+ b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')
+
+ with b.wait_timeout(300):
+ b.wait_not_present(".pf-v5-c-modal-box")
+
+ if self.has_criu:
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)
+ b.wait_in_text(
+ f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Latest checkpoint") + dd',
+ 'today at'
+ )
+ else:
+ # expect proper error message
+ b.wait_in_text(".pf-v5-c-alert.pf-m-danger", "Failed to checkpoint container swamped-crate")
+ b.wait(lambda: "checkpoint/restore requires at least criu" in
+ b.text(".pf-v5-c-alert.pf-m-danger > .pf-v5-c-alert__description").lower())
+ return
+
+ # Restore the container
+ self.waitContainerRow("swamped-crate")
+ self.performContainerAction("swamped-crate", "Restore")
+ b.set_checked('.pf-v5-c-modal-box input#restore-dialog-keep', True)
+ b.set_checked('.pf-v5-c-modal-box input#restore-dialog-tcpEstablished', True)
+ b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticIP', True)
+ b.set_checked('.pf-v5-c-modal-box input#restore-dialog-ignoreStaticMAC', True)
+ b.click('.pf-v5-c-modal-box button:contains(Restore)')
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')
+
+ # A new MAC address should have been generated
+ # Fixed in podman 4.4.0 https://github.com/containers/podman/issues/16666
+ cmd = "podman inspect --format '{{.NetworkSettings.MacAddress}}' swamped-crate"
+ new_mac_address = self.execute(True, cmd).strip()
+ if podman_version(self) >= (4, 4, 0):
+ self.assertNotEqual(new_mac_address, mac_address)
+ else:
+ self.assertEqual(new_mac_address, mac_address)
+
+ # Checkpoint the container without stopping
+ self.waitContainerRow("swamped-crate")
+ self.performContainerAction("swamped-crate", "Checkpoint")
+ b.set_checked('.pf-v5-c-modal-box input#checkpoint-dialog-leaveRunning', True)
+ b.click('.pf-v5-c-modal-box button:contains(Checkpoint)')
+ b.wait_not_present('.modal_dialog')
+
+ # Stop the container
+ m.execute("podman stop swamped-crate")
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in NOT_RUNNING)
+
+ # Restore the container
+ self.performContainerAction("swamped-crate", "Restore")
+ b.click('.pf-v5-c-modal-box button:contains(Restore)')
+ b.wait(lambda: self.getContainerAttr("swamped-crate", "State") in 'Running')
+
+ def testNotRunning(self):
+ b = self.browser
+
+ def disable_system():
+ self.execute(True, "systemctl disable --now podman.socket; systemctl stop podman.service")
+
+ def enable_system():
+ self.execute(True, "systemctl enable --now podman.socket")
+
+ def enable_user():
+ self.execute(False, "systemctl --user enable --now podman.socket")
+
+ def disable_user():
+ self.execute(False, "systemctl --user disable --now podman.socket")
+
+ def is_active_system(string):
+ b.wait(lambda: self.execute(True, "systemctl is-active podman.socket || true").strip() == string)
+
+ def is_enabled_system(string):
+ b.wait(lambda: self.execute(True, "systemctl is-enabled podman.socket || true").strip() == string)
+
+ def is_active_user(string):
+ b.wait(lambda: self.execute(False, "systemctl --user is-active podman.socket || true").strip() == string)
+
+ def is_enabled_user(string):
+ b.wait(lambda: self.execute(False, "systemctl --user is-enabled podman.socket || true").strip() == string)
+
+ disable_system()
+ disable_user()
+ self.login_and_go("/podman")
+
+ # Troubleshoot action
+ b.click("#app .pf-v5-c-empty-state button.pf-m-link")
+ b.enter_page("/system/services")
+ # services page is too slow
+ with b.wait_timeout(60):
+ b.wait_in_text("#service-details", "podman.socket")
+
+ # Start action, with enabling (by default)
+ b.go("/podman")
+ b.enter_page("/podman")
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+
+ b.wait_visible("#containers-containers")
+ b.wait_not_present("#overview div.pf-v5-c-alert.pf-m-info")
+
+ is_active_system("active")
+ is_active_user("active")
+ is_enabled_system("enabled")
+ is_enabled_user("enabled")
+
+ # Start action, without enabling
+ disable_system()
+ disable_user()
+ b.click("#app .pf-v5-c-empty-state input[type=checkbox]")
+ b.assert_pixels("#app .pf-v5-c-empty-state", "podman-service-disabled", skip_layouts=["medium", "mobile"])
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+
+ b.wait_visible("#containers-containers")
+ is_enabled_system("disabled")
+ is_enabled_user("disabled")
+ is_active_system("active")
+ is_active_user("active")
+
+ b.logout()
+ disable_system()
+ # HACK: Due to https://github.com/containers/podman/issues/7180, avoid
+ # user podman.service to time out; make sure to start it afresh
+ disable_user()
+ enable_user()
+ self.login_and_go("/podman")
+ b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "System Podman service is also available")
+ b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+ is_active_system("active")
+ is_active_user("active")
+ is_enabled_user("enabled")
+ is_enabled_system("enabled")
+
+ b.logout()
+ disable_user()
+ enable_system()
+ self.login_and_go("/podman")
+ b.wait_in_text("#overview div.pf-v5-c-alert .pf-v5-c-alert__title", "User Podman service is also available")
+ b.click("#overview div.pf-v5-c-alert .pf-v5-c-alert__action > button:contains(Start)")
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+ is_active_system("active")
+ is_active_user("active")
+ is_enabled_user("enabled")
+ is_enabled_system("enabled")
+
+ b.logout()
+ disable_user()
+ disable_system()
+ self.login_and_go("/podman", superuser=False)
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_visible("#containers-containers")
+ b.wait_not_present("#overview div.pf-v5-c-alert")
+
+ is_active_system("inactive")
+ is_active_user("active")
+ is_enabled_user("enabled")
+ is_enabled_system("disabled")
+ b.logout()
+
+ # no Troubleshoot action without cockpit-system package
+ disable_system()
+ disable_user()
+ self.restore_dir("/usr/share/cockpit/systemd")
+ self.machine.execute("rm /usr/share/cockpit/systemd/manifest.json")
+ self.login_and_go("/podman")
+ b.wait_visible("#app .pf-v5-c-empty-state button.pf-m-primary")
+ self.assertFalse(b.is_present("#app .pf-v5-c-empty-state button.pf-m-link"))
+ # starting still works
+ b.click("#app .pf-v5-c-empty-state button.pf-m-primary")
+ b.wait_visible("#containers-containers")
+
+ self.allow_restart_journal_messages()
+ self.allow_journal_messages(".*podman/podman.sock/.*: couldn't connect:.*")
+ self.allow_journal_messages(".*podman/podman.sock: .*Connection.*Error.*")
+ self.allow_journal_messages(".*podman/podman.sock/.*/events.*: received truncated HTTP response.*")
+
+ def testCreateContainerSystem(self):
+ self._testCreateContainer(True)
+
+ def testCreateContainerUser(self):
+ self._testCreateContainer(False)
+
+ def _testCreateContainer(self, auth):
+ new_container = 'new-container'
+ self.execute(True, f"podman run -d --name {new_container} --stop-timeout 0 {IMG_BUSYBOX} touch /latest")
+ self.execute(True, f"podman commit {new_container} newimage")
+ new_image_sha = self.execute(True, "podman inspect --format '{{.Id}}' newimage").strip()
+
+ self.execute(True, f"podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
+ self.execute(True, f"podman run -d -p 6000:5000 --name registry_alt --stop-timeout 0 {IMG_REGISTRY}")
+ # Add local insecure registry into registries conf
+ self.machine.write("/etc/containers/registries.conf", REGISTRIES_CONF)
+ self.execute(True, "systemctl stop podman.service")
+ # Push busybox image to the local registries
+ self.execute(True,
+ f"podman tag {IMG_BUSYBOX} localhost:5000/my-busybox; podman push localhost:5000/my-busybox")
+ self.execute(True,
+ f"podman tag {IMG_BUSYBOX} localhost:6000/my-busybox; podman push localhost:6000/my-busybox")
+ # Untag busybox image which duplicates the image we are about to download
+ self.execute(True, f"podman rmi -f {IMG_BUSYBOX} localhost:5000/my-busybox localhost:6000/my-busybox")
+
+ self.login(auth)
+
+ b = self.browser
+ container_name = "busybox-downloaded"
+
+ b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
+ b.set_input_text("#run-image-dialog-name", container_name)
+
+ # Test invalid input
+ b.set_input_text("#create-image-image-select-typeahead", "|alpi*ne?\\")
+ b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost/test-alpine:latest")
+
+ # No local results found
+ b.set_input_text("#create-image-image-select-typeahead", "notfound")
+
+ b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
+ b.wait_text("button.pf-v5-c-select__menu-item.pf-m-disabled", "No images found")
+
+ # Local results found
+ b.set_input_text("#create-image-image-select-typeahead", "registry")
+ if auth:
+ b.assert_pixels(".pf-v5-c-modal-box", "image-select", skip_layouts=["rtl"])
+ b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
+ b.wait_text("button.pf-v5-c-select__menu-item", IMG_REGISTRY_LATEST)
+
+ # Local registry
+ b.set_input_text("#create-image-image-select-typeahead", "my-busybox")
+ b.click('button.pf-v5-c-toggle-group__button:contains("localhost:5000")')
+ b.wait_text("button.pf-v5-c-select__menu-item:not(.pf-m-disabled)", "localhost:5000/my-busybox")
+
+ # Select image
+ b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")')
+
+ # Remote image, no pull latest image option
+ b.wait_not_present("#run-image-dialog-pull-latest-image")
+
+ # Create Container, image is pulled and should end up being "running"
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ sel = " span:not(.downloading)"
+ b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')
+ self.execute(auth, f"podman exec {container_name} test ! -e /latest")
+
+ # Now that we have downloaded an image, verify that selecting download latest image
+ # downloads the latest image we now push to the registry. Note this image has a /latest file
+ # to differnatiate it from the other local image.
+ self.execute(True, f"podman push {new_image_sha} localhost:5000/my-busybox")
+ self.execute(True, f"podman push {new_image_sha} localhost:6000/my-busybox")
+ self.execute(True, f"podman rmi {new_image_sha}")
+
+ container_name = "busybox-latest"
+
+ b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
+ b.set_input_text("#run-image-dialog-name", container_name)
+
+ # Local registry
+ b.set_input_text("#create-image-image-select-typeahead", "my-busybox")
+ b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
+
+ # Select image
+ b.click('button.pf-v5-c-select__menu-item:contains("localhost:5000/my-busybox")')
+
+ # Pull the latest image
+ b.set_checked("#run-image-dialog-pull-latest-image", True)
+
+ # Create Container, image is pulled and should end up being "running"
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ sel = " span:not(.downloading)"
+ b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')
+ # Verify that the latest file exists
+ output = self.execute(auth, f"podman exec {container_name} ls -lh /latest").strip()
+ self.assertNotIn("No such file or directory", output)
+
+ # Test creating a container with
+ if auth:
+ container_name = "busybox-download-admin"
+ b.click("#containers-containers button.pf-v5-c-button.pf-m-primary")
+
+ # Start container as admin
+ b.click('#run-image-dialog-owner-user')
+
+ # Create Container, image is pulled and should end up being "Running"
+ b.set_input_text("#run-image-dialog-name", container_name)
+
+ b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX)
+ b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
+ b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX}")')
+
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ b.wait(lambda: self.getContainerAttr(container_name, "State", sel) in 'Running')
+
+ def testRunImageSystem(self):
+ self._testRunImage(True)
+
+ def testRunImageUser(self):
+ self._testRunImage(False)
+
+ def _testRunImage(self, auth):
+ b = self.browser
+ m = self.machine
+
+ # Just drop user images so we can use simpler selectors
+ if auth:
+ self.execute(False, "podman rmi --all")
+
+ self.login(auth)
+
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ b.wait_in_text("#containers-images", IMG_BUSYBOX)
+ b.wait_in_text("#containers-images", IMG_ALPINE)
+ if auth:
+ b.wait_not_in_text("#containers-images", "admin")
+
+ # Check command in alpine
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_ALPINE}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+ # depending on the precise container, this can be /bin/sh or /bin/ash
+ cmd = self.execute(auth, 'podman image inspect --format "{{.Config.Cmd}}" ' + IMG_ALPINE)
+ cmd = cmd.strip().replace('[', '').replace(']', '')
+ b.wait_attr("#run-image-dialog-command", "value", cmd)
+ b.click(".btn-cancel")
+
+ # Open run image dialog
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ # Inspect and fill modal dialog
+ b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
+
+ # Check that there is autogenerated name and then overwrite it
+ b.wait_not_val("#run-image-dialog-name", "")
+ b.set_input_text("#run-image-dialog-name", "busybox-with-tty")
+
+ b.wait_visible("#run-image-dialog-command[value='sh']")
+
+ # Check memory configuration
+ # Only works with CGroupsV2
+ if auth or self.has_cgroupsV2:
+ b.set_checked("#run-image-dialog-memory-limit-checkbox", True)
+ b.wait_visible("#run-image-dialog-memory-limit-checkbox:checked")
+ b.wait_visible('#run-image-dialog-memory-limit input[value="512"]')
+ b.set_input_text("#run-image-dialog-memory-limit input[type=number]", "0.5")
+ b.set_val('#memory-unit-select', "GB")
+
+ # CPU shares work only with system containers
+ if auth:
+ # Check that the checkbox is enabled when clicked on the field
+ b.wait_visible("#run-image-dialog-cpu-priority-checkbox:not(:checked)")
+ b.click('#run-image-cpu-priority')
+ b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked")
+ b.set_checked("#run-image-dialog-cpu-priority-checkbox", False)
+
+ b.set_checked("#run-image-dialog-cpu-priority-checkbox", True)
+ b.wait_visible("#run-image-dialog-cpu-priority-checkbox:checked")
+ b.wait_visible('#run-image-dialog-cpu-priority input[value="1024"]')
+ b.set_input_text("#run-image-dialog-cpu-priority input[type=number]", "512")
+ else:
+ b.wait_not_present("#run-image-dialog-cpu-priority-checkbox")
+
+ # Enable tty
+ b.set_checked("#run-image-dialog-tty", True)
+
+ # Set up command line
+ b.set_input_text('#run-image-dialog-command',
+ "sh -c 'for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity'")
+
+ if auth:
+ # Set restart policy to 3 retries
+ b.set_val("#run-image-dialog-restart-policy", "on-failure")
+ b.set_input_text('#run-image-dialog-restart-retries input', '3')
+ else: # no lingering enabled so it's disabled
+ b.wait_not_present("#run-image-dialog-restart-policy")
+
+ # Switch to Integration tab
+ b.click("#pf-tab-1-create-image-dialog-tab-integration")
+
+ # Configure published ports
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#run-image-dialog-publish-0-host-port', '6000')
+ b.set_input_text('#run-image-dialog-publish-0-container-port', '5000')
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#run-image-dialog-publish-1-ip-address', '127.0.0.1')
+ b.set_input_text('#run-image-dialog-publish-1-host-port', '6001')
+ b.set_input_text('#run-image-dialog-publish-1-container-port', '5001')
+ b.set_val('#run-image-dialog-publish-1-protocol', "udp")
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#run-image-dialog-publish-2-ip-address', '7001')
+ b.set_input_text('#run-image-dialog-publish-2-host-port', '7001')
+ b.click('#run-image-dialog-publish-2-btn-close')
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#run-image-dialog-publish-3-container-port', '8001')
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#run-image-dialog-publish-4-ip-address', '127.0.0.2')
+ b.set_input_text('#run-image-dialog-publish-4-container-port', '9001')
+
+ # Configure env
+ b.click('.env-form .btn-add')
+ b.set_input_text('#run-image-dialog-env-0-key', 'APPLE')
+ b.set_input_text('#run-image-dialog-env-0-value', 'ORANGE')
+ b.click('.env-form .btn-add')
+ b.set_input_text('#run-image-dialog-env-1-key', 'PEAR')
+ b.set_input_text('#run-image-dialog-env-1-value', 'BANANA')
+ b.click('.env-form .btn-add')
+ b.set_input_text('#run-image-dialog-env-2-key', 'MELON')
+ b.set_input_text('#run-image-dialog-env-2-value', 'GRAPE')
+ b.click('#run-image-dialog-env-2-btn-close')
+ b.click('.env-form .btn-add')
+ # Test inputting an key=var entry
+ b.set_val('#run-image-dialog-env-3-value',
+ "RHUBARB=STRAWBERRY DURIAN=LEMON TEST_URL=wss://cockpit/?start=1&stop=0")
+ # set_val does not trigger onChange so append a space.
+ b.set_input_text('#run-image-dialog-env-3-value', ' ', append=True, value_check=False)
+
+ b.click('.env-form .btn-add')
+ b.set_input_text('#run-image-dialog-env-6-key', 'HOSTNAME')
+ b.set_input_text('#run-image-dialog-env-6-value', 'busybox')
+
+ # Test inputting a var with = in it doesn't reset key
+ b.click('.env-form .btn-add')
+ b.set_input_text('#run-image-dialog-env-7-key', 'TEST')
+ b.set_input_text('#run-image-dialog-env-7-value', 'REBASE=1')
+
+ # Configure volumes
+ b.click('.volume-form .btn-add')
+ rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2]
+ m.execute(f"chown admin:admin {rodir}")
+ m.execute(f"chown admin:admin {rwdir}")
+ b.set_checked("#run-image-dialog-volume-0-mode", False)
+
+ if self.has_selinux:
+ b.set_val('#run-image-dialog-volume-0-selinux', "z")
+ else:
+ b.wait_not_present('#run-image-dialog-volume-0-selinux')
+
+ b.set_file_autocomplete_val("#run-image-dialog-volume-0 .pf-v5-c-select", rodir)
+ b.key_press(["\r"])
+ b.set_input_text('#run-image-dialog-volume-0-container-path', '/tmp/ro')
+ ro_label = m.execute(f"ls -dZ {rodir}").split(" ")[0]
+ b.key_press(["\r"])
+ b.click('.volume-form .btn-add')
+ b.wait_visible('#run-image-dialog-volume-1')
+ b.click('#run-image-dialog-volume-1-btn-close')
+ b.wait_not_present('#run-image-dialog-volume-1')
+ b.click('.volume-form .btn-add')
+
+ if auth:
+ b.assert_pixels(".pf-v5-c-modal-box", "integration",
+ ignore=["#run-image-dialog-volume-0 .pf-v5-c-select__toggle-typeahead"],
+ skip_layouts=["rtl"])
+
+ if self.has_selinux:
+ b.set_val('#run-image-dialog-volume-2-selinux', "Z")
+ else:
+ b.wait_not_present('#run-image-dialog-volume-2-selinux')
+
+ b.set_file_autocomplete_val("#run-image-dialog-volume-2 .pf-v5-c-select", rwdir)
+ b.key_press(["\r"])
+ b.set_input_text('#run-image-dialog-volume-2-container-path', '/tmp/rw')
+ rw_label = m.execute(f"ls -dZ {rwdir}").split(" ")[0]
+
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ b.wait_not_present("div.pf-v5-c-modal-box")
+ self.waitContainerRow(IMG_BUSYBOX)
+ sha = self.execute(auth, "podman inspect --format '{{.Id}}' busybox-with-tty").strip()
+ self.waitContainer(sha, auth, name='busybox-with-tty', image=IMG_BUSYBOX,
+ cmd='sh -c "for i in $(seq 20); do sleep 1; echo $i; done; sleep infinity"',
+ state='Running', owner="system" if auth else "admin")
+ hasTTY = self.execute(auth, "podman inspect --format '{{.Config.Tty}}' busybox-with-tty").strip()
+ self.assertEqual(hasTTY, 'true')
+ # Only works with CGroupsV2
+ if auth or self.has_cgroupsV2:
+ memory = self.execute(auth, "podman inspect --format '{{.HostConfig.Memory}}' busybox-with-tty").strip()
+ self.assertEqual(memory, '500000000')
+
+ if auth:
+ cpuShares = self.execute(auth,
+ "podman inspect --format '{{.HostConfig.CpuShares}}' busybox-with-tty").strip()
+ self.assertEqual(cpuShares, '512')
+
+ restartPolicy = self.getRestartPolicy(auth, "busybox-with-tty")
+ if auth:
+ self.assertEqual(restartPolicy, '{on-failure 3}')
+ else:
+ # No restart policy
+ self.assertEqual(restartPolicy, '{ 0}')
+
+ b.wait(lambda: "3" in self.execute(auth, "podman logs busybox-with-tty"))
+
+ self.toggleExpandedContainer(IMG_BUSYBOX)
+
+ b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Created") + dd', 'today at')
+
+ b.click(".pf-m-expanded button:contains('Integration')")
+
+ b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
+ '0.0.0.0:6000 \u2192 5000/tcp')
+ b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
+ '127.0.0.1:6001 \u2192 5001/udp')
+ b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
+ '127.0.0.2:')
+ b.wait_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
+ ' \u2192 8001/tcp')
+ b.wait_not_in_text(f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Ports") + dd',
+ '7001/tcp')
+
+ ports = self.execute(auth, "podman inspect --format '{{.NetworkSettings.Ports}}' busybox-with-tty")
+ self.assertRegex(ports, r'5000/tcp:\[{(0.0.0.0)? 6000}\]')
+ self.assertIn('5001/udp:[{127.0.0.1 6001}]', ports)
+ self.assertIn('8001/tcp:[{', ports)
+ self.assertIn('9001/tcp:[{127.0.0.2 ', ports)
+ self.assertNotIn('7001/tcp', ports)
+
+ env_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Environment variables") + dd'
+ b.wait_in_text(env_select, 'APPLE=ORANGE')
+ b.wait_in_text(env_select, 'PEAR=BANANA')
+ b.wait_in_text(env_select, 'RHUBARB=STRAWBERRY')
+ b.wait_in_text(env_select, 'DURIAN=LEMON')
+ b.wait_in_text(env_select, 'TEST_URL=wss://cockpit/?start=1&stop=0')
+ b.wait_in_text(env_select, 'HOSTNAME=busybox')
+ b.wait_in_text(env_select, 'TEST=REBASE=1')
+ # variables are present in env but are not displayed in the UI
+ b.wait_not_in_text(env_select, 'container=podman')
+ b.wait_not_in_text(env_select, 'TERM=xterm')
+ b.wait_not_in_text(env_select, 'HOME=/root')
+ b.wait_not_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')
+
+ b.click(".container-integration button:contains('Show more')")
+ # previously hidden variables are now visible
+ b.wait_in_text(env_select, 'container=podman')
+ b.wait_in_text(env_select, 'TERM=xterm')
+ b.wait_in_text(env_select, 'HOME=/root')
+ b.wait_in_text(env_select, 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin')
+
+ env = self.execute(auth, "podman exec busybox-with-tty env")
+ self.assertIn('APPLE=ORANGE', env)
+ self.assertIn('PEAR=BANANA', env)
+ self.assertIn('RHUBARB=STRAWBERRY', env)
+ self.assertIn('DURIAN=LEMON', env)
+ self.assertIn('HOSTNAME=busybox', env)
+ self.assertIn('TEST=REBASE=1', env)
+ self.assertIn('container=podman', env)
+ self.assertIn('TERM=xterm', env)
+ self.assertIn('HOME=/root', env)
+ self.assertIn('PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin', env)
+ self.assertNotIn('MELON=GRAPE', env)
+
+ vol_select = f'#containers-containers tr:contains("{IMG_BUSYBOX}") dt:contains("Volumes") + dd'
+ b.wait_in_text(vol_select, f"{rodir} \u2192 /tmp/ro")
+ b.wait_in_text(vol_select, f"{rwdir} \u2194 /tmp/rw")
+
+ romnt = self.execute(auth, "podman exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/ro")
+ self.assertIn('ro', romnt)
+ self.assertIn(rodir[4:], romnt)
+ rwmnt = self.execute(auth, "podman exec busybox-with-tty cat /proc/self/mountinfo | grep /tmp/rw")
+ self.assertIn('rw', rwmnt)
+ self.assertIn(rwdir[4:], rwmnt)
+
+ if self.has_selinux:
+ # rw was set to :Z so it should change, but not be shared
+ rw_label_new = m.execute(f"ls -dZ {rwdir}").split(" ")[0]
+ self.assertNotEqual(rw_label, rw_label_new)
+ self.assertRegex(rw_label_new, r"container_file_t:s0:c\d*,c\d*$")
+
+ # ro was set to :z to it should change and be shared
+ ro_label_new = m.execute(f"ls -dZ {rodir}").split(" ")[0]
+ self.assertNotEqual(ro_label, ro_label_new)
+ self.assertRegex(ro_label_new, "container_file_t:s0$")
+
+ def get_int(n):
+ try:
+ return int(n)
+ except ValueError:
+ return 0
+
+ b.wait_not_present("button:contains('Health check logs')")
+ b.click(".pf-m-expanded button:contains('Logs')")
+ b.wait_text(".pf-m-expanded .container-logs .xterm-accessibility-tree > div:nth-child(1)", "1")
+
+ # firefox optimizes these out when not visible
+ b.eval_js("""
+ document.querySelector('.pf-m-expanded .container-logs .xterm-accessibility-tree').scrollIntoView()
+ """)
+ b.wait_in_text(".pf-m-expanded .container-logs .xterm-accessibility-tree", "6")
+
+ b.click(".pf-m-expanded button:contains('Console')")
+ b.wait(lambda:
+ get_int(b.text(".pf-m-expanded .container-terminal .xterm-accessibility-tree > div:nth-child(3)")) > 7)
+
+ # Create another instance without port publishing
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ self.toggleExpandedContainer(IMG_BUSYBOX)
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
+ b.set_input_text("#run-image-dialog-name", "busybox-without-publish")
+
+ # Set up command line
+ b.set_input_text('#run-image-dialog-command',
+ "sh -c 'for i in $(seq 20); do echo $i; sleep 3; done; sleep infinity'")
+
+ # Run without tty, console should be able to `exec`
+ b.set_checked("#run-image-dialog-tty", False)
+
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ b.wait_not_present("div.pf-v5-c-modal-box")
+
+ self.waitContainerRow("busybox-without-publish")
+ self.toggleExpandedContainer("busybox-without-publish")
+ b.wait_not_present("""
+ #containers-containers tbody tr:contains("busybox-without-publish") + tr dt:contains("Ports")
+ """)
+
+ # Rootless only works with CGroupsV2
+ if auth or self.has_cgroupsV2:
+ cpuShares = self.execute(auth, """
+ podman inspect --format '{{.HostConfig.CpuShares}}' busybox-without-publish
+ """).strip()
+ # podman ≥ 1.8 translates 0 default into actual value
+ self.assertIn(cpuShares, ['0', '1024'])
+
+ b.set_val("#containers-containers-filter", "all")
+
+ b.click(".pf-m-expanded button:contains('Console')")
+ b.wait_in_text(".pf-m-expanded .xterm-accessibility-tree", "/ # ")
+ b.focus(".pf-m-expanded .xterm-helper-textarea")
+ b.key_press('clear\r')
+ b.wait_not_in_text(".pf-m-expanded .xterm-accessibility-tree", "clear")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # ")
+ b.key_press('echo hello\r')
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(2)", "hello")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(3)", "/ # ")
+ b.wait_text(".pf-m-expanded .xterm-accessibility-tree > div:nth-child(1)", "/ # echo hello")
+
+ b.go("#/?name=tty")
+ self.check_containers(["busybox-with-tty"], ["busybox-without-publish"])
+ b.go("#/?name=busy")
+ self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])
+
+ b.set_input_text('#containers-filter', 'tty')
+ self.check_containers(["busybox-with-tty"], ["busybox-without-publish"])
+ self.check_images([], [IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY])
+ b.set_input_text('#containers-filter', 'busy')
+ b.wait_js_cond('window.location.hash === "#/?name=busy"')
+ self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])
+ self.check_images([IMG_BUSYBOX], [IMG_ALPINE, IMG_REGISTRY])
+ b.set_input_text('#containers-filter', 'alpine')
+ b.wait_js_cond('window.location.hash === "#/?name=alpine"')
+ self.check_containers([], ["busybox-with-tty", "busybox-without-publish"])
+ self.check_images([IMG_ALPINE], [IMG_BUSYBOX, IMG_REGISTRY])
+ b.set_input_text('#containers-filter', '')
+ self.check_containers(["busybox-with-tty", "busybox-without-publish"], [])
+ self.check_images([IMG_ALPINE, IMG_BUSYBOX, IMG_REGISTRY], [])
+ b.wait_js_cond('window.location.hash === "#/"')
+
+ self.filter_containers("running")
+ id_with_tty = self.execute(auth, "podman inspect --format '{{.Id}}' busybox-with-tty").strip()
+
+ container_sel = f'#containers-images tbody tr:contains("{IMG_BUSYBOX}")'
+ b.click(f'{container_sel} td.pf-v5-c-table__toggle button')
+ # running container, just selects it, but leaves "Only running" alone
+ b.click(f"{container_sel} + tr div.ct-listing-panel-body dt:contains('Used by') + dd button:contains('busybox-with-tty')") # noqa: E501
+ b.wait_js_cond('window.location.hash === "#' + id_with_tty + '"')
+ b.wait_val("#containers-containers-filter", "running")
+ # FIXME: expanding running container details does not actually work right now
+ # b.wait_in_text("#containers-containers tr.pf-m-expanded .container-details", "sleep infinity")
+ # stopped container, switches to showing all containers
+
+ # Create a container without starting it
+ self.filter_containers("all")
+ container_name = "busybox-not-started"
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ b.wait_val("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
+ b.set_input_text("#run-image-dialog-name", container_name)
+ b.set_input_text("#run-image-dialog-command", "sh -c sleep infinity")
+
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-btn')
+ b.wait_not_present("div.pf-v5-c-modal-box")
+
+ sha = self.execute(auth, "podman inspect --format '{{.Id}}' " + container_name).strip()
+ self.waitContainer(sha, auth, name=container_name, image=IMG_BUSYBOX, state=['Configured', 'Created'])
+
+ self.filter_containers("running")
+ b.wait_not_in_text("#containers-containers", "busybox-not-started")
+ container_sel = f"#containers-images tbody tr:contains('{IMG_BUSYBOX}') + tr div.ct-listing-panel-body"
+ b.click(f"{container_sel} dt:contains('Used by') + dd button:contains('busybox-not-started')")
+ b.wait_js_cond(f"window.location.hash === '#{sha}'")
+ b.wait_val("#containers-containers-filter", "all")
+ b.wait_in_text("#containers-containers", "busybox-not-started")
+ # auto-expands container details
+ b.wait_in_text("#containers-containers tbody tr:contains('busybox-not-started') + tr", "sleep infinity")
+
+ b.click(f'#containers-images tbody tr:contains("{IMG_ALPINE}") td.pf-v5-c-table__toggle button')
+ b.wait_in_text(f"#containers-images tbody tr:contains('{IMG_ALPINE}') td[data-label='Used by']", 'unused')
+
+ b.set_input_text('#containers-filter', 'foobar')
+ b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers that match the current filter')
+ b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images that match the current filter')
+ b.set_input_text('#containers-filter', '')
+
+ if not auth or not self.machine.ostree_image: # don't kill ws container
+ # Ubuntu 22.04 has old podman that does not know about --time
+ if m.image != 'ubuntu-2204':
+ # Remove all containers first as it is not possible to set --time 0 to rmi command
+ self.execute(auth, "podman rm --all --force --time 0")
+ self.execute(auth, "podman rmi -af")
+ b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No containers')
+ b.set_val("#containers-containers-filter", "running")
+ b.wait_in_text('#containers-containers .pf-v5-c-empty-state', 'No running containers')
+ b.wait_in_text('#containers-images .pf-v5-c-empty-state', 'No images')
+
+ def check_content(self, kind, present, not_present):
+ b = self.browser
+ for item in present:
+ b.wait_visible(f'#containers-{kind} tbody tr:first-child:contains({item})')
+ for item in not_present:
+ b.wait_not_present(f'#containers-{kind} tbody tr:first-child:contains({item})')
+
+ def check_containers(self, present, not_present):
+ self.check_content("containers", present, not_present)
+
+ def check_images(self, present, not_present):
+ self.check_content("images", present, not_present)
+
+ def waitContainer(self, row_id, auth, name="", image="", cmd="", owner="", state=None, pod="no-pod"):
+ """Check the container with row_name has the expected values
+ "image" can be substring, "state" might be string or array of possible states, other are
+ checked for exact match.
+ """
+ sel = "#containers-containers #table-" + pod + f" tbody tr[data-row-id=\"{row_id}{auth}\"]".lower()
+ b = self.browser
+ if name:
+ b.wait_text(sel + " .container-name", name)
+ if image:
+ b.wait_in_text(sel + " .container-block small:nth-child(2)", image)
+ if cmd:
+ b.wait_text(sel + " .container-block small:last-child", cmd)
+ if owner:
+ if owner == "system":
+ b.wait_text(sel + " td[data-label=Owner]", owner)
+ else:
+ b.wait_text(sel + " td[data-label=Owner]", "user: " + owner)
+ if state is not None:
+ if not isinstance(state, list):
+ state = [state]
+ b.wait(lambda: b.text(sel + " td[data-label=State]") in state)
+
+ def filter_containers(self, value):
+ """Use dropdown menu in the header to filter containers"""
+ b = self.browser
+ b.set_val("#containers-containers-filter", value)
+
+ def confirm_modal(self, text):
+ """Wait for the pop up window and click the button with text"""
+ b = self.browser
+ b.click(f".pf-v5-c-modal-box footer button:contains({text})")
+ b.wait_not_present(f".pf-v5-c-modal-box footer button:contains({text})")
+
+ def testPruneUnusedImagesSystem(self):
+ self._testPruneUnusedImagesSystem(True)
+
+ def testPruneUnusedImagesUser(self):
+ self._testPruneUnusedImagesSystem(False)
+
+ @testlib.skipOstree("no root login available on ostree")
+ def testPruneUnusedImagesRoot(self):
+ self._testPruneUnusedImagesSystem(False, True)
+
+ def _testPruneUnusedImagesSystem(self, auth, root=False):
+ b = self.browser
+ if root:
+ self.login_and_go("/podman", user="root", enable_root_login=True)
+ b.wait_visible("#app")
+ else:
+ self.login(auth)
+
+ leftover_images = 1
+ # cockpit-ws image
+ if self.machine.ostree_image and auth:
+ leftover_images += 1
+
+ # By default we have 3 unused images, start one.
+ self.execute(auth or root, f"podman run -d --name used_image --stop-timeout 0 {IMG_ALPINE} sh")
+ b.click("#image-actions-dropdown")
+ b.click("#prune-unused-images-button")
+
+ if auth:
+ b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
+ (self.user_images_count + self.system_images_count) - leftover_images)
+ elif root:
+ b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
+ self.system_images_count - leftover_images)
+ else:
+ b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
+ self.user_images_count - leftover_images)
+ b.click(".pf-v5-c-modal-box button:contains(Prune)")
+
+ # When being superuser, admin images are also removed
+ if auth:
+ self.waitNumImages(leftover_images)
+ checkImage(b, IMG_ALPINE, "system")
+ else:
+ self.waitNumImages(leftover_images)
+ # Two images removed, one in use kept
+ b.wait_not_present(f"#containers-images:contains('{IMG_BUSYBOX}')")
+ b.wait_not_present(f"#containers-images:contains('{IMG_REGISTRY}')")
+ b.wait_visible(f"#containers-images:contains('{IMG_ALPINE}')")
+
+ # Prune button should now be disabled
+ b.click("#image-actions-dropdown")
+ b.wait_visible(".pf-m-disabled.pf-v5-c-menu__list-item:contains(Prune unused images)")
+
+ def testPruneUnusedImagesSystemSelections(self):
+ """ Test the prune unused images selection options"""
+ b = self.browser
+ self.login(True)
+
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Prune unused images)")
+
+ # Deselect both
+ b.click("#deleteSystemImages")
+ b.click("#deleteUserImages")
+ b.wait_visible(".pf-v5-c-modal-box button:contains(Prune):disabled")
+
+ # Admin / user images are selected
+ expected_images = self.user_images_count + self.system_images_count
+ if self.machine.ostree_image:
+ expected_images -= 1
+ b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li", expected_images)
+ # Select user images
+ b.click("#deleteUserImages")
+ b.click(".pf-v5-c-modal-box button:contains(Prune)")
+
+ # System images are left over
+ self.waitNumImages(self.system_images_count)
+ checkImage(b, IMG_ALPINE, "system")
+ checkImage(b, IMG_BUSYBOX, "system")
+ checkImage(b, IMG_REGISTRY, "system")
+
+ # Pruning again, should delete all system images
+ b.click("#image-actions-dropdown")
+ b.click("button:contains(Prune unused images)")
+ b.wait_js_func("ph_count_check", ".pf-v5-c-modal-box__body .pf-v5-c-list li",
+ self.system_images_count - 1 if self.machine.ostree_image else self.system_images_count)
+ b.click(".pf-v5-c-modal-box button:contains(Prune)")
+ self.waitNumImages(1 if self.machine.ostree_image else 0)
+
+ # Prune button should now be disabled
+ b.click("#image-actions-dropdown")
+ b.wait_visible(".pf-v5-c-menu__list-item.pf-m-disabled:contains(Prune unused images)")
+
+ def testPruneUnusedContainersSystem(self):
+ self._testPruneUnusedContainersSystem(True)
+
+ def testPruneUnusedContainersUser(self):
+ self._testPruneUnusedContainersSystem(False)
+
+ def _testPruneUnusedContainersSystem(self, auth):
+ """Test the prune unused container image dialog"""
+
+ b = self.browser
+ self.login(auth)
+
+ # Create running and non-running containers
+ self.execute(auth, "podman pod create --name pod")
+ notrunninginpodId = self.execute(auth, f"""
+ podman run --name inpod --pod pod -tid {IMG_BUSYBOX} sh -c 'exit 1'""").strip()
+ runninginpodId = self.execute(auth, f"""
+ podman run --name inpodrunning --pod pod -tid {IMG_BUSYBOX} sh -c 'sleep infinity'""").strip()
+
+ self.execute(auth, f"podman run --name notrunning -tid {IMG_BUSYBOX} sh -c 'exit 1'")
+ self.execute(auth, f"podman run --name containerrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'")
+
+ # Create containers for the opposite of what we are, admin or super admin
+ if auth:
+ self.execute(False, f"podman run --name adminnotrunning -tid {IMG_BUSYBOX} sh 'exit 1'")
+ b.wait(lambda: self.getContainerAttr("adminnotrunning", "State") in NOT_RUNNING)
+ self.execute(False, f"podman run --name adminrunning -tid {IMG_BUSYBOX} sh -c 'sleep infinity'")
+ b.wait(lambda: self.getContainerAttr("adminrunning", "State") == "Running")
+
+ b.click("#containers-actions-dropdown")
+ b.click("button:contains(Prune unused containers)")
+
+ if auth:
+ b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(1) td[data-label=Name]", "adminnotrunning")
+ b.wait_in_text(".pf-v5-c-modal-box__body tbody:nth-of-type(2) td[data-label=Name]", "notrunning")
+ else:
+ b.wait_in_text(".pf-v5-c-modal-box__body tbody td[data-label=Name]", "notrunning")
+
+ b.click(".pf-v5-c-modal-box button:contains(Prune)")
+ b.wait_not_present(".pf-v5-c-modal-box__body")
+
+ if auth:
+ self.waitContainerRow("notrunning", False)
+ self.waitContainerRow("adminnotrunning", False)
+ else:
+ self.waitContainerRow("notrunning", False)
+
+ # Verify running containers still exists
+ self.waitContainerRow("containerrunning")
+ pods = [{"name": "inpod", "state": "Exited", "id": notrunninginpodId,
+ "image": IMG_BUSYBOX, "command": 'sh -c "exit 1"'},
+ {"name": "inpodrunning", "state": "Running", "id": runninginpodId,
+ "image": IMG_BUSYBOX, "command": 'sh -c "sleep infinity"'}]
+ self.waitPodContainer("pod", pods, auth)
+
+ def testCreateContainerValidation(self):
+ def validateField(groupSelector, value, errorMessage, resetValue=""):
+ b.set_input_text(f"{groupSelector} input", value)
+ b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)")
+ b.wait_in_text(f"{groupSelector} .pf-v5-c-helper-text__item-text", errorMessage)
+ b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn[aria-disabled=true]")
+ # Reset to acceptable value and verify the validation message is not present
+ b.set_input_text(f"{groupSelector} input", resetValue)
+ b.wait_not_present(f"{groupSelector} .pf-v5-c-helper-text__item-text")
+ b.wait_visible(".pf-v5-c-modal-box__footer #create-image-create-run-btn:not(:disabled)")
+
+ # Test the validation errors
+
+ # complaint about port conflict
+ self.allow_browser_errors("error: Container failed to be started:.*")
+ self.allow_browser_errors("No routable interface.*")
+ self.allow_browser_errors(".*ddress already in use.*5000.*")
+ b = self.browser
+ self.login(False)
+ container_name = 'portused'
+
+ # Start a podman container which uses a port
+ self.execute(False, f"podman run -d -p 5000:5000 --name registry --stop-timeout 0 {IMG_REGISTRY}")
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ validateField("#image-name-group", "registry", "Name already in use")
+
+ # Switch to Integration tab
+ b.click("#pf-tab-1-create-image-dialog-tab-integration")
+
+ # Test validation of port mapping
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text("#run-image-dialog-publish-0-container-port-group input", "1")
+ validateField("#run-image-dialog-publish-0-ip-address-group", "abcd", "valid IP address")
+ validateField("#run-image-dialog-publish-0-host-port-group", "-1", "1 to 65535")
+ validateField("#run-image-dialog-publish-0-host-port-group", "99999", "1 to 65535")
+ validateField("#run-image-dialog-publish-0-container-port-group", "-1", "1 to 65535", resetValue="1")
+ validateField("#run-image-dialog-publish-0-container-port-group", "", "must not be empty", resetValue="1")
+ validateField("#run-image-dialog-publish-0-container-port-group", "99999", "1 to 65535", resetValue="1")
+
+ # Test validation of volumes
+ b.click('.volume-form .btn-add')
+ b.set_input_text("#run-image-dialog-volume-0-container-path-group input", "/somepath")
+ validateField("#run-image-dialog-volume-0-container-path-group", "", "not be empty", resetValue="/somepath")
+
+ # Test validation of environment variables
+ b.click('.env-form .btn-add')
+ b.set_input_text("#run-image-dialog-env-0-key-group input", "sometext")
+ validateField("#run-image-dialog-env-0-key-group", "", "must not be empty", resetValue="sometext")
+
+ b.set_input_text("#run-image-dialog-name", container_name)
+
+ # Port address is already in use
+ b.set_input_text('#run-image-dialog-publish-0-host-port', '5000')
+ b.set_input_text('#run-image-dialog-publish-0-container-port', '5000')
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ # Can be "[aA]ddress"
+ b.wait_in_text(".pf-v5-c-alert", "ddress already in use")
+
+ # Changing the port should allow creation of container
+ b.set_input_text('#run-image-dialog-publish-0-host-port', '5001')
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ self.waitContainerRow(container_name)
+
+ def _testHealthcheck(self, auth):
+ b = self.browser
+
+ # Just drop user images so we can use simpler selectors
+ if auth:
+ self.execute(False, f"podman rmi {IMG_BUSYBOX}")
+
+ self.login(auth)
+
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ b.set_input_text("#run-image-dialog-name", "healthy")
+
+ b.click("#pf-tab-2-create-image-dialog-tab-healthcheck")
+ b.set_input_text('#run-image-dialog-healthcheck-command', 'true')
+ b.set_input_text('#run-image-healthcheck-interval input', '325')
+ b.set_input_text('#run-image-healthcheck-timeout input', '35')
+ b.set_input_text('#run-image-healthcheck-start-period input', '5')
+ b.click('#run-image-healthcheck-retries .pf-v5-c-input-group__item:nth-child(1) button')
+ b.wait_val("#run-image-healthcheck-retries input", 2)
+ if auth:
+ b.assert_pixels('.pf-v5-c-modal-box', "healthcheck-modal", skip_layouts=["rtl"])
+ # Test that the healthcheck option is not available before podman 4.3
+ if podman_version(self) < (4, 3, 0):
+ b.wait_not_present("#run-image-healthcheck-action")
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+
+ self.waitContainerRow("healthy")
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ healthy_sha = self.execute(auth, "podman inspect --format '{{.Id}}' healthy").strip()
+ self.waitContainer(healthy_sha, auth, state='RunningHealthy')
+
+ self.toggleExpandedContainer("healthy")
+ b.click(".pf-m-expanded button:contains('Health check')")
+
+ b.wait_in_text('#container-details-healthcheck dt:contains("Command") + dd', 'true')
+ b.wait_in_text('#container-details-healthcheck dt:contains("Interval") + dd', '325 seconds')
+ b.wait_in_text('#container-details-healthcheck dt:contains("Retries") + dd', '2')
+ b.wait_in_text('#container-details-healthcheck dt:contains("Timeout") + dd', '35 seconds')
+ b.wait_in_text('#container-details-healthcheck dt:contains("Start period") + dd', '5 seconds')
+ b.wait_not_present('#container-details-healthcheck dt:contains("Failing streak")')
+ if podman_version(self) >= (4, 3, 0):
+ b.wait_in_text('#container-details-healthcheck dt:contains("When unhealthy") + dd', 'No action')
+
+ self.assertEqual(self.execute(auth, "podman inspect --format '{{.Config.Healthcheck}}' healthy").strip(),
+ "{[true] 5s 5m25s 35s 2}")
+
+ # single successful health check
+ b.wait_in_text(".ct-listing-panel-body tbody tr", "Passed health run")
+ b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(1) svg.green")
+ b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(2)")
+
+ # Trigger run manually, adds one more healthy run
+ self.performContainerAction("healthy", "Run health check")
+ b.wait_visible(".ct-listing-panel-body tbody:nth-of-type(2) svg.green")
+ b.wait_not_present(".ct-listing-panel-body tbody:nth-of-type(3)")
+
+ self.toggleExpandedContainer("healthy")
+
+ self.execute(auth, f"podman run --name sick -dt --health-cmd false --health-interval 5s {IMG_BUSYBOX}")
+ self.waitContainerRow("sick")
+ unhealthy_sha = self.execute(auth, "podman inspect --format '{{.Id}}' sick").strip()
+ self.waitContainer(unhealthy_sha, auth, state='RunningUnhealthy')
+ # Unhealthy should be first
+ expected_ws = ""
+ if auth and self.machine.ostree_image:
+ expected_ws = "ws"
+ b.wait_collected_text("#containers-containers .container-name", "healthysick" + expected_ws)
+
+ self.toggleExpandedContainer("sick")
+ b.click(".pf-m-expanded button:contains('Health check')")
+ b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(1)")
+ b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(4)")
+ b.wait_visible(".pf-m-expanded .ct-listing-panel-body tbody:nth-of-type(2) svg.red")
+ b.wait_visible('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak")')
+ failures = int(b.text('.pf-m-expanded #container-details-healthcheck dt:contains("Failing streak") + dd'))
+ self.assertGreater(failures, 3)
+ if auth:
+ b.wait_js_func("ph_count_check", ".pf-m-expanded table[aria-label=Logs] tbody tr", 5)
+ b.assert_pixels(".pf-m-expanded .pf-v5-c-table__expandable-row-content",
+ "healthcheck-details",
+ ignore=["thead", "#container-details-healthcheck dt:contains('Failing streak') + dd",
+ "td[data-label='Started at']"],
+ skip_layouts=["rtl"])
+
+ self.toggleExpandedContainer("sick")
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ b.wait_visible('#containers-images td[data-label="Image"]:contains("busybox:latest")')
+ b.click('#containers-images tbody tr:contains("busybox:latest") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ # Test the health check action, only supported in podman 4.3 and later.
+ # To test this we make a healthcheck which depends on a file, so when starting the
+ # container is healthy, after we remove the file the healthcheck should fail and our
+ # configured action should executed.
+ if podman_version(self) < (4, 3, 0):
+ return
+
+ containername = "healthaction"
+ b.set_input_text("#run-image-dialog-name", containername)
+ b.set_input_text("#run-image-dialog-command", "/bin/sh -c 'echo 1 > /healthy && sleep infinity'")
+
+ b.click("#pf-tab-2-create-image-dialog-tab-healthcheck")
+ b.set_input_text('#run-image-dialog-healthcheck-command', '/bin/test -f /healthy')
+ b.set_input_text('#run-image-healthcheck-interval input', '1')
+ b.set_input_text('#run-image-healthcheck-timeout input', '1')
+ b.click('#run-image-healthcheck-action-2')
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+
+ self.waitContainerRow(containername)
+ self.toggleExpandedContainer(containername)
+ b.wait(lambda: self.getContainerAttr(containername, "State") == "RunningHealthy")
+ b.click(".pf-m-expanded button:contains('Health check')")
+ b.wait_in_text('.pf-m-expanded #container-details-healthcheck dt:contains("When unhealthy") + dd',
+ 'Force stop')
+ # Removing the file should kill the container
+ status = self.execute(auth, f"podman exec {containername} rm -f /healthy").strip()
+ b.wait(lambda: self.getContainerAttr(containername,
+ "State", "span:not(.ct-badge-container-unhealthy)") in NOT_RUNNING)
+ status = self.execute(auth, f"podman inspect --format '{{{{.State.Health.Status}}}}' {containername}").strip()
+ self.assertEqual(status, "unhealthy")
+
+ def testHealthcheckSystem(self):
+ self._testHealthcheck(True)
+
+ def testHealthcheckUser(self):
+ self._testHealthcheck(False)
+
+ # Ubuntu 2204 lacks user systemd units
+ # https://github.com/containers/podman/commit/9312d458b4254b48e331d1ae40cb2f6d0fec9bd0
+ @testlib.skipImage("podman-restart not available for user", "ubuntu-2204")
+ def testPodmanRestartEnabledUser(self):
+ self._testPodmanRestartEnabled(False)
+
+ def testPodmanRestartEnabledSystem(self):
+ self._testPodmanRestartEnabled(True)
+
+ def _testPodmanRestartEnabled(self, auth):
+ b = self.browser
+ if auth:
+ self.addCleanup(self.machine.execute, "systemctl disable podman-restart.service")
+ else:
+ self.addCleanup(self.machine.execute, "systemctl --user disable podman-restart.service")
+ self.machine.execute("loginctl enable-linger $(id -u admin)")
+ # HACK: verify that the file we watch exists
+ self.assertTrue(self.machine.execute("""
+ if test -e /var/lib/systemd/linger/admin; then echo yes; fi
+ """).strip() != "")
+ self.addCleanup(self.machine.execute, "loginctl disable-linger $(id -u admin)")
+
+ # Drop user images for easy selection
+ if auth:
+ self.execute(False, f"podman rmi {IMG_BUSYBOX}")
+
+ self.login(auth)
+ b.click("#containers-images button.pf-v5-c-expandable-section__toggle")
+
+ def create_container(name, policy=None):
+ b.wait_visible(f'#containers-images td[data-label="Image"]:contains("{IMG_BUSYBOX}")')
+ b.click(f'#containers-images tbody tr:contains("{IMG_BUSYBOX}") .ct-container-create')
+ b.wait_visible('div.pf-v5-c-modal-box header:contains("Create container")')
+
+ b.set_input_text("#run-image-dialog-name", name)
+ if policy:
+ b.set_val("#run-image-dialog-restart-policy", "always")
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ self.waitContainerRow(name)
+
+ container_name = 'none'
+ create_container(container_name)
+ self.assertEqual(self.getRestartPolicy(auth, container_name), '{ 0}')
+
+ container_name = 'restart'
+ create_container(container_name, 'always')
+ self.assertEqual(self.getRestartPolicy(auth, container_name), '{always 0}')
+ if auth:
+ podmanRestartEnabled = self.execute(True, "systemctl is-enabled podman-restart.service || true").strip()
+ else:
+ podmanRestartEnabled = self.execute(False,
+ "systemctl --user is-enabled podman-restart.service || true").strip()
+ self.assertEqual(podmanRestartEnabled, 'enabled')
+
+ def _testCreateContainerInPod(self, auth):
+ b = self.browser
+
+ container_name = 'containerinpod'
+ podname = "pod1"
+ self.execute(auth, f"podman pod create --infra=false --name={podname}")
+
+ self.login(auth)
+
+ self.filter_containers('all')
+ b.click(".create-container-in-pod")
+
+ # the podname should be in the "Create container" header
+ self.assertIn(podname, b.text("#pf-modal-part-1"))
+
+ b.set_input_text("#run-image-dialog-name", container_name)
+ b.set_input_text("#create-image-image-select-typeahead", IMG_BUSYBOX_LATEST)
+ b.click('button.pf-v5-c-toggle-group__button:contains("Local")')
+ b.click(f'button.pf-v5-c-select__menu-item:contains("{IMG_BUSYBOX_LATEST}")')
+ b.click('.pf-v5-c-modal-box__footer #create-image-create-run-btn')
+ b.wait_not_present("#run-image-dialog-name")
+
+ container_sha = self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name}").strip()
+ self.waitContainer(container_sha, auth, name=container_name, image=IMG_BUSYBOX, cmd='sh',
+ state='Running', owner="system" if auth else "admin", pod=podname)
+
+ # Check that we correctly preselect owner
+ if auth:
+ self.execute(False, "podman pod create --infra=false --name=system_pod")
+ b.click("#table-system_pod .create-container-in-pod")
+ b.wait_visible("#run-image-dialog-owner-user:checked")
+ b.wait_visible("#run-image-dialog-owner-user:disabled")
+ b.wait_visible("#run-image-dialog-owner-system:disabled")
+
+ def testCreateContainerInPodSystem(self):
+ self._testCreateContainerInPod(True)
+
+ def testCreateContainerInPodUser(self):
+ self._testCreateContainerInPod(False)
+
+ def testPauseResumeContainerSystem(self):
+ self._testPauseResumeContainer(True)
+
+ def testPauseResumeContainerUser(self):
+ # rootless cgroupv1 containers do not support pausing
+ if not self.has_cgroupsV2:
+ return
+ self._testPauseResumeContainer(False)
+
+ def _testPauseResumeContainer(self, auth):
+ b = self.browser
+ container_name = "pauseresume"
+
+ self.execute(auth, f"podman run -dt --name {container_name} --stop-timeout 0 {IMG_ALPINE}")
+ self.login(auth)
+
+ self.waitContainerRow(container_name)
+ self.toggleExpandedContainer(container_name)
+ b.wait_not_present(self.getContainerAction(container_name, 'Resume'))
+ self.performContainerAction(container_name, "Pause")
+
+ # show all containers and check status
+ self.filter_containers('all')
+
+ # Check that container details are not lost when the container is paused
+ b.click(".pf-m-expanded button:contains('Integration')")
+ b.wait_visible(f'#containers-containers tr:contains("{IMG_ALPINE}") dt:contains("Environment variables")')
+
+ b.wait(lambda: self.getContainerAttr(container_name, "State") == "Paused")
+ b.wait_not_present(self.getContainerAction(container_name, 'Pause'))
+ self.performContainerAction(container_name, "Resume")
+ b.wait(lambda: self.getContainerAttr(container_name, "State") == "Running")
+
+ def testRenameContainerSystem(self):
+ self._testRenameContainer(True)
+
+ def testRenameContainerUser(self):
+ self._testRenameContainer(False)
+
+ def _testRenameContainer(self, auth):
+ b = self.browser
+ container_name = "rename"
+ container_name_new = "rename-new"
+
+ self.execute(auth, f"podman container create -t --name {container_name} {IMG_BUSYBOX}")
+ self.login(auth)
+
+ self.filter_containers('all')
+
+ self.waitContainerRow(container_name)
+ self.toggleExpandedContainer(container_name)
+ self.performContainerAction(container_name, "Rename")
+
+ # the container name should be in the "Rename container" header
+ b.wait_in_text("#pf-modal-part-1", container_name)
+ b.set_input_text("#rename-dialog-container-name", "")
+ b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required")
+ b.set_input_text("#rename-dialog-container-name", "banana???")
+ b.wait_in_text("#commit-dialog-image-name-helper", "Name can only contain letters, numbers")
+
+ b.set_input_text("#rename-dialog-container-name", container_name_new)
+ b.click('#btn-rename-dialog-container')
+ b.wait_not_present("#rename-dialog-container-name")
+
+ self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name_new}").strip()
+ self.waitContainerRow(container_name_new)
+
+ # rename using the enter key
+ self.toggleExpandedContainer(container_name_new)
+ self.performContainerAction(container_name_new, "Rename")
+
+ container_name_new = "rename-new-enter"
+ b.set_input_text("#rename-dialog-container-name", "")
+ b.focus("#rename-dialog-container-name")
+ b.key_press("\r") # Simulate enter key
+ b.wait_in_text("#commit-dialog-image-name-helper", "Container name is required")
+ b.set_input_text("#rename-dialog-container-name", container_name_new)
+ b.focus("#rename-dialog-container-name")
+ b.key_press("\r") # Simulate enter key
+ b.wait_not_present("#rename-dialog-container-name")
+
+ self.execute(auth, f"podman inspect --format '{{{{.Id}}}}' {container_name_new}").strip()
+ self.waitContainerRow(container_name_new)
+
+ def testMultipleContainers(self):
+ self.login()
+
+ # Create 31 containers
+ for i in range(31):
+ self.execute(True, f"podman run -dt --name container{i} --stop-timeout 0 {IMG_BUSYBOX}")
+
+ self.waitContainerRow("container30")
+
+ # Generic cleanup takes too long and timeouts, so remove these container manually one by one
+ for i in range(31):
+ self.execute(True, f"podman rm -f container{i}")
+
+ def testSpecialContainers(self):
+ m = self.machine
+ b = self.browser
+
+ toolbox_label = "com.github.containers.toolbox=true"
+ distrobox_label = "manager=distrobox"
+
+ container_1_id = m.execute(f"podman run -d --name container_1 -l {toolbox_label} {IMG_BUSYBOX}").strip()
+ container_2_id = m.execute(f"podman run -d --name container_2 -l {distrobox_label} {IMG_BUSYBOX}").strip()
+
+ self.login()
+
+ self.waitContainerRow('container_1')
+ self.waitContainerRow('container_2')
+
+ container_1_sel = f"#containers-containers tbody tr[data-row-id=\"{container_1_id}{'true'}\"]"
+ container_2_sel = f"#containers-containers tbody tr[data-row-id=\"{container_2_id}{'true'}\"]"
+
+ b.wait_visible(container_1_sel + " .ct-badge-toolbox:contains('toolbox')")
+ b.wait_visible(container_2_sel + " .ct-badge-distrobox:contains('distrobox')")
+
+ def testCreatePodSystem(self):
+ self._createPod(True)
+
+ def testCreatePodUser(self):
+ self._createPod(False)
+
+ def _createPod(self, auth):
+ b = self.browser
+ m = self.machine
+ pod_name = "testpod1"
+
+ self.login(auth)
+
+ b.click("#containers-containers-create-pod-btn")
+ b.set_input_text("#create-pod-dialog-name", "")
+ b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:disabled")
+ b.wait_in_text("#pod-name-group .pf-v5-c-helper-text__item-text", "Invalid characters")
+
+ b.set_input_text("#create-pod-dialog-name", pod_name)
+ b.wait_visible(".pf-v5-c-modal-box__footer #create-pod-create-btn:not(:disabled)")
+
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text("#create-pod-dialog-publish-0-container-port-group input", "-1")
+ b.click(".pf-v5-c-modal-box__footer #create-pod-create-btn")
+ b.wait_in_text("#create-pod-dialog-publish-0-container-port-group .pf-v5-c-helper-text__item-text",
+ "1 to 65535")
+ b.click("#create-pod-dialog-publish-0-btn-close")
+
+ if auth:
+ b.wait_visible("#create-pod-dialog-owner-system:checked")
+ else:
+ b.wait_not_present("#create-pod-dialog-owner-system")
+
+ # Ports
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-1-host-port', '6000')
+ b.set_input_text('#create-pod-dialog-publish-1-container-port', '5000')
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-2-ip-address', '127.0.0.1')
+ b.set_input_text('#create-pod-dialog-publish-2-host-port', '6001')
+ b.set_input_text('#create-pod-dialog-publish-2-container-port', '5001')
+ b.set_val('#create-pod-dialog-publish-2-protocol', "udp")
+ b.click('.publish-port-form .btn-add')
+ b.set_input_text('#create-pod-dialog-publish-3-ip-address', '127.0.0.2')
+ b.set_input_text('#create-pod-dialog-publish-3-container-port', '9001')
+
+ # Volumes
+ if self.machine.image not in ["ubuntu-2204"]:
+ b.click('.volume-form .btn-add')
+ rodir, rwdir = m.execute("mktemp; mktemp").split('\n')[:2]
+ m.execute(f"chown admin:admin {rodir}")
+ m.execute(f"chown admin:admin {rwdir}")
+
+ if self.has_selinux:
+ b.set_val('#create-pod-dialog-volume-0-selinux', "z")
+ else:
+ b.wait_not_present('#create-pod-dialog-volume-0-selinux')
+
+ b.set_file_autocomplete_val("#create-pod-dialog-volume-0 .pf-v5-c-select", rodir)
+ b.set_input_text('#create-pod-dialog-volume-0-container-path', '/tmp/ro')
+ b.click('.volume-form .btn-add')
+
+ b.set_file_autocomplete_val("#create-pod-dialog-volume-1 .pf-v5-c-select", rwdir)
+ b.set_input_text('#create-pod-dialog-volume-1-container-path', '/tmp/rw')
+
+ b.click("#create-pod-create-btn")
+ b.set_val("#containers-containers-filter", "all")
+ self.waitPodContainer(pod_name, [])
+
+ container_name = 'test-pod-1-system' if auth else 'test-pod-1'
+ cmd = f"podman run -d --pod {pod_name} --name {container_name} --stop-timeout 0 {IMG_ALPINE} sleep 500"
+ containerId = self.execute(auth, cmd).strip()
+ self.waitPodContainer(pod_name,
+ [{"name": container_name, "image": IMG_ALPINE,
+ "command": "sleep 500", "state": "Running", "id": containerId}], auth)
+
+ self.toggleExpandedContainer(container_name)
+ b.click(".pf-m-expanded button:contains('Integration')")
+ if self.machine.image not in ["ubuntu-2204"]:
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd',
+ f"{rodir} \u2194 /tmp/ro")
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Volumes") + dd',
+ f"{rwdir} \u2194 /tmp/rw")
+
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
+ '0.0.0.0:6000 \u2192 5000/tcp')
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
+ '127.0.0.1:6001 \u2192 5001/udp')
+ b.wait_in_text('#containers-containers tr:contains("alpine") dt:contains("Ports") + dd',
+ ' \u2192 9001/tcp')
+
+ # Create pod as admin
+ if auth:
+ pod_name = 'testpod2'
+ b.click("#containers-containers-create-pod-btn")
+ b.set_input_text("#create-pod-dialog-name", pod_name)
+ b.click("#create-pod-dialog-owner-user")
+ b.click("#create-pod-create-btn")
+
+ b.set_val("#containers-containers-filter", "all")
+ self.waitPodContainer(pod_name, [])
+
+ @testlib.skipImage("passthrough log driver not supported", "ubuntu-2204")
+ def testLogErrors(self):
+ b = self.browser
+ container_name = "logissue"
+ self.login()
+
+ self.execute(False,
+ f"podman run --log-driver=passthrough --name {container_name} -d {IMG_ALPINE} false </dev/null")
+ self.waitContainerRow(container_name)
+ self.toggleExpandedContainer(container_name)
+ b.click(".pf-m-expanded button:contains('Logs')")
+ b.wait_in_text(".pf-m-expanded .pf-v5-c-empty-state__content", "failed to obtain logs for Container")
+
+ @testlib.skipOstree("/lib is read-only")
+ def testManifest(self):
+ b = self.browser
+ m = self.machine
+ self.restore_file("/lib/systemd/system/podman.socket", post_restore_action="systemctl daemon-reload")
+ m.execute("rm /lib/systemd/system/podman.socket")
+ self.login_and_go(None)
+ b.wait_in_text("#host-apps .pf-m-current", "Overview")
+
+ # HACK: is_pybridge should also check TEST_SCENARIO
+ if self.is_pybridge() or os.getenv('TEST_SCENARIO') == 'pybridge':
+ self.assertNotIn("Podman", b.text("#host-apps"))
+ else:
+ self.assertIn("Podman", b.text("#host-apps"))
+
+
+if __name__ == '__main__':
+ testlib.test_main()
diff --git a/test/common/__init__.py b/test/common/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/test/common/__init__.py
diff --git a/test/common/cdp.py b/test/common/cdp.py
new file mode 100644
index 0000000..034f7a2
--- /dev/null
+++ b/test/common/cdp.py
@@ -0,0 +1,381 @@
+import abc
+import fcntl
+import glob
+import json
+import os
+import random
+import resource
+import shutil
+import subprocess
+import sys
+import tempfile
+import time
+import typing
+import urllib.request
+from urllib.error import URLError
+
+TEST_DIR = os.path.normpath(os.path.dirname(os.path.realpath(os.path.join(__file__, ".."))))
+
+
+class Browser(abc.ABC):
+ # The name of the browser
+ NAME: str
+ # The executable names available for the browser
+ EXECUTABLES: typing.List[str]
+ # The filename of the cdp driver JS file
+ CDP_DRIVER_FILENAME: str
+
+ @property
+ def name(self):
+ return self.NAME
+
+ def find_exe(self):
+ """Try to find the path of the browser, or None if not found."""
+ for name in self.EXECUTABLES:
+ exe = shutil.which(name)
+ if exe is not None:
+ return exe
+ return None
+
+ @abc.abstractmethod
+ def _path(self, show_browser):
+ """Return the path of the browser if available, or None.
+
+ Reimplement this in subclasses, so it is easier to return None
+ than to raise the proper exception (done at once in path()).
+ """
+
+ def path(self, show_browser):
+ """Return the path of the browser, if available.
+
+ In case it is not found, this raises SystemError.
+ """
+ p = self._path(show_browser)
+ if p is not None:
+ return p
+ raise SystemError(f"{self.name} is not installed")
+
+ @abc.abstractmethod
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ pass
+
+
+class Chromium(Browser):
+ NAME = "chromium"
+ EXECUTABLES = ["chromium-browser", "chromium", "google-chrome", "chromium-freeworld"]
+ CDP_DRIVER_FILENAME = f"{TEST_DIR}/common/chromium-cdp-driver.js"
+
+ def _path(self, show_browser):
+ """Return path to chromium browser.
+
+ Support the following locations:
+ - /usr/lib*/chromium-browser/headless_shell (chromium-headless RPM)
+ - the executables in self.EXECUTABLES available in $PATH (distro package)
+ - node_modules/chromium/lib/chromium/chrome-linux/chrome (npm install chromium)
+ """
+
+ # If we want to have interactive chromium, we don't want to use headless_shell
+ if not show_browser:
+ g = glob.glob("/usr/lib*/chromium-browser/headless_shell")
+ if g:
+ return g[0]
+
+ p = self.find_exe()
+ if p:
+ return p
+
+ p = os.path.join(os.path.dirname(TEST_DIR), "node_modules/chromium/lib/chromium/chrome-linux/chrome")
+ if os.access(p, os.X_OK):
+ return p
+
+ return None
+
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ exe = self.path(show_browser)
+
+ return [exe, "--headless" if not show_browser else "",
+ "--no-sandbox", "--disable-setuid-sandbox",
+ "--disable-namespace-sandbox", "--disable-seccomp-filter-sandbox",
+ "--disable-sandbox-denial-logging", "--disable-pushstate-throttle",
+ "--font-render-hinting=none",
+ "--v=0", f"--remote-debugging-port={cdp_port}", "about:blank"]
+
+
+class Firefox(Browser):
+ NAME = "firefox"
+ EXECUTABLES = ["firefox-developer-edition", "firefox-nightly", "firefox"]
+ CDP_DRIVER_FILENAME = f"{TEST_DIR}/common/firefox-cdp-driver.js"
+
+ def _path(self, show_browser):
+ """Return path to Firefox browser."""
+ return self.find_exe()
+
+ def cmd(self, cdp_port, env, show_browser, browser_home, download_dir):
+ exe = self.path(show_browser)
+
+ subprocess.check_call([exe, "--headless", "--no-remote", "-CreateProfile", "blank"], env=env)
+ profile = glob.glob(os.path.join(browser_home, ".mozilla/firefox/*.blank"))[0]
+
+ with open(os.path.join(profile, "user.js"), "w") as f:
+ f.write(f"""
+ user_pref("remote.enabled", true);
+ user_pref("remote.frames.enabled", true);
+ user_pref("app.update.auto", false);
+ user_pref("datareporting.policy.dataSubmissionEnabled", false);
+ user_pref("toolkit.telemetry.reportingpolicy.firstRun", false);
+ user_pref("dom.disable_beforeunload", true);
+ user_pref("browser.download.dir", "{download_dir}");
+ user_pref("browser.download.folderList", 2);
+ user_pref("signon.rememberSignons", false);
+ user_pref("dom.navigation.locationChangeRateLimit.count", 9999);
+ // HACK: https://bugzilla.mozilla.org/show_bug.cgi?id=1746154
+ user_pref("fission.webContentIsolationStrategy", 0);
+ user_pref("fission.bfcacheInParent", false);
+ """)
+
+ with open(os.path.join(profile, "handlers.json"), "w") as f:
+ f.write('{'
+ '"defaultHandlersVersion":{"en-US":4},'
+ '"mimeTypes":{"application/xz":{"action":0,"extensions":["xz"]}}'
+ '}')
+
+ cmd = [exe, "-P", "blank", f"--remote-debugging-port={cdp_port}", "--no-remote", "localhost"]
+ if not show_browser:
+ cmd.insert(3, "--headless")
+ return cmd
+
+
+def get_browser(browser):
+ browser_classes = [
+ Chromium,
+ Firefox,
+ ]
+ for klass in browser_classes:
+ if browser == klass.NAME:
+ return klass()
+ raise SystemError(f"Unsupported browser: {browser}")
+
+
+def jsquote(obj):
+ return json.dumps(obj)
+
+
+class CDP:
+ def __init__(self, lang=None, verbose=False, trace=False, inject_helpers=None, start_profile=False):
+ self.lang = lang
+ self.timeout = 15
+ self.valid = False
+ self.verbose = verbose
+ self.trace = trace
+ self.inject_helpers = inject_helpers or []
+ self.start_profile = start_profile
+ self.browser = get_browser(os.environ.get("TEST_BROWSER", "chromium"))
+ self.show_browser = bool(os.environ.get("TEST_SHOW_BROWSER", ""))
+ self.download_dir = tempfile.mkdtemp()
+ self._driver = None
+ self._browser = None
+ self._browser_home = None
+ self._cdp_port_lockfile = None
+
+ def invoke(self, fn, **kwargs):
+ """Call a particular CDP method such as Runtime.evaluate
+
+ Use command() for arbitrary JS code.
+ """
+ trace = self.trace and not kwargs.get("no_trace", False)
+ try:
+ del kwargs["no_trace"]
+ except KeyError:
+ pass
+
+ cmd = fn + "(" + json.dumps(kwargs) + ")"
+
+ # frame support for Runtime.evaluate(): map frame name to
+ # executionContextId and insert into argument object; this must not be quoted
+ # see "Frame tracking" in cdp-driver.js for how this works
+ if fn == 'Runtime.evaluate':
+ cmd = "%s, contextId: getFrameExecId(%s)%s" % (cmd[:-2], jsquote(self.cur_frame), cmd[-2:])
+
+ if trace:
+ print("-> " + kwargs.get('trace', cmd))
+
+ # avoid having to write the "client." prefix everywhere
+ cmd = "client." + cmd
+ res = self.command(cmd)
+ if trace:
+ if res and "result" in res:
+ print("<- " + repr(res["result"]))
+ else:
+ print("<- " + repr(res))
+ return res
+
+ def command(self, cmd):
+ if not self._driver:
+ self.start()
+ self._driver.stdin.write(cmd.encode("UTF-8"))
+ self._driver.stdin.write(b"\n")
+ self._driver.stdin.flush()
+ line = self._driver.stdout.readline().decode("UTF-8")
+ if not line:
+ self.kill()
+ raise RuntimeError("CDP broken")
+ try:
+ res = json.loads(line)
+ except ValueError:
+ print(line.strip())
+ raise
+
+ if "error" in res:
+ if self.trace:
+ print("<- raise %s" % str(res["error"]))
+ raise RuntimeError(res["error"])
+ return res["result"]
+
+ def claim_port(self, port):
+ f = None
+ try:
+ f = open(os.path.join(tempfile.gettempdir(), ".cdp-%i.lock" % port), "w")
+ fcntl.flock(f, fcntl.LOCK_EX | fcntl.LOCK_NB)
+ self._cdp_port_lockfile = f
+ return True
+ except (IOError, OSError):
+ if f:
+ f.close()
+ return False
+
+ def find_cdp_port(self):
+ """Find an unused port and claim it through lock file"""
+
+ for _ in range(100):
+ # don't use the default CDP port 9222 to avoid interfering with running browsers
+ port = random.randint(9223, 10222)
+ if self.claim_port(port):
+ return port
+
+ raise RuntimeError("unable to find free port")
+
+ def start(self):
+ environ = os.environ.copy()
+ if self.lang:
+ environ["LC_ALL"] = self.lang
+ self.cur_frame = None
+
+ # allow attaching to external browser
+ cdp_port = None
+ if "TEST_CDP_PORT" in os.environ:
+ p = int(os.environ["TEST_CDP_PORT"])
+ if self.claim_port(p):
+ # can fail when a test starts multiple browsers; only show the first one
+ cdp_port = p
+
+ if not cdp_port:
+ # start browser on a new port
+ cdp_port = self.find_cdp_port()
+ self._browser_home = tempfile.mkdtemp()
+ environ = os.environ.copy()
+ environ["HOME"] = self._browser_home
+ environ["LC_ALL"] = "C.UTF-8"
+ # this might be set for the tests themselves, but we must isolate caching between tests
+ try:
+ del environ["XDG_CACHE_HOME"]
+ except KeyError:
+ pass
+
+ cmd = self.browser.cmd(cdp_port, environ, self.show_browser,
+ self._browser_home, self.download_dir)
+
+ # sandboxing does not work in Docker container
+ self._browser = subprocess.Popen(
+ cmd, env=environ, close_fds=True,
+ preexec_fn=lambda: resource.setrlimit(resource.RLIMIT_CORE, (0, 0)))
+ if self.verbose:
+ sys.stderr.write("Started %s (pid %i) on port %i\n" % (cmd[0], self._browser.pid, cdp_port))
+
+ # wait for CDP to be up and have at least one target
+ for _ in range(120):
+ try:
+ res = urllib.request.urlopen(f"http://127.0.0.1:{cdp_port}/json/list", timeout=5)
+ if res.getcode() == 200 and json.loads(res.read()):
+ break
+ except URLError:
+ pass
+ time.sleep(0.5)
+ else:
+ raise RuntimeError('timed out waiting for browser to start')
+
+ # now start the driver
+ if self.trace:
+ # enable frame/execution context debugging if tracing is on
+ environ["TEST_CDP_DEBUG"] = "1"
+
+ self._driver = subprocess.Popen([self.browser.CDP_DRIVER_FILENAME, str(cdp_port)],
+ env=environ,
+ stdout=subprocess.PIPE,
+ stdin=subprocess.PIPE,
+ close_fds=True)
+ self.valid = True
+
+ for inject in self.inject_helpers:
+ with open(inject) as f:
+ src = f.read()
+ # HACK: injecting sizzle fails on missing `document` in assert()
+ src = src.replace('function assert( fn ) {', 'function assert( fn ) { if (true) return true; else ')
+ # HACK: sizzle tracks document and when we switch frames, it sees the old document
+ # although we execute it in different context.
+ src = src.replace('context = context || document;', 'context = context || window.document;')
+ self.invoke("Page.addScriptToEvaluateOnNewDocument", source=src, no_trace=True)
+
+ if self.start_profile:
+ self.invoke("Profiler.enable")
+ self.invoke("Profiler.startPreciseCoverage", callCount=False, detailed=True)
+
+ def kill(self):
+ self.valid = False
+ self.cur_frame = None
+ if self._driver:
+ self._driver.stdin.close()
+ self._driver.wait()
+ self._driver = None
+
+ shutil.rmtree(self.download_dir, ignore_errors=True)
+
+ if self._browser:
+ if self.verbose:
+ sys.stderr.write("Killing browser (pid %i)\n" % self._browser.pid)
+ try:
+ self._browser.terminate()
+ except OSError:
+ pass # ignore if it crashed for some reason
+ self._browser.wait()
+ self._browser = None
+ shutil.rmtree(self._browser_home, ignore_errors=True)
+ os.remove(self._cdp_port_lockfile.name)
+ self._cdp_port_lockfile.close()
+
+ def set_frame(self, frame):
+ self.cur_frame = frame
+ if self.trace:
+ print("-> switch to frame %s" % frame)
+
+ def get_js_log(self):
+ """Return the current javascript console log"""
+
+ if self.valid:
+ # needs to be wrapped in Promise
+ messages = self.command("Promise.resolve(messages)")
+ return ["%s: %s" % tuple(m) for m in messages]
+ return []
+
+ def read_log(self):
+ """Returns an iterator that produces log messages one by one.
+
+ Blocks if there are no new messages right now."""
+
+ if not self.valid:
+ yield []
+ return
+
+ while True:
+ messages = self.command("waitLog()")
+ for m in messages:
+ yield m
diff --git a/test/common/chromium-cdp-driver.js b/test/common/chromium-cdp-driver.js
new file mode 100755
index 0000000..0d7a597
--- /dev/null
+++ b/test/common/chromium-cdp-driver.js
@@ -0,0 +1,332 @@
+#!/usr/bin/env node
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2017 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* chromium-cdp-driver -- A command-line JSON input/output wrapper around
+ * chrome-remote-interface (Chrome Debug Protocol).
+ * See https://chromedevtools.github.io/devtools-protocol/
+ * This needs support for protocol version 1.3.
+ *
+ * Set $TEST_CDP_DEBUG environment variable to enable additional
+ * frame/execution context debugging.
+ */
+
+import * as readline from 'node:readline/promises';
+import CDP from 'chrome-remote-interface';
+
+let enable_debug = false;
+
+function debug(msg) {
+ if (enable_debug)
+ process.stderr.write("CDP: " + msg + "\n");
+}
+
+/**
+ * Format response to the client
+ */
+
+function fail(err) {
+ if (typeof err === 'undefined')
+ err = null;
+ process.stdout.write(JSON.stringify({ error: err }) + '\n');
+}
+
+function success(result) {
+ if (typeof result === 'undefined')
+ result = null;
+ process.stdout.write(JSON.stringify({ result }) + '\n');
+}
+
+/**
+ * Record console.*() calls and Log messages so that we can forward them to
+ * stderr and dump them on test failure
+ */
+const messages = [];
+let logPromiseResolver;
+let nReportedLogMessages = 0;
+const unhandledExceptions = [];
+
+function clearExceptions() {
+ unhandledExceptions.length = 0;
+ return Promise.resolve();
+}
+
+function stringifyConsoleArg(arg) {
+ try {
+ if (arg.type === 'string')
+ return arg.value;
+ if (arg.type === 'number')
+ return arg.value;
+ if (arg.type === 'undefined')
+ return "undefined";
+ if (arg.value === null)
+ return "null";
+ if (arg.type === 'object' && arg.preview?.properties) {
+ const obj = {};
+ arg.preview.properties.forEach(prop => {
+ obj[prop.name] = prop.value.toString();
+ });
+ return JSON.stringify(obj);
+ }
+ return JSON.stringify(arg);
+ } catch (error) {
+ return "[error stringifying argument: " + error.toString() + "]";
+ }
+}
+
+function setupLogging(client) {
+ client.Runtime.enable();
+
+ client.Runtime.consoleAPICalled(info => {
+ const msg = info.args.map(stringifyConsoleArg).join(" ");
+ messages.push([info.type, msg]);
+ process.stderr.write("> " + info.type + ": " + msg + "\n");
+
+ resolveLogPromise();
+ });
+
+ client.Runtime.exceptionThrown(info => {
+ const details = info.exceptionDetails;
+ // don't log test timeouts, they already get handled
+ if (details.exception && details.exception.className === "PhWaitCondTimeout")
+ return;
+
+ process.stderr.write(details.description || JSON.stringify(details) + "\n");
+
+ unhandledExceptions.push(details.exception.message ||
+ details.exception.description ||
+ details.exception.value ||
+ JSON.stringify(details.exception));
+ });
+
+ client.Log.enable();
+ client.Log.entryAdded(entry => {
+ const msg = entry.entry;
+
+ messages.push(["cdp", msg]);
+ /* Ignore authentication failure log lines that don't denote failures */
+ if (!(msg.url || "").endsWith("/login") || (msg.text || "").indexOf("401") === -1) {
+ process.stderr.write("CDP: " + JSON.stringify(msg) + "\n");
+ }
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Resolve the log promise created with waitLog().
+ */
+function resolveLogPromise() {
+ if (logPromiseResolver) {
+ logPromiseResolver(messages.slice(nReportedLogMessages));
+ nReportedLogMessages = messages.length;
+ logPromiseResolver = undefined;
+ }
+}
+
+/**
+ * Returns a promise that resolves when log messages are available. If there
+ * are already some unreported ones in the global messages variable, resolves
+ * immediately.
+ *
+ * Only one such promise can be active at a given time. Once the promise is
+ * resolved, this function can be called again to wait for further messages.
+ */
+function waitLog() { // eslint-disable-line no-unused-vars
+ console.assert(logPromiseResolver === undefined);
+
+ return new Promise((resolve, reject) => {
+ logPromiseResolver = resolve;
+
+ if (nReportedLogMessages < messages.length)
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Frame tracking
+ *
+ * For tests to be able to select the current frame (by its name) and make
+ * subsequent queries apply to that, we need to track frame name → frameId →
+ * executionContextId. Frame and context IDs can even change through page
+ * operations (e. g. in systemd/logs.js when reporting a crash is complete),
+ * so we also need a helper function to explicitly wait for a particular frame
+ * to load. This is very laborious, see this issue for discussing improvements:
+ * https://github.com/ChromeDevTools/devtools-protocol/issues/72
+ */
+const frameIdToContextId = {};
+const frameNameToFrameId = {};
+
+let pageLoadHandler = null;
+
+function setupFrameTracking(client) {
+ client.Page.enable();
+
+ // map frame names to frame IDs; root frame has no name, no need to track that
+ client.Page.frameNavigated(info => {
+ debug("frameNavigated " + JSON.stringify(info));
+ frameNameToFrameId[info.frame.name || "cockpit1"] = info.frame.id;
+ });
+
+ client.Page.loadEventFired(() => {
+ if (pageLoadHandler) {
+ debug("loadEventFired, resolving pageLoadHandler");
+ pageLoadHandler();
+ }
+ });
+
+ // track execution contexts so that we can map between context and frame IDs
+ client.Runtime.executionContextCreated(info => {
+ debug("executionContextCreated " + JSON.stringify(info));
+ frameIdToContextId[info.context.auxData.frameId] = info.context.id;
+ });
+
+ client.Runtime.executionContextDestroyed(info => {
+ debug("executionContextDestroyed " + info.executionContextId);
+ for (const frameId in frameIdToContextId) {
+ if (frameIdToContextId[frameId] == info.executionContextId) {
+ delete frameIdToContextId[frameId];
+ break;
+ }
+ }
+ });
+}
+
+function setupLocalFunctions(client) {
+ client.waitPageLoad = (args) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ pageLoadHandler = null;
+ reject("Timeout waiting for page load"); // eslint-disable-line prefer-promise-reject-errors
+ }, (args.timeout ?? 15) * 1000);
+ pageLoadHandler = () => {
+ clearTimeout(timeout);
+ pageLoadHandler = null;
+ resolve({});
+ };
+ });
+
+ client.reloadPageAndWait = (args) => new Promise((resolve, reject) => {
+ pageLoadHandler = () => { pageLoadHandler = null; resolve({}) };
+ client.Page.reload(args);
+ });
+
+ async function setCSS({ text, frame }) {
+ await client.DOM.enable();
+ await client.CSS.enable();
+ const id = (await client.CSS.createStyleSheet({ frameId: frameNameToFrameId[frame] })).styleSheetId;
+ await client.CSS.setStyleSheetText({
+ styleSheetId: id,
+ text
+ });
+ }
+
+ client.setCSS = setCSS;
+}
+
+// helper functions for testlib.py which are too unwieldy to be poked in from Python
+
+// eslint-disable-next-line no-unused-vars
+const getFrameExecId = frame => frameIdToContextId[frameNameToFrameId[frame ?? "cockpit1"]];
+
+/**
+ * SSL handling
+ */
+
+// secure by default; tests can override to "continue"
+// https://chromedevtools.github.io/devtools-protocol/1-3/Security/#type-CertificateErrorAction
+let ssl_bad_certificate_action = "cancel";
+
+/**
+ * Change what happens when the browser opens a page with an invalid SSL certificate.
+ * Defaults to "cancel", can be set to "continue".
+ */
+function setSSLBadCertificateAction(action) { // eslint-disable-line no-unused-vars
+ ssl_bad_certificate_action = action;
+ return Promise.resolve();
+}
+
+function setupSSLCertHandling(client) {
+ client.Security.enable();
+
+ client.Security.setOverrideCertificateErrors({ override: true });
+ client.Security.certificateError(info => {
+ process.stderr.write(`CDP: Security.certificateError ${JSON.stringify(info)}; action: ${ssl_bad_certificate_action}\n`);
+ client.Security.handleCertificateError({ eventId: info.eventId, action: ssl_bad_certificate_action })
+ .catch(ex => {
+ // some race condition in Chromium, ok if the event is already gone
+ if (ex.response && ex.response.message && ex.response.message.indexOf("Unknown event id") >= 0)
+ debug(`setupSSLCertHandling for event ${info.eventId} failed, ignoring: ${JSON.stringify(ex.response)}`);
+ else
+ throw ex;
+ });
+ });
+}
+
+/**
+ * Main input/process loop
+ *
+ * Read one line with a JS expression, eval() it, and respond with the result:
+ * success <JSON formatted return value>
+ * fail <JSON formatted error>
+ * EOF shuts down the client.
+ */
+async function main() {
+ process.stdin.setEncoding('utf8');
+
+ if (process.env.TEST_CDP_DEBUG)
+ enable_debug = true;
+
+ const options = { };
+ if (process.argv.length >= 3) {
+ options.port = parseInt(process.argv[2]);
+ if (!options.port) {
+ process.stderr.write("Usage: chromium-cdp-driver.js [port]\n");
+ process.exit(1);
+ }
+ }
+
+ const target = await CDP.New(options);
+ target.port = options.port;
+ const client = await CDP({ target });
+ setupLogging(client);
+ setupFrameTracking(client);
+ setupSSLCertHandling(client);
+ setupLocalFunctions(client);
+
+ for await (const command of readline.createInterface(process.stdin)) {
+ try {
+ const reply = await eval(command); // eslint-disable-line no-eval
+ if (unhandledExceptions.length === 0) {
+ success(reply);
+ } else {
+ const message = unhandledExceptions[0];
+ fail(message.split("\n")[0]);
+ clearExceptions();
+ }
+ } catch (err) {
+ fail(err);
+ }
+ }
+ await CDP.Close(target);
+}
+
+main().catch(err => {
+ console.error(err);
+ process.exit(1);
+});
diff --git a/test/common/firefox-cdp-driver.js b/test/common/firefox-cdp-driver.js
new file mode 100755
index 0000000..f5b2bf8
--- /dev/null
+++ b/test/common/firefox-cdp-driver.js
@@ -0,0 +1,392 @@
+#!/usr/bin/env node
+
+/*
+ * This file is part of Cockpit.
+ *
+ * Copyright (C) 2019 Red Hat, Inc.
+ *
+ * Cockpit is free software; you can redistribute it and/or modify it
+ * under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation; either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * Cockpit is distributed in the hope that it will be useful, but
+ * WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/* firefox-cdp-driver -- A command-line JSON input/output wrapper around
+ * chrome-remote-interface (Chrome Debug Protocol).
+ * See https://chromedevtools.github.io/devtools-protocol/
+ * This needs support for protocol version 1.3.
+ *
+ * Set $TEST_CDP_DEBUG environment variable to enable additional
+ * frame/execution context debugging.
+ */
+
+import * as readline from 'readline';
+import CDP from 'chrome-remote-interface';
+
+let enable_debug = false;
+
+function debug(msg) {
+ if (enable_debug)
+ process.stderr.write("CDP: " + msg + "\n");
+}
+
+/**
+ * Format response to the client
+ */
+
+function fatal() {
+ console.error.apply(console.error, arguments);
+ process.exit(1);
+}
+
+// We keep sequence numbers so that we never get the protocol out of
+// synch with re-ordered or duplicate replies. This only matters for
+// duplicate replies due to destroyed contexts, but that is already so
+// hairy that this big hammer seems necessary.
+
+let cur_cmd_seq = 0;
+let next_reply_seq = 1;
+
+function fail(seq, err) {
+ if (seq != next_reply_seq)
+ return;
+ next_reply_seq++;
+
+ if (typeof err === 'undefined')
+ err = null;
+ process.stdout.write(JSON.stringify({ error: err }) + '\n');
+}
+
+function success(seq, result) {
+ if (seq != next_reply_seq)
+ return;
+ next_reply_seq++;
+
+ if (typeof result === 'undefined')
+ result = null;
+ process.stdout.write(JSON.stringify({ result }) + '\n');
+}
+
+/**
+ * Record console.*() calls and Log messages so that we can forward them to
+ * stderr and dump them on test failure
+ */
+const messages = [];
+let logPromiseResolver;
+let nReportedLogMessages = 0;
+const unhandledExceptions = [];
+
+function clearExceptions() {
+ unhandledExceptions.length = 0;
+ return Promise.resolve();
+}
+
+function stringifyConsoleArg(arg) {
+ if (arg.type === 'string')
+ return arg.value;
+ if (arg.type === 'object')
+ return JSON.stringify(arg.value);
+ return JSON.stringify(arg);
+}
+
+function setupLogging(client) {
+ client.Runtime.enable();
+
+ client.Runtime.consoleAPICalled(info => {
+ const msg = info.args.map(stringifyConsoleArg).join(" ");
+ messages.push([info.type, msg]);
+ process.stderr.write("> " + info.type + ": " + msg + "\n");
+
+ resolveLogPromise();
+ });
+
+ function processException(info) {
+ let details = info.exceptionDetails;
+ if (details.exception)
+ details = details.exception;
+
+ // don't log test timeouts, they already get handled
+ if (details.className === "PhWaitCondTimeout")
+ return;
+
+ process.stderr.write(details.description || details.text || JSON.stringify(details) + "\n");
+
+ unhandledExceptions.push(details.message ||
+ details.description ||
+ details.value ||
+ JSON.stringify(details));
+ }
+
+ client.Runtime.exceptionThrown(info => processException(info));
+
+ client.Log.enable();
+ client.Log.entryAdded(entry => {
+ // HACK: Firefox does not implement `Runtime.exceptionThrown` but logs it
+ // Lets parse it to have at least some basic check that code did not throw
+ // exception
+ // https://bugzilla.mozilla.org/show_bug.cgi?id=1549528
+
+ const msg = entry.entry;
+ let text = msg.text;
+ if (typeof text !== "string")
+ if (text[0] && typeof text[0] === "string")
+ text = text[0];
+
+ if (msg.stackTrace !== undefined &&
+ typeof text === "string" &&
+ text.indexOf("Error: ") !== -1) {
+ const trace = text.split(": ", 1);
+ processException({
+ exceptionDetails: {
+ exception: {
+ className: trace[0],
+ message: trace.length > 1 ? trace[1] : "",
+ stacktrace: msg.stackTrace,
+ entry: msg,
+ },
+ }
+ });
+ } else {
+ messages.push(["cdp", msg]);
+ /* Ignore authentication failure log lines that don't denote failures */
+ if (!(msg.url || "").endsWith("/login") || (text || "").indexOf("401") === -1) {
+ process.stderr.write("CDP: " + JSON.stringify(msg) + "\n");
+ }
+ resolveLogPromise();
+ }
+ });
+}
+
+/**
+ * Resolve the log promise created with waitLog().
+ */
+function resolveLogPromise() {
+ if (logPromiseResolver) {
+ logPromiseResolver(messages.slice(nReportedLogMessages));
+ nReportedLogMessages = messages.length;
+ logPromiseResolver = undefined;
+ }
+}
+
+/**
+ * Returns a promise that resolves when log messages are available. If there
+ * are already some unreported ones in the global messages variable, resolves
+ * immediately.
+ *
+ * Only one such promise can be active at a given time. Once the promise is
+ * resolved, this function can be called again to wait for further messages.
+ */
+function waitLog() { // eslint-disable-line no-unused-vars
+ console.assert(logPromiseResolver === undefined);
+
+ return new Promise((resolve, reject) => {
+ logPromiseResolver = resolve;
+
+ if (nReportedLogMessages < messages.length)
+ resolveLogPromise();
+ });
+}
+
+/**
+ * Frame tracking
+ *
+ * For tests to be able to select the current frame (by its name) and make
+ * subsequent queries apply to that, we need to track frame name → frameId →
+ * executionContextId. Frame and context IDs can even change through page
+ * operations (e. g. in systemd/logs.js when reporting a crash is complete),
+ * so we also need a helper function to explicitly wait for a particular frame
+ * to load. This is very laborious, see this issue for discussing improvements:
+ * https://github.com/ChromeDevTools/devtools-protocol/issues/72
+ */
+const scriptsOnNewContext = [];
+const frameIdToContextId = {};
+const frameNameToFrameId = {};
+
+let pageLoadHandler = null;
+let currentExecId = null;
+
+function setupFrameTracking(client) {
+ client.Page.enable();
+
+ // map frame names to frame IDs; root frame has no name, no need to track that
+ client.Page.frameNavigated(info => {
+ if (info.frame?.url?.startsWith("about:")) {
+ debug("frameNavigated: ignoring about: frame " + JSON.stringify(info));
+ return;
+ }
+ debug("frameNavigated " + JSON.stringify(info));
+ frameNameToFrameId[info.frame.name || "cockpit1"] = info.frame.id;
+ });
+
+ client.Page.loadEventFired(() => {
+ if (pageLoadHandler) {
+ debug("loadEventFired, resolving pageLoadHandler");
+ pageLoadHandler();
+ }
+ });
+
+ // track execution contexts so that we can map between context and frame IDs
+ client.Runtime.executionContextCreated(info => {
+ debug("executionContextCreated " + JSON.stringify(info));
+ frameIdToContextId[info.context.auxData.frameId] = info.context.id;
+ scriptsOnNewContext.forEach(s => {
+ client.Runtime.evaluate({ expression: s, contextId: info.context.id })
+ .catch(ex => {
+ // race condition with short-lived frames -- OK if the frame is already gone
+ if (ex.response && ex.response.message && ex.response.message.indexOf("Cannot find context") >= 0)
+ debug(`scriptsOnNewContext for context ${info.context.id} failed, ignoring: ${JSON.stringify(ex.response)}`);
+ else
+ throw ex;
+ });
+ });
+ });
+
+ client.Runtime.executionContextDestroyed(info => {
+ debug("executionContextDestroyed " + info.executionContextId);
+ for (const frameId in frameIdToContextId) {
+ if (frameIdToContextId[frameId] == info.executionContextId) {
+ delete frameIdToContextId[frameId];
+ break;
+ }
+ }
+
+ // Firefox does not report an error when the execution context
+ // of a Runtime.evaluate call gets destroyed. It will never
+ // ever resolve or be rejected. So let's provide the failure
+ // reply from here.
+ //
+ // However, if the timing is just right, the context gets
+ // destroyed before Runtime.evaluate has started the real
+ // processing, and in that case it will return an error. Then
+ // we would send the reply here, and would also send the
+ // error. This would drive the protocol out of synch. Also, our driver
+ // might immediately send more commands after seeing the first reply,
+ // and the unwanted second reply might be triggered in the middle of one
+ // of the next commands. To reliably suppress the second reply we have
+ // the pretty general sequence number checks.
+ //
+ if (info.executionContextId == currentExecId) {
+ currentExecId = null;
+ fail(cur_cmd_seq, { response: { message: "Execution context was destroyed." } });
+ }
+ });
+}
+
+function setupLocalFunctions(client) {
+ client.waitPageLoad = (args) => new Promise((resolve, reject) => {
+ const timeout = setTimeout(() => {
+ pageLoadHandler = null;
+ reject("Timeout waiting for page load"); // eslint-disable-line prefer-promise-reject-errors
+ }, 15000);
+ pageLoadHandler = () => {
+ clearTimeout(timeout);
+ pageLoadHandler = null;
+ resolve({});
+ };
+ });
+
+ client.reloadPageAndWait = (args) => new Promise((resolve, reject) => {
+ pageLoadHandler = () => { pageLoadHandler = null; resolve({}) };
+ client.Page.reload(args);
+ });
+}
+
+// helper functions for testlib.py which are too unwieldy to be poked in from Python
+function getFrameExecId(frame) { // eslint-disable-line no-unused-vars
+ const frameId = frameNameToFrameId[frame || "cockpit1"];
+ const execId = frameIdToContextId[frameId];
+ if (execId !== undefined)
+ currentExecId = execId;
+ else
+ debug(`WARNING: getFrameExecId: frame ${frame} ID ${frameId} has no known execution context`);
+ return execId;
+}
+
+/**
+ * Main input/process loop
+ *
+ * Read one line with a JS expression, eval() it, and respond with the result:
+ * success <JSON formatted return value>
+ * fail <JSON formatted error>
+ * EOF shuts down the client.
+ */
+process.stdin.setEncoding('utf8');
+
+if (process.env.TEST_CDP_DEBUG)
+ enable_debug = true;
+
+const options = { };
+if (process.argv.length >= 3) {
+ options.port = parseInt(process.argv[2]);
+ if (!options.port) {
+ process.stderr.write("Usage: firefox-cdp-driver.js [port]\n");
+ process.exit(1);
+ }
+}
+
+// HACK: `addScriptToEvaluateOnNewDocument` is not implemented in Firefox
+// thus save all scripts in array and on each new context just execute these
+// scripts in them
+// https://bugzilla.mozilla.org/show_bug.cgi?id=1549465
+function addScriptToEvaluateOnNewDocument(script) { // eslint-disable-line no-unused-vars
+ return new Promise((resolve, reject) => {
+ scriptsOnNewContext.push(script.source);
+ resolve();
+ });
+}
+
+// This should work on different targets (meaning tabs)
+// CDP takes {target:target} so we can pick target
+// Problem is that CDP.New() which creates new target works only for chrome/ium
+// But we should be able to use CPD.List to list all targets and then pick one
+// Firefox just gives them ascending numbers, so we can pick the one with highest number
+// and if we feel fancy we can check that url is `about:newtab`.
+// That still though does not create new tab - but we can just call `firefox about:blank`
+// from cdline and since firefox would open it in the same browser, it should work.
+// This would work just fine in CI (as there would be only one browser) but on our machines it may
+// pick a wrong window (no idea if they can be somehow distinguish and execute it in a specific
+// one). But I guess we can live with it (and it seems it picks the last opened window anyway,
+// so having your own browser running should not interfere)
+//
+// Just calling executable to open another tab in the same browser works also for chromium, so
+// should be fine
+CDP(options)
+ .then(client => {
+ setupLogging(client);
+ setupFrameTracking(client);
+ setupLocalFunctions(client);
+ // TODO: Security handling not yet supported in Firefox
+
+ readline.createInterface(process.stdin)
+ .on('line', command => {
+ // HACKS: See description of related functions
+ if (command.startsWith("client.Page.addScriptToEvaluateOnNewDocument"))
+ command = command.substring(12);
+
+ // run the command
+ const seq = ++cur_cmd_seq;
+ eval(command).then(reply => { // eslint-disable-line no-eval
+ currentExecId = null;
+ if (unhandledExceptions.length === 0) {
+ success(seq, reply);
+ } else {
+ const message = unhandledExceptions[0];
+ fail(seq, message.split("\n")[0]);
+ clearExceptions();
+ }
+ }, err => {
+ currentExecId = null;
+ fail(seq, err);
+ });
+ })
+ .on('close', () => process.exit(0));
+ })
+ .catch(fatal);
diff --git a/test/common/git-utils.sh b/test/common/git-utils.sh
new file mode 100644
index 0000000..dc0767d
--- /dev/null
+++ b/test/common/git-utils.sh
@@ -0,0 +1,150 @@
+# shellcheck shell=sh
+# doesn't do anything on its own. must be sourced.
+
+# The script which sources this script must set the following variables:
+# GITHUB_REPO = the relative repo name of the submodule on github
+# SUBDIR = the location in the working tree where the submodule goes
+# We also expect `set -eu`, but set them ourselves for shellcheck.
+set -eu
+[ -n "${GITHUB_REPO}" ]
+[ -n "${SUBDIR}" ]
+
+# Set by git-rebase for spawned actions
+unset GIT_DIR GIT_EXEC_PATH GIT_PREFIX GIT_REFLOG_ACTION GIT_WORK_TREE
+
+GITHUB_BASE="${GITHUB_BASE:-cockpit-project/cockpit}"
+GITHUB_REPOSITORY="${GITHUB_BASE%/*}/${GITHUB_REPO}"
+HTTPS_REMOTE="https://github.com/${GITHUB_REPOSITORY}"
+# shellcheck disable=SC2034 # used in other scripts
+SSH_REMOTE="git@github.com:${GITHUB_REPOSITORY}"
+
+CACHE_DIR="${XDG_CACHE_HOME-${HOME}/.cache}/cockpit-dev/${GITHUB_REPOSITORY}.git"
+
+if [ "${V-}" = 0 ]; then
+ message() { printf " %-8s %s\n" "$1" "$2" >&2; }
+ quiet='--quiet'
+else
+ message() { :; }
+ quiet=''
+fi
+
+init_cache() {
+ if [ ! -d "${CACHE_DIR}" ]; then
+ message INIT "${CACHE_DIR}"
+ mkdir -p "${CACHE_DIR}"
+ git init --bare --template='' ${quiet} "${CACHE_DIR}"
+ git --git-dir "${CACHE_DIR}" remote add origin "${HTTPS_REMOTE}"
+ fi
+}
+
+# runs a git command on the cache dir
+git_cache() {
+ init_cache
+ git --git-dir "${CACHE_DIR}" "$@"
+}
+
+# reads the named gitlink from the current state of the index
+# returns (ie: prints) a 40-character commit ID
+get_index_gitlink() {
+ if ! git ls-files -s "$1" | grep -E -o '\<[[:xdigit:]]{40}\>'; then
+ echo "*** couldn't read gitlink for file $1 from the index" >&2
+ exit 1
+ fi
+}
+
+# This checks if the given argument "$1" (already) exists in the repository
+# we use git rev-list --objects to to avoid problems with incomplete fetches:
+# we want to make sure the complete commit is there
+check_ref() {
+ git_cache rev-list --quiet --objects "$1" -- 2>/dev/null
+}
+
+# Fetch a specific commit ID into the cache
+# Either we have this commit available locally (in which case this function
+# does nothing), or we need to fetch it. There's no chance that the object
+# changed on the server, because we define it by its checksum.
+fetch_sha_to_cache() {
+ sha="$1"
+
+ # No "offline mode" here: we either have the commit, or we don't
+ if ! check_ref "${sha}"; then
+ message FETCH "${SUBDIR} [ref: ${sha}]"
+ git_cache fetch --no-tags ${quiet} origin "${sha}"
+ # tag it to keep it from being GC'd.
+ git_cache tag "sha-${sha}" "${sha}"
+ fi
+}
+
+# General purpose "fetch" function to be used with tags, refs, or nothing at
+# all (to fetch everything). This checks the server for updates, because all
+# of those things might change at any given time. Supports an "offline" mode
+# to skip the fetch and use the possibly-stale local version, if we have it.
+fetch_to_cache() {
+ # We're fetching a named ref (or all refs), which means:
+ # - we should always do the fetch because it might have changed. but
+ # - we might be able to skip updating in case we already have it
+ if [ -z "${OFFLINE-}" ]; then
+ for retry in $(seq 3); do
+ message FETCH "${SUBDIR} ${1+[ref: $*]}"
+ if git_cache fetch --prune ${quiet} origin "$@"; then
+ return
+ fi
+ sleep $((retry * retry * 5))
+ done
+ echo "repeated git fetch failure, giving up" >&2
+ exit 1
+ fi
+}
+
+# Get the content of "$2" from cache commit "$1"
+cat_from_cache() {
+ git_cache cat-file blob "$1:$2"
+}
+
+# Consistency checking: for a given cache commit "$1", check if it contains a
+# file "$2" which is equal to the file "$3" present in the working tree.
+cmp_from_cache() {
+ cat_from_cache "$1" "$2" | cmp "$3"
+}
+
+# Like `git clone` except that it uses the original origin url and supports
+# checking out commit IDs as detached heads. The target directory must either
+# be empty, or not exist.
+clone_from_cache() {
+ message CLONE "${SUBDIR} [ref: $1]"
+ [ ! -e "${SUBDIR}" ] || rmdir "${SUBDIR}"
+ mkdir "${SUBDIR}"
+ cp -a --reflink=auto "${CACHE_DIR}" "${SUBDIR}/.git"
+ git --git-dir "${SUBDIR}/.git" config --unset core.bare
+ git -c advice.detachedHead=false -C "${SUBDIR}" checkout ${quiet} "$1"
+}
+
+# This stores a .tar file from stdin into the cache as a tree object.
+# Returns the ID. Opposite of `git archive`, basically.
+tar_to_cache() {
+ # Need to do this before we set the GIT_* variables
+ init_cache
+
+ # Use a sub-shell to enable cleanup of the temporary directory
+ (
+ tmpdir="$(mktemp --tmpdir --directory cockpit-tar-to-git.XXXXXX)"
+ # shellcheck disable=SC2064 # we want ${tmpdir} expanded now
+ trap "rm -r '${tmpdir}'" EXIT
+
+ export GIT_INDEX_FILE="${tmpdir}/tmp-index"
+ export GIT_WORK_TREE="${tmpdir}/work"
+
+ mkdir "${GIT_WORK_TREE}"
+ cd "${GIT_WORK_TREE}"
+
+ tar --extract --exclude '.git*'
+ message INDEX "${SUBDIR}"
+ git_cache add --all
+ git_cache write-tree
+ )
+}
+
+ # Small helper to run a git command on the cache directory
+cmd_git() {
+ git_cache "$@"
+}
diff --git a/test/common/lcov.py b/test/common/lcov.py
new file mode 100755
index 0000000..7d2fec5
--- /dev/null
+++ b/test/common/lcov.py
@@ -0,0 +1,505 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/pywrap", sys.argv)
+
+# This file is part of Cockpit.
+#
+# Copyright (C) 2022 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+# This module can convert profile data from CDP to LCOV, produce a
+# HTML report, and post review comments.
+#
+# - write_lcov (coverage_data, outlabel)
+# - create_coverage_report()
+
+import glob
+import gzip
+import itertools
+import json
+import os
+import re
+import shutil
+import subprocess
+import sys
+from bisect import bisect_left
+
+from task import github
+
+BASE_DIR = os.path.realpath(f'{__file__}/../../..')
+
+debug = False
+
+# parse_vlq and parse_sourcemap are based on
+# https://github.com/mattrobenolt/python-sourcemap, licensed with
+# "BSD-2-Clause License"
+
+# Mapping of base64 letter -> integer value.
+B64 = {c: i for i, c in
+ enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
+ '0123456789+/')}
+
+
+def parse_vlq(segment):
+ """Parse a string of VLQ-encoded data.
+ Returns:
+ a list of integers.
+ """
+
+ values = []
+
+ cur, shift = 0, 0
+ for c in segment:
+ val = B64[c]
+ # Each character is 6 bits:
+ # 5 of value and the high bit is the continuation.
+ val, cont = val & 0b11111, val >> 5
+ cur += val << shift
+ shift += 5
+
+ if not cont:
+ # The low bit of the unpacked value is the sign.
+ cur, sign = cur >> 1, cur & 1
+ if sign:
+ cur = -cur
+ values.append(cur)
+ cur, shift = 0, 0
+
+ if cur or shift:
+ raise Exception('leftover cur/shift in vlq decode')
+
+ return values
+
+
+def parse_sourcemap(f, line_starts, dir_name):
+ smap = json.load(f)
+ sources = smap['sources']
+ mappings = smap['mappings']
+ lines = mappings.split(';')
+
+ our_map = []
+
+ our_sources = set()
+ for s in sources:
+ if "node_modules" not in s and (s.endswith(('.js', '.jsx'))):
+ our_sources.add(s)
+
+ dst_col, src_id, src_line = 0, 0, 0
+ for dst_line, line in enumerate(lines):
+ segments = line.split(',')
+ dst_col = 0
+ for segment in segments:
+ if not segment:
+ continue
+ parse = parse_vlq(segment)
+ dst_col += parse[0]
+
+ src = None
+ if len(parse) > 1:
+ src_id += parse[1]
+ src = sources[src_id]
+ src_line += parse[2]
+
+ if src in our_sources:
+ norm_src = os.path.normpath(os.path.join(dir_name, src))
+ our_map.append((line_starts[dst_line] + dst_col, norm_src, src_line))
+
+ return our_map
+
+
+class DistFile:
+ def __init__(self, path):
+ line_starts = [0]
+ with open(path, newline='') as f:
+ for line in f.readlines():
+ line_starts.append(line_starts[-1] + len(line))
+ with open(path + ".map") as f:
+ self.smap = parse_sourcemap(f, line_starts, os.path.relpath(os.path.dirname(path), BASE_DIR))
+
+ def find_sources_slow(self, start, end):
+ res = []
+ for m in self.smap:
+ if m[0] >= start and m[0] < end:
+ res.append(m)
+ return res
+
+ def find_sources(self, start, end):
+ res = []
+ i = bisect_left(self.smap, start, key=lambda m: m[0])
+ while i < len(self.smap) and self.smap[i][0] < end:
+ res.append(self.smap[i])
+ i += 1
+ if debug and res != self.find_sources_slow(start, end):
+ raise RuntimeError("Bug in find_sources")
+ return res
+
+
+def get_dist_map(package):
+ dmap = {}
+ for manifest_json in glob.glob(f"{BASE_DIR}/dist/*/manifest.json") + glob.glob(f"{BASE_DIR}/dist/manifest.json"):
+ with open(manifest_json) as f:
+ m = json.load(f)
+ if "name" in m:
+ dmap[m["name"]] = os.path.dirname(manifest_json)
+ elif manifest_json == f"{BASE_DIR}/dist/manifest.json":
+ if "name" in package:
+ dmap[package["name"]] = os.path.dirname(manifest_json)
+ return dmap
+
+
+def get_distfile(url, dist_map):
+ parts = url.split("/")
+ if len(parts) < 3 or "cockpit" not in parts:
+ return None
+
+ base = parts[-2]
+ file = parts[-1]
+ if file == "manifests.js":
+ return None
+ if base in dist_map:
+ path = dist_map[base] + "/" + file
+ else:
+ path = f"{BASE_DIR}/dist/" + base + "/" + file
+ if os.path.exists(path) and os.path.exists(path + ".map"):
+ return DistFile(path)
+ else:
+ sys.stderr.write(f"SKIP {url} -> {path}\n")
+ return None
+
+
+def grow_array(arr, size, val):
+ if len(arr) < size:
+ arr.extend([val] * (size - len(arr)))
+
+
+def record_covered(file_hits, src, line, hits):
+ if src in file_hits:
+ line_hits = file_hits[src]
+ else:
+ line_hits = []
+ grow_array(line_hits, line + 1, None)
+ line_hits[line] = hits
+ file_hits[src] = line_hits
+
+
+def record_range(file_hits, r, distfile):
+ sources = distfile.find_sources(r['startOffset'], r['endOffset'])
+ for src in sources:
+ record_covered(file_hits, src[1], src[2], r['count'])
+
+
+def merge_hits(file_hits, hits):
+ for src in hits:
+ if src not in file_hits:
+ file_hits[src] = hits[src]
+ else:
+ lines = file_hits[src]
+ merge_lines = hits[src]
+ grow_array(lines, len(merge_lines), None)
+ for i in range(len(merge_lines)):
+ if lines[i] is None:
+ lines[i] = merge_lines[i]
+ elif merge_lines[i] is not None:
+ lines[i] += merge_lines[i]
+
+
+def print_file_coverage(path, line_hits, out):
+ lines_found = 0
+ lines_hit = 0
+ src = f"{BASE_DIR}/{path}"
+ out.write(f"SF:{src}\n")
+ for i in range(len(line_hits)):
+ if line_hits[i] is not None:
+ lines_found += 1
+ out.write(f"DA:{i + 1},{line_hits[i]}\n")
+ if line_hits[i] > 0:
+ lines_hit += 1
+ out.write(f"LH:{lines_hit}\n")
+ out.write(f"LF:{lines_found}\n")
+ out.write("end_of_record\n")
+
+
+class DiffMap:
+ # Parse a unified diff and make a index for the added lines
+ def __init__(self, diff):
+ self.map = {}
+ self.source_map = {}
+ plus_name = None
+ diff_line = 0
+ with open(diff) as f:
+ for line in f.readlines():
+ diff_line += 1
+ if line.startswith("+++ /dev/null"):
+ # removed file, only `^-` following after that until the next hunk
+ continue
+ elif line.startswith("+++ b/"):
+ plus_name = os.path.normpath(line[6:].strip())
+ plus_line = 1
+ self.map[plus_name] = {}
+ elif line.startswith("@@ "):
+ plus_line = int(line.split(" ")[2].split(",")[0])
+ elif line.startswith(" "):
+ plus_line += 1
+ elif line.startswith("+"):
+ self.map[plus_name][plus_line] = diff_line
+ self.source_map[diff_line] = (plus_name, plus_line, line[1:])
+ plus_line += 1
+
+ def find_line(self, file, line):
+ if file in self.map and line in self.map[file]:
+ return self.map[file][line]
+ return None
+
+ def find_source(self, diff_line):
+ return self.source_map.get(diff_line)
+
+
+def print_diff_coverage(path, file_hits, out):
+ if not os.path.exists(path):
+ return
+ dm = DiffMap(path)
+ src = f"{BASE_DIR}/{path}"
+ lines_found = 0
+ lines_hit = 0
+ out.write(f"SF:{src}\n")
+ for f in file_hits:
+ line_hits = file_hits[f]
+ for i in range(len(line_hits)):
+ if line_hits[i] is not None:
+ diff_line = dm.find_line(f, i + 1)
+ if diff_line:
+ lines_found += 1
+ out.write(f"DA:{diff_line},{line_hits[i]}\n")
+ if line_hits[i] > 0:
+ lines_hit += 1
+ out.write(f"LH:{lines_hit}\n")
+ out.write(f"LF:{lines_found}\n")
+ out.write("end_of_record\n")
+
+
+def write_lcov(covdata, outlabel):
+
+ with open(f"{BASE_DIR}/package.json") as f:
+ package = json.load(f)
+ dist_map = get_dist_map(package)
+ file_hits = {}
+
+ def covranges(functions):
+ for f in functions:
+ for r in f['ranges']:
+ yield r
+
+ # Coverage data is reported as a "count" value for a range of
+ # text. These ranges overlap when functions are nested. For
+ # example, take this source code:
+ #
+ # 1 . function foo(x) {
+ # 2 . function bar() {
+ # 3 . }
+ # 4 . if (x)
+ # 5 . bar();
+ # 6 . }
+ # 7 .
+ # 8 . foo(0)
+ #
+ # There will be a range with count 1 for the whole source code
+ # (lines 1 to 8) since all code is executed when loading a file.
+ # Then there will be a range with count 1 for "foo" (lines 1 to 6)
+ # since it is called from the top-level, and there will be a range
+ # with count 0 for "bar" (lines 2 and 3), since it is never
+ # actually called. If block-level precision has been enabled
+ # while collecting the coverage data, there will also be a range
+ # with count 0 for line 5, since that branch if the "if" is not
+ # executed.
+ #
+ # We process ranges like this in order, from longest to shortest,
+ # and record their counts for each line they cover. The count of a
+ # range that is processed later will overwrite any count that has
+ # been recorded earlier. This makes the count correct for nested
+ # functions since they are processed last.
+ #
+ # In the example, first lines 1 to 8 are set to count 1, then
+ # lines 1 to 6 are set to count 1 again, then lines 2 and 3 are
+ # set to count 0, and finally line 5 is also set to count 0:
+ #
+ # 1 1 function foo(x) {
+ # 2 0 function bar() {
+ # 3 0 }
+ # 4 1 if (x)
+ # 5 0 bar();
+ # 6 1 }
+ # 7 1
+ # 8 1 foo(0)
+ #
+ # Thus, when processing ranges for a single file, we must
+ # prioritize the counts of smaller ranges over larger ones, and
+ # can't just add them all up. This doesn't work, however, when
+ # something like webpack is involved, and a source file is copied
+ # into multiple files in "dist/".
+ #
+ # The coverage data contains ranges for all files that are loaded
+ # into the browser during the whole session, such as when
+ # transitioning from the login page to the shell, and when loading
+ # multiple iframes for the individual pages.
+ #
+ # For example, if both shell.js (loaded at the top-level) and
+ # overview.js (loaded into an iframe) include lib/button.js, then
+ # the coverage data might report that shell.js does execute line 5
+ # of lib/button.js and also that overview.js does not execute it.
+ # We need to add the counts up for line 5 so that the combined
+ # report says that is has been executed.
+ #
+ # The same applies to reloading and navigating in the browser. If
+ # a page is reloaded, there will be separate coverage reports for
+ # its files. For example, if a reload happens, shell.js will be
+ # mentioned twice in the report, and we need to add up the counts
+ # from each mention.
+
+ for script in covdata:
+ distfile = get_distfile(script['url'], dist_map)
+ if distfile:
+ ranges = sorted(covranges(script['functions']),
+ key=lambda r: r['endOffset'] - r['startOffset'], reverse=True)
+ hits = {}
+ for r in ranges:
+ record_range(hits, r, distfile)
+ merge_hits(file_hits, hits)
+
+ if len(file_hits) > 0:
+ os.makedirs(f"{BASE_DIR}/lcov", exist_ok=True)
+ filename = f"{BASE_DIR}/lcov/{outlabel}.info.gz"
+ with gzip.open(filename, "wt") as out:
+ for f in file_hits:
+ print_file_coverage(f, file_hits[f], out)
+ print_diff_coverage("lcov/github-pr.diff", file_hits, out)
+ print("Wrote coverage data to " + filename)
+
+
+def get_review_comments(diff_info_file):
+ comments = []
+ cur_src = None
+ start_line = None
+ cur_line = None
+
+ def is_interesting_line(text):
+ # Don't complain when being told to shut up
+ if "// not-covered: " in text:
+ return False
+ # Don't complain about lines that contain only punctuation, or
+ # nothing but "else". We don't seem to get reliable
+ # information for them.
+ if not re.search('[a-zA-Z0-9]', text.replace("else", "")):
+ return False
+ return True
+
+ def flush_cur_comment():
+ nonlocal comments
+ if cur_src:
+ ta_url = os.environ.get("TEST_ATTACHMENTS_URL", None)
+ comment = {"path": cur_src,
+ "line": cur_line}
+ if start_line != cur_line:
+ comment["start_line"] = start_line
+ body = f"These {cur_line - start_line + 1} added lines are not executed by any test."
+ else:
+ body = "This added line is not executed by any test."
+ if ta_url:
+ body += f" [Details]({ta_url}/Coverage/lcov/github-pr.diff.gcov.html)"
+ comment["body"] = body
+ comments.append(comment)
+
+ dm = DiffMap("lcov/github-pr.diff")
+
+ with open(diff_info_file) as f:
+ for line in f.readlines():
+ if line.startswith("DA:"):
+ parts = line[3:].split(",")
+ if int(parts[1]) == 0:
+ info = dm.find_source(int(parts[0]))
+ if not info:
+ continue
+ (src, line, text) = info
+ if not is_interesting_line(text):
+ continue
+ if src == cur_src and line == cur_line + 1:
+ cur_line = line
+ else:
+ flush_cur_comment()
+ cur_src = src
+ start_line = line
+ cur_line = line
+ flush_cur_comment()
+
+ return comments
+
+
+def prepare_for_code_coverage():
+ # This gives us a convenient link at the top of the logs, see link-patterns.json
+ print("Code coverage report in Coverage/index.html")
+ if os.path.exists("lcov"):
+ shutil.rmtree("lcov")
+ os.makedirs("lcov")
+ # Detect the default branch to compare with, Anaconda still uses master as main.
+ branch = "main"
+ try:
+ subprocess.check_call(["git", "rev-parse", "--quiet", "--verify", branch], stdout=subprocess.DEVNULL)
+ except subprocess.SubprocessError:
+ branch = "master"
+ with open("lcov/github-pr.diff", "w") as f:
+ subprocess.check_call(["git", "-c", "diff.noprefix=false", "diff", "--patience", branch], stdout=f)
+
+
+def create_coverage_report():
+ output = os.environ.get("TEST_ATTACHMENTS", BASE_DIR)
+ lcov_files = glob.glob(f"{BASE_DIR}/lcov/*.info.gz")
+ try:
+ title = os.path.basename(subprocess.check_output(["git", "remote", "get-url", "origin"])).decode().strip()
+ except subprocess.CalledProcessError:
+ title = "?"
+ if len(lcov_files) > 0:
+ all_file = f"{BASE_DIR}/lcov/all.info"
+ diff_file = f"{BASE_DIR}/lcov/diff.info"
+ excludes = []
+ # Exclude pkg/lib in Cockpit projects such as podman/machines.
+ if title != "cockpit.git":
+ excludes = ["--exclude", "pkg/lib"]
+ subprocess.check_call(["lcov", "--quiet", "--output", all_file, *excludes,
+ *itertools.chain(*[["--add", f] for f in lcov_files])])
+ subprocess.check_call(["lcov", "--quiet", "--ignore-errors", "empty,empty,unused,unused", "--output", diff_file,
+ "--extract", all_file, "*/github-pr.diff"])
+ summary = subprocess.check_output(["genhtml", "--no-function-coverage",
+ "--prefix", os.getcwd(),
+ "--title", title,
+ "--output-dir", f"{output}/Coverage", all_file]).decode()
+
+ coverage = summary.split("\n")[-2]
+ match = re.search(r".*lines\.*:\s*([\d\.]*%).*", coverage)
+ if match:
+ print("Overall line coverage:", match.group(1))
+
+ comments = get_review_comments(diff_file)
+ rev = os.environ.get("TEST_REVISION", None)
+ pull = os.environ.get("TEST_PULL", None)
+ if rev and pull:
+ api = github.GitHub()
+ old_comments = api.get(f"pulls/{pull}/comments?sort=created&direction=desc&per_page=100") or []
+ for oc in old_comments:
+ if ("body" in oc and "path" in oc and "line" in oc and
+ "not executed by any test." in oc["body"]):
+ api.delete(f"pulls/comments/{oc['id']}")
+ if len(comments) > 0:
+ api.post(f"pulls/{pull}/reviews",
+ {"commit_id": rev, "event": "COMMENT",
+ "comments": comments})
+ else:
+ sys.stderr.write("Error: no code coverage files generated\n")
diff --git a/test/common/link-patterns.json b/test/common/link-patterns.json
new file mode 100644
index 0000000..cef6c78
--- /dev/null
+++ b/test/common/link-patterns.json
@@ -0,0 +1,39 @@
+[
+ {
+ "label": "screenshot",
+ "pattern": "Wrote screenshot to ([A-Za-z0-9.-]+.png)$",
+ "url": "$1",
+ "icon": "bi bi-camera-fill"
+ },
+ {
+ "label": "new pixels",
+ "pattern": "New pixel test reference ([A-Za-z0-9.-]+.png)$",
+ "url": "$1"
+ },
+ {
+ "label": "journal",
+ "pattern": "Journal extracted to ([A-Za-z0-9.-]+.log(?:.[gx]z)?)$",
+ "url": "$1",
+ "icon": "bi bi-card-text"
+ },
+ {
+ "label": "changed pixels",
+ "pattern": "Differences in pixel test ([A-Za-z0-9.-]+)$",
+ "url": "pixeldiff.html#$1"
+ },
+ {
+ "label": "coverage",
+ "pattern": "Code coverage report in ([A-Za-z0-9.-]+)$",
+ "url": "$1/"
+ },
+ {
+ "label": "vm xml",
+ "pattern": "Wrote ([A-Za-z0-9.-]+) XML to ([A-Za-z0-9.-]+.xml)$",
+ "url": "$2"
+ },
+ {
+ "label": "vm log",
+ "pattern": "Wrote ([A-Za-z0-9.-]+) log to ([A-Za-z0-9.-]+.log)$",
+ "url": "$2"
+ }
+]
diff --git a/test/common/make-bots b/test/common/make-bots
new file mode 100755
index 0000000..5160e90
--- /dev/null
+++ b/test/common/make-bots
@@ -0,0 +1,28 @@
+#!/bin/sh
+
+# Prepare bots by creating ./bots directory
+# Specify $COCKPIT_BOTS_REF to checkout non-main branch
+
+GITHUB_REPO='bots'
+SUBDIR='bots'
+
+V="${V-0}" # default to friendly messages
+
+set -eu
+cd "${0%/*}/../.."
+# shellcheck source-path=SCRIPTDIR/../..
+. test/common/git-utils.sh
+
+if [ ! -e bots ]; then
+ [ -n "${quiet}" ] || set -x
+ if [ -h ~/.config/cockpit-dev/bots ]; then
+ message SYMLINK "bots → $(realpath --relative-to=. ~/.config/cockpit-dev/bots)"
+ ln -sfT "$(realpath --relative-to=. ~/.config/cockpit-dev)/bots" bots
+ else
+ # it's small, so keep everything cached
+ fetch_to_cache ${COCKPIT_BOTS_REF+"${COCKPIT_BOTS_REF}"}
+ clone_from_cache "${COCKPIT_BOTS_REF-main}"
+ fi
+else
+ echo "bots/ already exists, skipping"
+fi
diff --git a/test/common/netlib.py b/test/common/netlib.py
new file mode 100644
index 0000000..e9e868f
--- /dev/null
+++ b/test/common/netlib.py
@@ -0,0 +1,214 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import re
+import subprocess
+
+from testlib import Error, MachineCase, wait
+
+
+class NetworkHelpers:
+ """Mix-in class for tests that require network setup"""
+
+ def add_veth(self, name, dhcp_cidr=None, dhcp_range=None):
+ """Add a veth device that is manageable with NetworkManager
+
+ This is safe for @nondestructive tests, the interface gets cleaned up automatically.
+ """
+ if dhcp_range is None:
+ dhcp_range = ['10.111.112.2', '10.111.127.254']
+ self.machine.execute(r"""
+ mkdir -p /run/udev/rules.d/
+ echo 'ENV{ID_NET_DRIVER}=="veth", ENV{INTERFACE}=="%(name)s", ENV{NM_UNMANAGED}="0"' > /run/udev/rules.d/99-nm-veth-%(name)s-test.rules
+ udevadm control --reload
+ ip link add name %(name)s type veth peer name v_%(name)s
+ # Trigger udev to make sure that it has been renamed to its final name
+ udevadm trigger --subsystem-match=net
+ udevadm settle
+ """ % {"name": name})
+ self.addCleanup(self.machine.execute, f"rm /run/udev/rules.d/99-nm-veth-{name}-test.rules; ip link del dev {name}")
+ if dhcp_cidr:
+ # up the remote end, give it an IP, and start DHCP server
+ self.machine.execute(f"ip a add {dhcp_cidr} dev v_{name}; ip link set v_{name} up")
+ server = self.machine.spawn("dnsmasq --keep-in-foreground --log-queries --log-facility=- "
+ f"--conf-file=/dev/null --dhcp-leasefile=/tmp/leases.{name} --no-resolv "
+ f"--bind-interfaces --except-interface=lo --interface=v_{name} --dhcp-range={dhcp_range[0]},{dhcp_range[1]},4h",
+ f"dhcp-{name}.log")
+ self.addCleanup(self.machine.execute, "kill %i" % server)
+ self.machine.execute("if firewall-cmd --state >/dev/null 2>&1; then firewall-cmd --add-service=dhcp; fi")
+
+ def nm_activate_eth(self, iface):
+ """Create an NM connection for a given interface"""
+
+ m = self.machine
+ wait(lambda: m.execute(f'nmcli device | grep "{iface}.*disconnected"'))
+ m.execute(f"nmcli con add type ethernet ifname {iface} con-name {iface}")
+ m.execute(f"nmcli con up {iface} ifname {iface}")
+ self.addCleanup(m.execute, f"nmcli con delete {iface}")
+
+ def nm_checkpoints_disable(self):
+ self.browser.eval_js("window.cockpit_tests_disable_checkpoints = true;")
+
+ def nm_checkpoints_enable(self, settle_time=3.0):
+ self.browser.eval_js("window.cockpit_tests_disable_checkpoints = false;")
+ self.browser.eval_js(f"window.cockpit_tests_checkpoint_settle_time = {settle_time};")
+
+
+class NetworkCase(MachineCase, NetworkHelpers):
+ def setUp(self):
+ super().setUp()
+
+ m = self.machine
+
+ # clean up after nondestructive tests
+ if self.is_nondestructive():
+ def devs():
+ return set(self.machine.execute("ls /sys/class/net/ | grep -v bonding_masters").strip().split())
+
+ def cleanupDevs():
+ new = devs() - self.orig_devs
+ self.machine.execute(f"for d in {' '.join(new)}; do nmcli dev del $d; done")
+
+ self.orig_devs = devs()
+ self.restore_dir("/etc/NetworkManager", restart_unit="NetworkManager")
+ self.restore_dir("/etc/sysconfig/network-scripts")
+ self.restore_dir("/etc/netplan")
+ self.restore_dir("/run/NetworkManager/system-connections")
+ self.addCleanup(cleanupDevs)
+
+ m.execute("systemctl start NetworkManager")
+
+ # Ensure a clean and consistent state. We remove rogue
+ # connections that might still be here from the time of
+ # creating the image and we prevent NM from automatically
+ # creating new connections.
+ # if the command fails, try again
+ failures_allowed = 3
+ while True:
+ try:
+ print(m.execute("nmcli con show"))
+ m.execute(
+ """nmcli -f UUID,DEVICE connection show | awk '$2 == "--" { print $1 }' | xargs -r nmcli con del""")
+ break
+ except subprocess.CalledProcessError:
+ failures_allowed -= 1
+ if failures_allowed == 0:
+ raise
+
+ m.write("/etc/NetworkManager/conf.d/99-test.conf", "[main]\nno-auto-default=*\n")
+ m.execute("systemctl reload-or-restart NetworkManager")
+
+ # our assertions and pixel tests assume that virbr0 is absent
+ m.execute('[ -z "$(systemctl --legend=false list-unit-files libvirtd.service)" ] || '
+ 'systemctl try-restart libvirtd.service')
+ if 'default' in m.execute("virsh net-list --name || true"):
+ m.execute("virsh net-autostart --disable default; virsh net-destroy default")
+
+ ver = self.machine.execute(
+ "busctl --system get-property org.freedesktop.NetworkManager /org/freedesktop/NetworkManager org.freedesktop.NetworkManager Version || true")
+ ver_match = re.match('s "(.*)"', ver)
+ if ver_match:
+ self.networkmanager_version = [int(x) for x in ver_match.group(1).split(".")]
+ else:
+ self.networkmanager_version = [0]
+
+ # Something unknown sometimes goes wrong with PCP, see #15625
+ self.allow_journal_messages("pcp-archive: no such metric: network.interface.* Unknown metric name",
+ "direct: instance name lookup failed: network.*")
+
+ def get_iface(self, m, mac):
+ def getit():
+ path = m.execute(f"grep -li '{mac}' /sys/class/net/*/address")
+ return path.split("/")[-2]
+ iface = wait(getit).strip()
+ print(f"{mac} -> {iface}")
+ return iface
+
+ def add_iface(self, activate=True):
+ m = self.machine
+ mac = m.add_netiface(networking=self.network.interface())
+ # Wait for the interface to show up
+ self.get_iface(m, mac)
+ # Trigger udev to make sure that it has been renamed to its final name
+ m.execute("udevadm trigger; udevadm settle")
+ iface = self.get_iface(m, mac)
+ if activate:
+ self.nm_activate_eth(iface)
+ return iface
+
+ def wait_for_iface(self, iface, active=True, state=None, prefix="10.111."):
+ sel = f"#networking-interfaces tr[data-interface='{iface}']"
+
+ if state:
+ text = state
+ elif active:
+ text = prefix
+ else:
+ text = "Inactive"
+
+ try:
+ with self.browser.wait_timeout(30):
+ self.browser.wait_in_text(sel, text)
+ except Error as e:
+ print(f"Interface {iface} didn't show up.")
+ print(self.machine.execute(f"grep . /sys/class/net/*/address; nmcli con; nmcli dev; nmcli dev show {iface} || true"))
+ raise e
+
+ def select_iface(self, iface):
+ b = self.browser
+ b.click(f"#networking-interfaces tr[data-interface='{iface}'] button")
+
+ def iface_con_id(self, iface):
+ con_id = self.machine.execute(f"nmcli -m tabular -t -f GENERAL.CONNECTION device show {iface}").strip()
+ if con_id == "" or con_id == "--":
+ return None
+ else:
+ return con_id
+
+ def wait_for_iface_setting(self, setting_title, setting_value):
+ b = self.browser
+ b.wait_in_text(f"dt:contains('{setting_title}') + dd", setting_value)
+
+ def configure_iface_setting(self, setting_title):
+ b = self.browser
+ b.click(f"dt:contains('{setting_title}') + dd button")
+
+ def ensure_nm_uses_dhclient(self):
+ m = self.machine
+ m.write("/etc/NetworkManager/conf.d/99-dhcp.conf", "[main]\ndhcp=dhclient\n")
+ m.execute("systemctl restart NetworkManager")
+
+ def slow_down_dhclient(self, delay):
+ self.machine.execute(f"""
+ mkdir -p {self.vm_tmpdir}
+ cp -a /usr/sbin/dhclient {self.vm_tmpdir}/dhclient.real
+ printf '#!/bin/sh\\nsleep {delay}\\nexec {self.vm_tmpdir}/dhclient.real "$@"' > {self.vm_tmpdir}/dhclient
+ chmod a+x {self.vm_tmpdir}/dhclient
+ if selinuxenabled 2>&1; then chcon --reference /usr/sbin/dhclient {self.vm_tmpdir}/dhclient; fi
+ mount -o bind {self.vm_tmpdir}/dhclient /usr/sbin/dhclient
+ """)
+ self.addCleanup(self.machine.execute, "umount /usr/sbin/dhclient")
+
+ def wait_onoff(self, sel, val):
+ self.browser.wait_visible(sel + " input[type=checkbox]" + (":checked" if val else ":not(:checked)"))
+
+ def toggle_onoff(self, sel):
+ self.browser.click(sel + " input[type=checkbox]")
+
+ def login_and_go(self, *args, **kwargs):
+ super().login_and_go(*args, **kwargs)
+ self.nm_checkpoints_disable()
diff --git a/test/common/packagelib.py b/test/common/packagelib.py
new file mode 100644
index 0000000..ed2fe89
--- /dev/null
+++ b/test/common/packagelib.py
@@ -0,0 +1,428 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import logging
+import os
+import textwrap
+
+from testlib import MachineCase
+
+
+class PackageCase(MachineCase):
+ def setUp(self):
+ super().setUp()
+
+ self.repo_dir = os.path.join(self.vm_tmpdir, "repo")
+
+ if self.machine.ostree_image:
+ logging.warning("PackageCase: OSTree images can't install additional packages")
+ return
+
+ # expected backend; hardcode this on image names to check the auto-detection
+ if self.machine.image.startswith("debian") or self.machine.image.startswith("ubuntu"):
+ self.backend = "apt"
+ self.primary_arch = "all"
+ self.secondary_arch = "amd64"
+ elif self.machine.image.startswith("fedora") or self.machine.image.startswith("rhel-") or self.machine.image.startswith("centos-"):
+ self.backend = "dnf"
+ self.primary_arch = "noarch"
+ self.secondary_arch = "x86_64"
+ elif self.machine.image == "arch":
+ self.backend = "alpm"
+ self.primary_arch = "any"
+ self.secondary_arch = "x86_64"
+ else:
+ raise NotImplementedError("unknown image " + self.machine.image)
+
+ if "debian" in self.image or "ubuntu" in self.image:
+ # PackageKit refuses to work when offline, and main interface is not managed by NM on these images
+ self.machine.execute("nmcli con add type dummy con-name fake ifname fake0 ip4 1.2.3.4/24 gw4 1.2.3.1")
+ self.addCleanup(self.machine.execute, "nmcli con delete fake")
+
+ # HACK: packagekit often hangs on shutdown; https://bugzilla.redhat.com/show_bug.cgi?id=1717185
+ self.write_file("/etc/systemd/system/packagekit.service.d/timeout.conf", "[Service]\nTimeoutStopSec=5\n")
+ self.addCleanup(self.machine.execute, "systemctl stop packagekit; systemctl reset-failed packagekit || true")
+
+ # disable all existing repositories to avoid hitting the network
+ if self.backend == "apt":
+ self.restore_dir("/var/lib/apt", reboot_safe=True)
+ self.restore_dir("/var/cache/apt", reboot_safe=True)
+ self.restore_dir("/etc/apt", reboot_safe=True)
+ self.machine.execute("echo > /etc/apt/sources.list; rm -f /etc/apt/sources.list.d/*; apt-get clean; apt-get update")
+ elif self.backend == "alpm":
+ self.restore_dir("/var/lib/pacman", reboot_safe=True)
+ self.restore_dir("/var/cache/pacman", reboot_safe=True)
+ self.restore_dir("/etc/pacman.d", reboot_safe=True)
+ self.restore_dir("/var/lib/PackageKit/alpm", reboot_safe=True)
+ self.restore_file("/etc/pacman.conf")
+ self.restore_file("/etc/pacman.d/mirrorlist")
+ self.restore_file("/usr/share/libalpm/hooks/90-packagekit-refresh.hook")
+
+ self.machine.execute("rm /etc/pacman.conf /etc/pacman.d/mirrorlist /var/lib/pacman/sync/* /usr/share/libalpm/hooks/90-packagekit-refresh.hook")
+ self.machine.execute("test -d /var/lib/PackageKit/alpm && rm -r /var/lib/PackageKit/alpm || true") # Drop alpm state directory as it interferes with running offline
+ # Initial config for installation
+ empty_repo_dir = '/var/lib/cockpittest/empty'
+ config = f"""
+[options]
+Architecture = auto
+HoldPkg = pacman glibc
+
+[empty]
+SigLevel = Never
+Server = file://{empty_repo_dir}
+"""
+ # HACK: Setup empty repo for packagekit
+ self.machine.execute(f"mkdir -p {empty_repo_dir} || true")
+ self.machine.execute(f"repo-add {empty_repo_dir}/empty.db.tar.gz")
+ self.machine.write("/etc/pacman.conf", config)
+ # Clean up possible leftover lockfile
+ self.machine.execute("""
+ if [ -f /var/lib/pacman/db.lck ]; then
+ fuser -k /var/lib/pacman/db.lck || true;
+ rm /var/lib/pacman/db.lck;
+ fi
+ """)
+ self.machine.execute("pacman -Sy")
+ else:
+ self.restore_dir("/etc/yum.repos.d", reboot_safe=True)
+ self.restore_dir("/var/cache/dnf", reboot_safe=True)
+ self.machine.execute("rm -rf /etc/yum.repos.d/* /var/cache/dnf/*")
+
+ # have PackageKit start from a clean slate
+ self.machine.execute("systemctl stop packagekit")
+ self.machine.execute("systemctl kill --signal=SIGKILL packagekit || true; rm -rf /var/cache/PackageKit")
+ self.machine.execute("systemctl reset-failed packagekit || true")
+ self.restore_file("/var/lib/PackageKit/transactions.db")
+
+ if self.image in ["debian-stable", "debian-testing"]:
+ # PackageKit tries to resolve some DNS names, but our test VM is offline; temporarily disable the name server to fail quickly
+ self.machine.execute("mv /etc/resolv.conf /etc/resolv.conf.test")
+ self.addCleanup(self.machine.execute, "mv /etc/resolv.conf.test /etc/resolv.conf")
+
+ # reset automatic updates
+ if self.backend == 'dnf':
+ self.machine.execute("systemctl disable --now dnf-automatic dnf-automatic-install "
+ "dnf-automatic.service dnf-automatic-install.timer")
+ self.machine.execute("rm -r /etc/systemd/system/dnf-automatic* && systemctl daemon-reload || true")
+
+ self.updateInfo = {}
+
+ # HACK: kpatch check sometimes complains that we don't set up a full repo in unrelated tests
+ self.allow_browser_errors("Could not determine kpatch packages:.*repodata updates was not complete")
+
+ #
+ # Helper functions for creating packages/repository
+ #
+
+ def createPackage(self, name, version, release, install=False,
+ postinst=None, depends="", content=None, arch=None, provides=None, **updateinfo):
+ """Create a dummy package in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ if provides:
+ provides = f"Provides: {provides}"
+ else:
+ provides = ""
+
+ if self.backend == "apt":
+ self.createDeb(name, version + '-' + release, depends, postinst, install, content, arch, provides)
+ elif self.backend == "alpm":
+ self.createPacmanPkg(name, version, release, depends, postinst, install, content, arch, provides)
+ else:
+ self.createRpm(name, version, release, depends, postinst, install, content, arch, provides)
+ if updateinfo:
+ self.updateInfo[(name, version, release)] = updateinfo
+
+ def createDeb(self, name, version, depends, postinst, install, content, arch, provides):
+ """Create a dummy deb in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ m = self.machine
+
+ if arch is None:
+ arch = self.primary_arch
+ deb = f"{self.repo_dir}/{name}_{version}_{arch}.deb"
+ if postinst:
+ postinstcode = f"printf '#!/bin/sh\n{postinst}' > /tmp/b/DEBIAN/postinst; chmod 755 /tmp/b/DEBIAN/postinst"
+ else:
+ postinstcode = ''
+ if content is not None:
+ for path, data in content.items():
+ dest = "/tmp/b/" + path
+ m.execute(f"mkdir -p '{os.path.dirname(dest)}'")
+ if isinstance(data, dict):
+ m.execute(f"cp '{data['path']}' '{dest}'")
+ else:
+ m.write(dest, data)
+ m.execute(f"mkdir -p {self.repo_dir}")
+ m.write("/tmp/b/DEBIAN/control", textwrap.dedent(f"""
+ Package: {name}
+ Version: {version}
+ Priority: optional
+ Section: test
+ Maintainer: foo
+ Depends: {depends}
+ Architecture: {arch}
+ Description: dummy {name}
+ {provides}
+ """))
+
+ cmd = f"""set -e
+ {postinstcode}
+ touch /tmp/b/stamp-{name}-{version}
+ dpkg -b /tmp/b {deb}
+ rm -r /tmp/b
+ """
+ if install:
+ cmd += "dpkg -i " + deb
+ m.execute(cmd)
+ self.addCleanup(m.execute, f"dpkg -P --force-depends --force-remove-reinstreq {name} 2>/dev/null || true")
+
+ def createRpm(self, name, version, release, requires, post, install, content, arch, provides):
+ """Create a dummy rpm in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+ if post:
+ postcode = '\n%%post\n' + post
+ else:
+ postcode = ''
+ if requires:
+ requires = f"Requires: {requires}\n"
+ if arch is None:
+ arch = self.primary_arch
+ installcmds = f"touch $RPM_BUILD_ROOT/stamp-{name}-{version}-{release}\n"
+ installedfiles = f"/stamp-{name}-{version}-{release}\n"
+ if content is not None:
+ for path, data in content.items():
+ installcmds += f'mkdir -p $(dirname "$RPM_BUILD_ROOT/{path}")\n'
+ if isinstance(data, dict):
+ installcmds += f"cp {data['path']} \"$RPM_BUILD_ROOT/{path}\""
+ else:
+ installcmds += f'cat >"$RPM_BUILD_ROOT/{path}" <<\'EOF\'\n' + data + '\nEOF\n'
+ installedfiles += f"{path}\n"
+
+ architecture = ""
+ if arch == self.primary_arch:
+ architecture = f"BuildArch: {self.primary_arch}"
+ spec = f"""
+Summary: dummy {name}
+Name: {name}
+Version: {version}
+Release: {release}
+License: BSD
+{provides}
+{architecture}
+{requires}
+
+%%install
+{installcmds}
+
+%%description
+Test package.
+
+%%files
+{installedfiles}
+
+{postcode}
+"""
+ self.machine.write("/tmp/spec", spec)
+ cmd = """
+rpmbuild --quiet -bb /tmp/spec
+mkdir -p {0}
+cp ~/rpmbuild/RPMS/{4}/*.rpm {0}
+rm -rf ~/rpmbuild
+"""
+ if install:
+ cmd += "rpm -i {0}/{1}-{2}-{3}.*.rpm"
+ self.machine.execute(cmd.format(self.repo_dir, name, version, release, arch))
+ self.addCleanup(self.machine.execute, f"rpm -e --nodeps {name} 2>/dev/null || true")
+
+ def createPacmanPkg(self, name, version, release, requires, postinst, install, content, arch, provides):
+ """Create a dummy pacman package in repo_dir on self.machine
+
+ If install is True, install the package. Otherwise, update the package
+ index in repo_dir.
+ """
+
+ if arch is None:
+ arch = 'any'
+
+ sources = ""
+ installcmds = 'package() {\n'
+ if content is not None:
+ sources = "source=("
+ files = 0
+ for path, data in content.items():
+ p = os.path.dirname(path)
+ installcmds += f'mkdir -p $pkgdir{p}\n'
+ if isinstance(data, dict):
+ dpath = data["path"]
+
+ file = os.path.basename(dpath)
+ sources += file
+ files += 1
+ # TODO: hardcoded /tmp
+ self.machine.execute(f'cp {data["path"]} /tmp/{file}')
+ installcmds += f'cp {file} $pkgdir{path}\n'
+ else:
+ installcmds += f'cat >"$pkgdir{path}" <<\'EOF\'\n' + data + '\nEOF\n'
+
+ sources += ")"
+
+ # Always stamp a file
+ installcmds += f"touch $pkgdir/stamp-{name}-{version}-{release}\n"
+ installcmds += '}'
+
+ pkgbuild = f"""
+pkgname={name}
+pkgver={version}
+pkgdesc="dummy {name}"
+pkgrel={release}
+arch=({arch})
+depends=({requires})
+{sources}
+
+{installcmds}
+"""
+
+ if postinst:
+ postinstcode = f"""
+post_install() {{
+ {postinst}
+}}
+
+post_upgrade() {{
+ post_install $*
+}}
+"""
+ self.machine.write(f"/tmp/{name}.install", postinstcode)
+ pkgbuild += f"\ninstall={name}.install\n"
+
+ self.machine.write("/tmp/PKGBUILD", pkgbuild)
+
+ cmd = """
+ cd /tmp/
+ su builder -c "makepkg --cleanbuild --clean --force --nodeps --skipinteg --noconfirm"
+"""
+
+ if install:
+ cmd += f"pacman -U --overwrite '*' --noconfirm {name}-{version}-{release}-{arch}.pkg.tar.zst\n"
+
+ cmd += f"mkdir -p {self.repo_dir}\n"
+ cmd += f"mv *.pkg.tar.zst {self.repo_dir}\n"
+ # Clean up packaging files
+ cmd += "rm PKGBUILD\n"
+ if postinst:
+ cmd += f"rm /tmp/{name}.install"
+ self.machine.execute(cmd)
+ self.addCleanup(self.machine.execute, f"pacman -Rdd --noconfirm {name} 2>/dev/null || true")
+
+ def createAptChangelogs(self):
+ # apt metadata has no formal field for bugs/CVEs, they are parsed from the changelog
+ for ((pkg, ver, rel), info) in self.updateInfo.items():
+ changes = info.get("changes", "some changes")
+ if info.get("bugs"):
+ changes += f" (Closes: {', '.join([('#' + str(b)) for b in info['bugs']])})"
+ if info.get("cves"):
+ changes += "\n * " + ", ".join(info["cves"])
+
+ path = f"{self.repo_dir}/changelogs/{pkg[0]}/{pkg}/{pkg}_{ver}-{rel}"
+ contents = f"""{pkg} ({ver}-{rel}) unstable; urgency=medium
+
+ * {changes}
+
+ -- Joe Developer <joe@example.com> Wed, 31 May 2017 14:52:25 +0200
+"""
+ self.machine.execute(f"mkdir -p $(dirname {path}); echo '{contents}' > {path}")
+
+ def createYumUpdateInfo(self):
+ xml = '<?xml version="1.0" encoding="UTF-8"?>\n<updates>\n'
+ for ((pkg, ver, rel), info) in self.updateInfo.items():
+ refs = ""
+ for b in info.get("bugs", []):
+ refs += f' <reference href="https://bugs.example.com?bug={b}" id="{b}" title="Bug#{b} Description" type="bugzilla"/>\n'
+ for c in info.get("cves", []):
+ refs += f' <reference href="https://www.cve.org/CVERecord?id={c}" id="{c}" title="{c}" type="cve"/>\n'
+ if info.get("securitySeverity"):
+ refs += ' <reference href="https://access.redhat.com/security/updates/classification/#{0}" id="" title="" type="other"/>\n'.format(info[
+ "securitySeverity"])
+ for e in info.get("errata", []):
+ refs += f' <reference href="https://access.redhat.com/errata/{e}" id="{e}" title="{e}" type="self"/>\n'
+
+ xml += """ <update from="test@example.com" status="stable" type="{severity}" version="2.0">
+ <id>UPDATE-{pkg}-{ver}-{rel}</id>
+ <title>{pkg} {ver}-{rel} update</title>
+ <issued date="2017-01-01 12:34:56"/>
+ <description>{desc}</description>
+ <references>
+{refs}
+ </references>
+ <pkglist>
+ <collection short="0815">
+ <package name="{pkg}" version="{ver}" release="{rel}" epoch="0" arch="noarch">
+ <filename>{pkg}-{ver}-{rel}.noarch.rpm</filename>
+ </package>
+ </collection>
+ </pkglist>
+ </update>
+""".format(pkg=pkg, ver=ver, rel=rel, refs=refs,
+ desc=info.get("changes", ""), severity=info.get("severity", "bugfix"))
+
+ xml += '</updates>\n'
+ return xml
+
+ def addPackageSet(self, name):
+ self.machine.execute(f"mkdir -p {self.repo_dir}; cp /var/lib/package-sets/{name}/* {self.repo_dir}")
+
+ def enableRepo(self):
+ if self.backend == "apt":
+ self.createAptChangelogs()
+ self.machine.execute(f"""echo 'deb [trusted=yes] file://{self.repo_dir} /' > /etc/apt/sources.list.d/test.list
+ cd {self.repo_dir}; apt-ftparchive packages . > Packages
+ xz -c Packages > Packages.xz
+ O=$(apt-ftparchive -o APT::FTPArchive::Release::Origin=cockpittest release .); echo "$O" > Release
+ echo 'Changelogs: http://localhost:12345/changelogs/@CHANGEPATH@' >> Release
+ """)
+ pid = self.machine.spawn(f"cd {self.repo_dir}; exec python3 -m http.server 12345", "changelog")
+ # pid will not be present for rebooting tests
+ self.addCleanup(self.machine.execute, "kill %i || true" % pid)
+ self.machine.wait_for_cockpit_running(port=12345) # wait for changelog HTTP server to start up
+ elif self.backend == "alpm":
+ self.machine.execute(f"""cd {self.repo_dir}
+ repo-add {self.repo_dir}/testrepo.db.tar.gz *.pkg.tar.zst
+ """)
+
+ config = f"""
+[testrepo]
+SigLevel = Never
+Server = file://{self.repo_dir}
+ """
+ if 'testrepo' not in self.machine.execute('grep testrepo /etc/pacman.conf || true'):
+ self.machine.write("/etc/pacman.conf", config, append=True)
+
+ else:
+ self.machine.execute("""printf '[updates]\nname=cockpittest\nbaseurl=file://{0}\nenabled=1\ngpgcheck=0\n' > /etc/yum.repos.d/cockpittest.repo
+ echo '{1}' > /tmp/updateinfo.xml
+ createrepo_c {0}
+ modifyrepo_c /tmp/updateinfo.xml {0}/repodata
+ dnf clean all""".format(self.repo_dir, self.createYumUpdateInfo()))
diff --git a/test/common/pixel-tests b/test/common/pixel-tests
new file mode 100755
index 0000000..0c70dc6
--- /dev/null
+++ b/test/common/pixel-tests
@@ -0,0 +1,261 @@
+#!/bin/bash
+
+set -eu
+
+TEST_REFERENCE_SUBDIR="${TEST_REFERENCE_SUBDIR:-test/reference}"
+REPO=pixel-test-reference
+
+GITHUB_BASE="${GITHUB_BASE:-cockpit-project/cockpit}"
+GITHUB_REPOSITORY="${GITHUB_BASE%/*}/${REPO}"
+CLONE_REMOTE="https://github.com/${GITHUB_REPOSITORY}"
+PUSH_REMOTE="git@github.com:${GITHUB_REPOSITORY}"
+
+message() {
+ [ "${V-}" != 0 ] || printf " %-8s %s\n" "$1" "$2"
+}
+
+cmd_init() {
+ git submodule add -b empty "$CLONE_REMOTE" "$TEST_REFERENCE_SUBDIR"
+}
+
+cmd_update() {
+ git submodule update --init -- "$TEST_REFERENCE_SUBDIR" || (
+ echo ""
+ echo "Updating test/reference has failed, maybe because of"
+ echo "local changes that have been accidentally made while"
+ echo "it was out of date."
+ echo ""
+ echo "If you want to throw away these local changes, run"
+ echo ""
+ echo " $ ./test/common/pixel-tests reset"
+ echo ""
+ exit 1
+ )
+}
+
+cmd_pull() {
+ cmd_update
+}
+
+cmd_status() {
+ cmd_update
+ ( cd "$TEST_REFERENCE_SUBDIR"
+ git rm --force --cached --quiet '*.png'
+ git add *.png
+ if git diff-index --name-status --cached --exit-code HEAD; then
+ echo No changes
+ fi
+ )
+}
+
+cmd_push() {
+ cmd_update
+ ( cd "$TEST_REFERENCE_SUBDIR"
+ git rm --force --cached --quiet '*.png'
+ git add *.png
+ if ! git diff-index --name-status --cached --exit-code HEAD; then
+ git fetch origin empty:empty
+ git reset --soft empty
+ git commit --quiet -m "$(date)"
+ else
+ echo No changes
+ fi
+ tag="sha-$(git rev-parse HEAD)"
+ [ $(git tag -l "$tag") ] || git tag "$tag" HEAD
+ git push "$PUSH_REMOTE" "$tag"
+ )
+ git add "$TEST_REFERENCE_SUBDIR"
+ if [ -n "$(git status --porcelain "$TEST_REFERENCE_SUBDIR")" ]; then
+ echo ""
+ echo "The test/reference link has changed. The next step is to commit and"
+ echo "push this change, just like any other change to a file."
+ echo ""
+ echo "The change has already been added with 'git add', so you could now"
+ echo "amend your current HEAD commit with it like this:"
+ echo ""
+ echo " $ git commit --amend"
+ echo ""
+ echo "Then the HEAD commit can be pushed like normally. There is nothing"
+ echo "special about committing and pushing a change to test/reference."
+ fi
+}
+
+cmd_reset() {
+ rm -rf "$TEST_REFERENCE_SUBDIR"
+ cmd_update
+}
+
+pixel_test_logs_urls() {
+ arg=${1:-}
+
+ if [[ "$arg" == http* ]]; then
+ echo $arg
+ return
+ fi
+
+ repo=$(git remote get-url origin | sed -re 's,git@github.com:|https://github.com/,,' -e 's,\.git$,,')
+
+ if [ -n "$arg" ]; then
+ revision=$(curl -s "https://api.github.com/repos/$repo/pulls/$arg" | python3 -c "
+import json
+import sys
+
+print(json.load(sys.stdin)['head']['sha'])
+")
+ else
+ revision=$(git rev-parse @{upstream})
+ fi
+
+ context=$(cat test/reference-image)
+ curl -s "https://api.github.com/repos/$repo/statuses/$revision?per_page=100" | python3 -c "
+import json
+import sys
+import os
+
+seen = set()
+for s in json.load(sys.stdin):
+ c = s['context']
+ if 'pybridge' in c or 'firefox' in c or 'devel' in c:
+ continue
+ if c.split('/')[0] == sys.argv[1] and s['target_url']:
+ url=os.path.dirname(s['target_url'])
+ if url not in seen:
+ seen.add(url)
+ print(url)
+" "$context"
+}
+
+cmd_fetch() {
+ urls=$(pixel_test_logs_urls ${1:-})
+ if [ -z "$urls" ]; then
+ echo >&2 "Can't find test results for $(cat test/reference-image), sorry."
+ exit 1
+ fi
+ cmd_update
+ for url in ${urls}; do
+ url=${url/\/log.html/}
+ echo "Fetching new pixel test references from $url"
+ pixels=$(curl -s "$url/index.html" | grep '[^=><"]*-pixels.png' -o | uniq)
+ for f in ${pixels}; do
+ echo "$f"
+ curl -s --output-dir test/reference/ -O "$url/$f"
+ done
+ done
+}
+
+cmd_help() {
+ cat <<EOF
+$0 - Maintain the test/reference directory
+
+The following commands are available:
+
+update - Ensure that test/reference matches the checked out branch
+
+ The test/reference directory is a git submodule, and it needs to
+ be kept in sync with the main worktree (just like any other
+ submodule). This is necessary whenever you check out a different
+ branch and then start working on pixel tests, for example.
+
+ You can use "git checkout --recurse-submodules" when changing
+ branches to achieve this, or some variation of "git submodule
+ update test/reference", or run "pixel-tests update".
+
+ Most other pixel-tests commands will run "pixel-tests update"
+ automatically, so you might not actually need to run it explicitly
+ very often, but it's good to remember that this step is
+ unfortunately necessary.
+
+ Like other ways to update submodules, this command will not
+ overwrite your local changes, so it is safe to use often and "just
+ in case". If your local changes conflict with the changes that
+ would be necessary for updating test/reference, the update will
+ not be done.
+
+ When "pixel-tests update" fails, it is probably easiest to throw
+ away local changes with "pixel-tests reset" and re-acquire the new
+ reference images that you want to install, maybe with "pixel-tests
+ fetch".
+
+pull - Old name for "update".
+
+reset - Throw away local changes
+
+ If you want to get rid of unpushed local changes, run "pixel-tests
+ reset". This will remove the test/reference directory completely
+ and then run "pixel-tests update" to recreate it.
+
+status - Show local changes
+
+ After writing new reference images into test/reference, running
+ "pixel-test status" will summarize the local changes. It will
+ list all images that have been modified (with a "M" prefix), added
+ ("A"), or deleted ("D").
+
+ The "status" command will run "update" as the first step, so it
+ might fail if it detects conflicts. To avoid that risk, run
+ "update" explicitly before making any changes in test/reference.
+
+push - Upload local changes and prepare test/reference for committing
+
+ Once you have finished writing new reference images into
+ test/reference (maybe by running "pixel-tests fetch") and are
+ ready to make them part of your pull request, you need to upload
+ them to Github and record them in the main source repository.
+
+ First, run "pixel-tests push" to do the uploading and to stage a
+ changed test/reference in the main source repository.
+
+ In the main source reository, test/reference is a special kind of
+ object. It's not a file, or directory, and not even a symlink
+ (although it is similar to a symlink). It's a "gitlink". But
+ whatever it is, changes to it need to be staged, committed, and
+ pushed, just like changes to regular files.
+
+ When "pixel-tests push" is done, the change to test/reference has
+ already been staged with "git add" to remind you that there is
+ something to commit.
+
+ Committing the change is identical to committing any other change.
+
+ "push" will also run "update" as the first step.
+
+fetch - Download fresh reference images from a test run
+
+ When code or tests are changed, we often need to install new or
+ changed reference images for the pixel tests. A good way to do
+ that is to run the tests in our CI machinery, let them fail, and
+ grab the new reference images from the test log directory.
+ Running "pixel-tests fetch" can automate this.
+
+ Without any arguments, "fetch" will figure out all by itself where
+ to download the images from. For this to work, your "origin"
+ remote needs to point to the repository on Github that the current
+ PR will be merged into. For our main Cockpit repository, that
+ would be "cockpit-project/cockpit", for example.
+
+ You can also pass a PR number to "fetch" if the current branch
+ doesn't correspond to the PR with the new pixels. Or you can pass
+ the URL of the log results.
+
+ "push" will also run "update" as the first step.
+EOF
+}
+
+main() {
+ local cmd="${1-}"
+
+ if [ -z "${cmd}" ]; then
+ echo 'This command requires a subcommand: update status push reset fetch'
+ echo "Run '$0 help' for a longer explanation"
+ exit 1
+ elif ! type -t "cmd_${cmd}" | grep -q function; then
+ echo "Unknown subcommand ${cmd}"
+ exit 1
+ fi
+
+ shift
+ [ "${V-0}" = 0 ] || set -x
+ "cmd_$cmd" "$@"
+}
+
+main "$@"
diff --git a/test/common/pixeldiff.html b/test/common/pixeldiff.html
new file mode 100644
index 0000000..36970f2
--- /dev/null
+++ b/test/common/pixeldiff.html
@@ -0,0 +1,623 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Cockpit Integration Tests - Pixel diffs</title>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
+ <style>
+/*
+MIT License
+
+Copyright (c) 2018 Diamant Haxhimusa
+
+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.
+*/
+.pixelcompare-wrapper {
+ background: red;
+}
+.pixelcompare-wrapper.pixelcompare-horizontal {
+ height: 100% !important;
+}
+.pixelcompare-horizontal .pixelcompare-handle:before, .pixelcompare-horizontal .pixelcompare-handle:after, .pixelcompare-vertical .pixelcompare-handle:before, .pixelcompare-vertical .pixelcompare-handle:after {
+ content: "";
+ display: block;
+ background: #fff;
+ position: absolute;
+ z-index: 30;
+}
+img.pixelcompare-before, img.pixelcompare-after {
+ object-fit: cover;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label, .pixelcompare-overlay {
+ position: absolute;
+ top: 0;
+ width: 100%;
+ height: 100%;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label, .pixelcompare-overlay {
+ transition-duration: 0.5s;
+}
+
+.pixelcompare-before-label, .pixelcompare-after-label {
+ transition-property: opacity;
+}
+
+.pixelcompare-horizontal .pixelcompare-before-label:before, .pixelcompare-horizontal .pixelcompare-after-label:before {
+ top: 50%;
+ margin-top: -19px;
+}
+
+.pixelcompare-vertical .pixelcompare-before-label:before, .pixelcompare-vertical .pixelcompare-after-label:before {
+ left: 50%;
+ margin-left: -45px;
+ text-align: center;
+ width: 90px;
+}
+
+.pixelcompare-left-arrow, .pixelcompare-right-arrow, .pixelcompare-up-arrow, .pixelcompare-down-arrow {
+ width: 0;
+ height: 0;
+ border: 6px inset transparent;
+ position: absolute;
+}
+
+.pixelcompare-left-arrow, .pixelcompare-right-arrow {
+ top: 50%;
+ margin-top: -6px;
+}
+
+.pixelcompare-up-arrow, .pixelcompare-down-arrow {
+ left: 50%;
+ margin-left: -6px;
+}
+
+.pixelcompare-container {
+ box-sizing: content-box;
+ z-index: 0;
+ height: 100%;
+ overflow: hidden;
+ position: relative;
+ -webkit-user-select: none;
+ -khtml-user-select: none;
+ -moz-user-select: none;
+ -o-user-select: none;
+ user-select: none;
+}
+.pixelcompare-container img {
+ max-width: 100%;
+ position: absolute;
+ top: 0;
+ display: block;
+}
+/* .pixelcompare-container.active .pixelcompare-overlay,
+.pixelcompare-container.active :hover.pixelcompare-overlay {
+ background: transparent;
+} */
+.pixelcompare-container.active .pixelcompare-overlay .pixelcompare-before-label,
+.pixelcompare-container.active .pixelcompare-overlay .pixelcompare-after-label,
+.pixelcompare-container.active :hover.pixelcompare-overlay .pixelcompare-before-label,
+.pixelcompare-container.active :hover.pixelcompare-overlay .pixelcompare-after-label {
+ opacity: 0;
+}
+.pixelcompare-container * {
+ box-sizing: content-box;
+}
+
+.pixelcompare-before-label {
+ opacity: 0;
+}
+.pixelcompare-before-label:before {
+ content: attr(data-content);
+}
+
+.pixelcompare-after-label {
+ opacity: 0;
+}
+.pixelcompare-after-label:before {
+ content: attr(data-content);
+}
+
+.pixelcompare-horizontal .pixelcompare-before-label:before {
+ left: 10px;
+}
+
+.pixelcompare-horizontal .pixelcompare-after-label:before {
+ right: 10px;
+}
+
+.pixelcompare-vertical .pixelcompare-before-label:before {
+ top: 10px;
+}
+
+.pixelcompare-vertical .pixelcompare-after-label:before {
+ bottom: 10px;
+}
+
+.pixelcompare-overlay {
+ transition-property: background;
+ background: transparent;
+ z-index: 25;
+}
+.pixelcompare-overlay:hover,
+.pixelcompare-container.active .pixelcompare-overlay,
+.pixelcompare-handle:hover + .pixelcompare-overlay {
+ background: rgba(0, 0, 0, 0.5);
+}
+.pixelcompare-overlay:hover .pixelcompare-after-label {
+ opacity: 1;
+}
+.pixelcompare-overlay:hover .pixelcompare-before-label {
+ opacity: 1;
+}
+
+.pixelcompare-before {
+ z-index: 20;
+}
+
+.pixelcompare-after {
+ z-index: 10;
+}
+
+.pixelcompare-vertical .pixelcompare-handle {
+ background-color: rgba(247, 237, 237, 0.71);
+ height: 20px;
+ width: 65px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-left: -19px;
+ margin-top: -10px;
+ border-radius: 1000px;
+ box-shadow: 0px 0px 12px rgba(51, 51, 51, 0.5);
+ z-index: 40;
+ cursor: pointer;
+}
+
+.pixelcompare-horizontal .pixelcompare-handle {
+ background-color: rgba(247, 237, 237, 0.71);
+ height: 65px;
+ width: 20px;
+ position: absolute;
+ left: 50%;
+ top: 50%;
+ margin-top: -19px;
+ margin-left: -10px;
+ border-radius: 1000px;
+ box-shadow: 0px 0px 12px rgba(51, 51, 51, 0.5);
+ z-index: 40;
+ cursor: pointer;
+}
+
+.pixelcompare-horizontal .pixelcompare-handle:before {
+ bottom: 50%;
+ height: 799px;
+ margin-bottom: 33px;
+ margin-left: 10px;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-right: 1px dashed rgba(255, 255, 255, 0.5);
+}
+
+.pixelcompare-horizontal .pixelcompare-handle:after {
+ top: 50%;
+ height: 257px;
+ margin-top: 33px;
+ margin-left: 10px;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-right: 1px dashed rgba(255, 255, 255, 0.5);
+}
+
+
+.pixelcompare-vertical .pixelcompare-handle:before {
+ margin-top: 10px;
+ margin-left: 11px;
+ width: 100vw;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-top: 1px dashed rgba(255, 255, 255, 0.5);
+ right: 50%;
+ margin-right: 33px;
+}
+
+.pixelcompare-vertical .pixelcompare-handle:after {
+ margin-top: 10px;
+ width: 100vw;
+ background: none;
+ position: absolute;
+ z-index: 999;
+ border-top: 1px dashed rgba(255, 255, 255, 0.5);
+ left: 50%;
+ margin-left: 33px;
+}
+
+.pixelcompare-left-arrow {
+ border-right: 6px solid rgba(247, 237, 237, 0.71);
+ left: 50%;
+ margin-left: -13px;
+}
+
+.pixelcompare-right-arrow {
+ border-left: 6px solid rgba(247, 237, 237, 0.71);
+ right: 50%;
+ margin-right: -13px;
+}
+
+.pixelcompare-up-arrow {
+ border-bottom: 6px solid rgba(247, 237, 237, 0.71);
+ top: 50%;
+ margin-top: -13px;
+}
+
+.pixelcompare-down-arrow {
+ border-top: 6px solid rgba(247, 237, 237, 0.71);
+ bottom: 50%;
+ margin-bottom: -13px;
+}
+ </style>
+ <script>
+"use strict";
+/**
+ * PIXELCOMPARE
+ * Javascript image comparison
+ * @author diamanthaxhimusa@gmail.com
+
+MIT License
+
+Copyright (c) 2018 Diamant Haxhimusa
+
+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.
+
+ */
+window.setup_pixelcompare = function() {
+ var _extend = function(defaults, options) {
+ var extended = {};
+ var prop;
+ for (prop in defaults) {
+ if(Object.prototype.hasOwnProperty.call(defaults, prop)) {
+ extended[prop] = defaults[prop];
+ }
+ }
+ for (prop in options) {
+ if(Object.prototype.hasOwnProperty.call(options, prop)) {
+ extended[prop] = options[prop];
+ }
+ }
+ return extended;
+ };
+
+ var _addClass = function(element, classname) {
+ var arr;
+ arr = element.className.split(" ");
+ if(arr.indexOf(classname) == -1) {
+ element.className += " " + classname;
+ }
+ };
+
+ var _hasClass = function(element, className) {
+ return new RegExp("(\\s|^)" + className + "(\\s|$)").test(
+ element.className
+ );
+ };
+
+ var _removeClass = function(element, className) {
+ element.classList.remove(className);
+ };
+
+ var _wrap = function(element, tag, sliderOrientation) {
+ var div = document.createElement(tag);
+ _addClass(div, "pixelcompare-wrapper pixelcompare-" + sliderOrientation);
+ element.parentElement.insertBefore(div, element);
+ div.appendChild(element);
+ return div;
+ };
+
+ var _createSlider = function(beforeDirection, afterDirection) {
+ var pxcHandleNode = document.createElement("div");
+ _addClass(pxcHandleNode, "pixelcompare-handle");
+
+ var sliderChildNodeBeforeDirection = document.createElement("span");
+ _addClass(
+ sliderChildNodeBeforeDirection,
+ "pixelcompare-" + beforeDirection + "-arrow"
+ );
+ pxcHandleNode.appendChild(sliderChildNodeBeforeDirection);
+ var sliderChildNodeAfterDirection = document.createElement("span");
+ _addClass(
+ sliderChildNodeAfterDirection,
+ "pixelcompare-" + afterDirection + "-arrow"
+ );
+ pxcHandleNode.appendChild(sliderChildNodeAfterDirection);
+
+ pxcHandleNode.addEventListener("touchmove", function(e) {
+ e.preventDefault();
+ });
+ return pxcHandleNode;
+ };
+
+ var options = {
+ default_offset_pct: 0.5,
+ orientation: "horizontal",
+ overlay: false,
+ hover: false,
+ move_with_handle_only: true,
+ click_to_move: false,
+ showSlider: true,
+ };
+
+ var pxcContainers = document.querySelectorAll("[data-pixelcompare]");
+
+ pxcContainers.forEach(function(pcContainer) {
+ var sliderPct = options.default_offset_pct;
+ var imageContainer = pcContainer;
+ options.hover = pcContainer.hasAttribute("data-hover");
+ options.showSlider = options.hover ? pcContainer.hasAttribute("data-show-slider") : options.showSlider;
+ options.orientation = pcContainer.hasAttribute("data-vertical")
+ ? "vertical"
+ : "horizontal";
+ var orientations = ["vertical", "horizontal", "sides"];
+
+ var datasetOrientation = imageContainer.dataset.pixelcompareOrientation;
+ var sliderOrientation = orientations.includes(datasetOrientation)
+ ? datasetOrientation
+ : options.orientation;
+ var beforeDirection = sliderOrientation === "vertical" ? "down" : "left";
+ var afterDirection = sliderOrientation === "vertical" ? "up" : "right";
+
+ var container = _wrap(pcContainer, "div", sliderOrientation);
+
+ var beforeImg = container.querySelectorAll("img")[0];
+ beforeImg.draggable = false;
+ var afterImg = container.querySelectorAll("img")[1];
+ afterImg.draggable = false;
+
+ _addClass(container, "pixelcompare-container");
+ _addClass(beforeImg, "pixelcompare-before");
+ _addClass(afterImg, "pixelcompare-after");
+
+ var slider = null;
+ if(options.showSlider) {
+ slider = _createSlider(beforeDirection, afterDirection);
+ container.appendChild(slider);
+ }
+ if(options.overlay) {
+ var overlayNode = document.createElement("div");
+ _addClass(overlayNode, "pixelcompare-overlay");
+ container.appendChild(overlayNode);
+ }
+
+ var calcOffset = function(dimensionPct) {
+ var w = beforeImg.getBoundingClientRect().width;
+ var h = beforeImg.getBoundingClientRect().height;
+ return {
+ w: w + "px",
+ h: h + "px",
+ wp: dimensionPct * 100,
+ cw: dimensionPct * w + "px",
+ ch: dimensionPct * h + "px",
+ };
+ };
+
+ var adjustContainer = function(offset) {
+ if(sliderOrientation === "vertical") {
+ beforeImg.style.clip =
+ "rect(0, " + offset.w + ", " + offset.ch + ", 0)";
+ afterImg.style.clip =
+ "rect(" + offset.ch + ", " + offset.w + ", " + offset.h + ", 0)";
+ } else if(sliderOrientation === "sides") {
+ beforeImg.style.clipPath = `polygon(0% ${2 * (50 - offset.wp)}%, ${
+ 2 * offset.wp
+ }% 100%, 0% 100%)`;
+ afterImg.style.clipPath = `polygon(100% ${2 * (100 - offset.wp)}%, ${
+ -2 * (50 - offset.wp)
+ }% 0%, 100% 0%)`;
+ } else {
+ beforeImg.style.clip =
+ "rect(0, " + offset.cw + ", " + offset.h + ", 0)";
+ afterImg.style.clip =
+ "rect(0, " + offset.w + "," + offset.h + "," + offset.cw + ")";
+ }
+ container.style.height = offset.h;
+ };
+
+ var adjustSlider = function(pct) {
+ var offset = calcOffset(pct);
+ if(slider) {
+ if(sliderOrientation === "vertical") {
+ slider.style.top = offset.ch;
+ } else {
+ slider.style.left = offset.cw;
+ }
+ }
+ adjustContainer(offset);
+ };
+
+ // Return the number specified or the min/max number if it outside the range given.
+ var minMaxNumber = function(num, min, max) {
+ return Math.max(min, Math.min(max, num));
+ };
+
+ // Calculate the slider percentage based on the position.
+ var getSliderPercentage = function(positionX, positionY) {
+ var sliderPercentage =
+ sliderOrientation === "vertical"
+ ? (positionY - offsetY) / imgHeight
+ : (positionX - offsetX) / imgWidth;
+ return minMaxNumber(sliderPercentage, 0, 1);
+ };
+
+ window.addEventListener("resize.pixelcompare", function(e) {
+ adjustSlider(sliderPct);
+ });
+
+ var offsetX = 0;
+ var offsetY = 0;
+ var imgWidth = 0;
+ var imgHeight = 0;
+ var onMoveStart = function(e) {
+ if(
+ ((e.distX > e.distY && e.distX < -e.distY) ||
+ (e.distX < e.distY && e.distX > -e.distY)) &&
+ sliderOrientation !== "vertical"
+ ) {
+ e.preventDefault();
+ } else if(
+ ((e.distX < e.distY && e.distX < -e.distY) ||
+ (e.distX > e.distY && e.distX > -e.distY)) &&
+ sliderOrientation === "vertical"
+ ) {
+ e.preventDefault();
+ }
+ _addClass(container, "active");
+ offsetX = container.offsetLeft;
+ offsetY = container.offsetTop;
+ imgWidth = beforeImg.getBoundingClientRect().width;
+ imgHeight = beforeImg.getBoundingClientRect().height;
+ };
+ var onMove = function(e) {
+ if(_hasClass(container, "active")) {
+ sliderPct = getSliderPercentage(
+ e.pageX || e.changedTouches[0].pageX,
+ e.pageY || e.changedTouches[0].pageY
+ );
+ adjustSlider(sliderPct);
+ }
+ };
+ var onMoveEnd = function() {
+ _removeClass(container, "active");
+ };
+
+ if(options.hover) {
+ container.addEventListener("mouseenter", onMoveStart);
+ container.addEventListener("mouseleave", onMoveEnd);
+ container.addEventListener("mousemove", onMove);
+ } else {
+ var moveTarget = options.move_with_handle_only ? slider : container;
+ window.addEventListener("mouseup", onMoveEnd);
+ container.addEventListener("mousemove", onMove);
+ moveTarget.addEventListener("mousedown", onMoveStart);
+ moveTarget.addEventListener("touchstart", onMoveStart);
+ container.addEventListener("touchmove", onMove);
+ window.addEventListener("touchend", onMoveEnd);
+ }
+
+ container
+ .querySelector("img")
+ .addEventListener("mousedown", function(event) {
+ event.preventDefault();
+ });
+
+ if(options.click_to_move) {
+ container.on("click", function(e) {
+ offsetX = container.offset().left;
+ offsetY = container.offset().top;
+ imgWidth = beforeImg.width();
+ imgHeight = beforeImg.height();
+ sliderPct = getSliderPercentage(e.pageX, e.pageY);
+ adjustSlider(sliderPct);
+ });
+ }
+ window.dispatchEvent(new Event("resize.pixelcompare"));
+ });
+};
+ </script>
+ <script>
+document.addEventListener("DOMContentLoaded", () => {
+ const base = window.location.hash.replace(/[^\w-]/g, "");
+ document.getElementById("key").textContent = base;
+
+ const e_new = document.getElementById("new");
+ e_new.textContent = base + "-pixels.png";
+ e_new.setAttribute("href", base + "-pixels.png");
+
+ const img_now = document.createElement("img");
+ img_now.setAttribute("src", base + "-pixels.png");
+
+ const img_ref = document.createElement("img");
+ img_ref.setAttribute("src", base + "-reference.png");
+
+ const img_delta = document.createElement("img");
+ img_delta.setAttribute("src", base + "-delta.png");
+
+ let loaded = { };
+
+ function load_event(tag) {
+ loaded[tag] = true;
+ if (loaded.now && loaded.ref && loaded.delta) {
+ const diff = document.getElementById("diff");
+ const w = Math.max(img_now.naturalWidth, img_ref.naturalWidth);
+ const h = Math.max(img_now.naturalHeight, img_ref.naturalHeight);
+ diff.style.width = w + "px";
+ diff.style.height = h + "px";
+ const ecompare = document.createElement("div");
+ ecompare.classList.add("pixelcompare");
+ ecompare.setAttribute("data-pixelcompare", "");
+ ecompare.appendChild(img_now);
+ ecompare.appendChild(img_ref);
+ diff.appendChild(ecompare);
+ window.setup_pixelcompare();
+
+ const delta = document.getElementById("delta");
+ delta.style.width = img_delta.naturalWidth + "px";
+ delta.style.height = img_delta.naturalHeight + "px";
+ delta.appendChild(img_delta);
+ }
+ }
+
+ img_now.addEventListener("load", () => load_event("now"));
+ img_ref.addEventListener("load", () => load_event("ref"));
+ img_delta.addEventListener("load", () => load_event("delta"));
+});
+ </script>
+ </head
+ <body>
+ <h2>Pixel comparison for <span id="key"></span></h2>
+ <p>New <a id="new"></a> on the left, reference on the right.</p>
+ <div id="diff">
+ </div>
+ <h2>Changed pixels in red, ignored changes in green</h2>
+ <div id="delta">
+ </div>
+ </body>
+</html>
diff --git a/test/common/pywrap b/test/common/pywrap
new file mode 100755
index 0000000..c5e78b3
--- /dev/null
+++ b/test/common/pywrap
@@ -0,0 +1,30 @@
+#!/bin/sh
+
+# Run a Python script, setting up PYTHONPATH for access to test/common and the
+# python libraries in bots/. Checks out the bots first, if necessary.
+
+# This is intended to be used from the interpreter line of executable Python
+# scripts, referring to it with a relative path. The interpreter line should
+# look something like so:
+
+ #!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/../test/common/pywrap", sys.argv)
+
+# with the `/../test/common/` part determined by the location of the script
+# relative to this script.
+
+set -eu
+
+realpath="$(realpath "$0")"
+top_srcdir="${realpath%/*}/../.."
+
+# Check out the bots if required
+test -d "${top_srcdir}/bots" || "${top_srcdir}/test/common/make-bots"
+
+# Prepend the path
+PYTHONPATH="${top_srcdir}/test/common:${top_srcdir}/bots:${top_srcdir}/bots/machine${PYTHONPATH:+:${PYTHONPATH}}"
+export PYTHONPATH
+
+# Run the script
+# -B : don't write .pyc files on import; also PYTHONDONTWRITEBYTECODE=x
+# -P : don't prepend a potentially unsafe path to sys.path -- but not available in RHEL 8/9 yet, use once we can
+exec python3 -B "$@"
diff --git a/test/common/ruff.toml b/test/common/ruff.toml
new file mode 100644
index 0000000..6e46ee9
--- /dev/null
+++ b/test/common/ruff.toml
@@ -0,0 +1,11 @@
+extend = "../../pyproject.toml"
+
+[lint]
+ignore = [
+ "E501", # https://github.com/charliermarsh/ruff/issues/3206#issuecomment-1562681390
+
+ "B010", # Do not call `setattr` with a constant attribute value. It is not any safer than normal property access.
+ "FBT001", # Boolean positional arg in function definition
+ "FBT002", # Boolean default value in function definition
+ "PT009", # Use a regular `assert` instead of unittest-style `assertEqual`
+]
diff --git a/test/common/run-tests b/test/common/run-tests
new file mode 100755
index 0000000..58c2962
--- /dev/null
+++ b/test/common/run-tests
@@ -0,0 +1,585 @@
+#!/usr/bin/python3 -cimport os, sys; os.execv(os.path.dirname(sys.argv[1]) + "/pywrap", sys.argv)
+
+import argparse
+import binascii
+import errno
+import glob
+import importlib.machinery
+import importlib.util
+import logging
+import os
+import socket
+import string
+import subprocess
+import sys
+import tempfile
+import time
+import unittest
+from typing import List, Optional, Tuple
+
+import testlib
+import testvm
+from lcov import create_coverage_report, prepare_for_code_coverage
+
+os.environ['PYTHONUNBUFFERED'] = '1'
+
+
+def flush_stdout():
+ while True:
+ try:
+ sys.stdout.flush()
+ break
+ except BlockingIOError:
+ time.sleep(0.1)
+
+
+class Test:
+ def __init__(self, test_id, command, timeout, nondestructive, retry_when_affected, todo, cost=1):
+ self.process = None
+ self.retries = 0
+ self.test_id = test_id
+ self.command = command
+ self.timeout = timeout
+ self.nondestructive = nondestructive
+ self.machine_id = None
+ self.retry_when_affected = retry_when_affected
+ self.todo = todo
+ self.cost = cost
+ self.returncode = None
+
+ def assign_machine(self, machine_id, ssh_address, web_address):
+ assert self.nondestructive, "assigning a machine only works for nondestructive test"
+ self.machine_id = machine_id
+ self.command.insert(-2, "--machine")
+ self.command.insert(-2, ssh_address)
+ self.command.insert(-2, "--browser")
+ self.command.insert(-2, web_address)
+
+ def start(self):
+ if self.nondestructive:
+ assert self.machine_id is not None, f"need to assign nondestructive test {self} {self.command} to a machine"
+ self.outfile = tempfile.TemporaryFile()
+ self.process = subprocess.Popen(["timeout", "-v", str(self.timeout), *self.command],
+ stdout=self.outfile, stderr=subprocess.STDOUT)
+
+ def poll(self):
+ poll_result = self.process.poll()
+ if poll_result is not None:
+ self.outfile.flush()
+ self.outfile.seek(0)
+ self.output = self.outfile.read()
+ self.outfile.close()
+ self.outfile = None
+ self.returncode = self.process.returncode
+
+ return poll_result
+
+ def finish(self, affected_tests: List[str], opts: argparse.Namespace) -> Tuple[Optional[str], int]:
+ """Returns if a test should retry or not
+
+ Call test-failure-policy on the test's output, print if needed.
+
+ Return (retry_reason, exit_code). retry_reason can be None or a string.
+ """
+
+ print_tap = not opts.list
+ affected = any(self.command[0].endswith(t) for t in affected_tests)
+ retry_reason = ""
+
+ # Try affected tests 3 times
+ if self.returncode == 0 and affected and self.retry_when_affected and self.retries < 2:
+ retry_reason = "test affected tests 3 times"
+ self.retries += 1
+ self._print_test(print_tap, f"# RETRY {self.retries} ({retry_reason})")
+ return retry_reason, 0
+
+ # If test is being skipped pick up the reason
+ if self.returncode == 77:
+ lines = self.output.splitlines()
+ skip_reason = lines[-1].strip().decode("utf-8")
+ self.output = b"\n".join(lines[:-1])
+ self._print_test(print_tap, skip_reason=skip_reason)
+ return None, 0
+
+ # If the test was marked with @todo then...
+ if self.todo is not None:
+ if self.returncode == 0:
+ # The test passed, but it shouldn't have.
+ self.returncode = 1 # that's a fail
+ self._print_test(print_tap, todo_reason=f'# expected failure: {self.todo}')
+ return None, 1
+ else:
+ # The test failed as expected
+ # Outputs 'not ok 1 test # TODO ...'
+ self._print_test(print_tap, todo_reason=f'# TODO {self.todo}')
+ return None, 0
+
+ if self.returncode == 0:
+ self._print_test(print_tap)
+ return None, 0
+
+ if not opts.thorough:
+ cmd = ["test-failure-policy", "--all"]
+ if not opts.track_naughties:
+ cmd.append("--offline")
+ cmd.append(testvm.DEFAULT_IMAGE)
+ try:
+ proc = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+ reason = proc.communicate(self.output + ("not ok " + str(self)).encode())[0].strip()
+
+ if proc.returncode == 77:
+ self.returncode = proc.returncode
+ self._print_test(skip_reason="# SKIP {0}".format(reason.decode("utf-8")))
+ return None, 0
+
+ if proc.returncode == 78:
+ self.returncode = proc.returncode
+ self._print_test(skip_reason="# NOTE {0}".format(reason.decode("utf-8")))
+ return None, 1
+
+ if proc.returncode == 1:
+ retry_reason = reason.decode("utf-8")
+
+ except OSError as ex:
+ if ex.errno != errno.ENOENT:
+ sys.stderr.write(f"\nCouldn't run test-failure-policy: {ex!s}\n")
+
+ # HACK: many tests are unstable, always retry them 3 times unless affected
+ if not affected and not retry_reason and not opts.no_retry_fail:
+ retry_reason = "be robust against unstable tests"
+
+ has_unexpected_message = testlib.UNEXPECTED_MESSAGE.encode() in self.output
+ has_pixel_test_message = testlib.PIXEL_TEST_MESSAGE.encode() in self.output
+ if self.retries < 2 and not (has_unexpected_message or has_pixel_test_message) and retry_reason:
+ self.retries += 1
+ self._print_test(retry_reason=f"# RETRY {self.retries} ({retry_reason})")
+ return retry_reason, 0
+
+ self.output += b"\n"
+ self._print_test()
+ self.machine_id = None
+ return None, 1
+
+ # internal methods
+
+ def __str__(self):
+ cost = "" if self.cost == 1 else f" ${self.cost}"
+ nd = f" [ND@{self.machine_id}]" if self.nondestructive else ""
+ return f"{self.test_id} {self.command[0]} {self.command[-1]}{cost}{nd}"
+
+ def _print_test(self, print_tap=True, retry_reason="", skip_reason="", todo_reason=""):
+ def write_line(line):
+ while line:
+ try:
+ sys.stdout.buffer.write(line)
+ break
+ except BlockingIOError as e:
+ line = line[e.characters_written:]
+ time.sleep(0.1)
+
+ # be quiet in TAP mode for successful tests
+ lines = self.output.strip().splitlines(keepends=True)
+ if print_tap and self.returncode == 0 and len(lines) > 0:
+ for line in lines[:-1]:
+ if line.startswith(b"WARNING:"):
+ write_line(line)
+ write_line(lines[-1])
+ else:
+ for line in lines:
+ write_line(line)
+
+ if retry_reason:
+ retry_reason = " " + retry_reason
+ if skip_reason:
+ skip_reason = " " + skip_reason
+ if todo_reason:
+ todo_reason = " " + todo_reason
+
+ if not print_tap:
+ print(retry_reason + skip_reason + todo_reason)
+ flush_stdout()
+ return
+
+ print() # Tap needs to start on a separate line
+ status = 'ok' if self.returncode in [0, 77] else 'not ok'
+ print(f"{status} {self}{retry_reason}{skip_reason}{todo_reason}")
+ flush_stdout()
+
+
+class GlobalMachine:
+ def __init__(self, restrict=True, cpus=None, memory_mb=None, machine_class=testvm.VirtMachine):
+ self.image = testvm.DEFAULT_IMAGE
+ self.network = testvm.VirtNetwork(image=self.image)
+ self.networking = self.network.host(restrict=restrict)
+ # provide enough RAM for cryptsetup's PBKDF, as long as that is not configurable:
+ # https://bugzilla.redhat.com/show_bug.cgi?id=1881829
+ self.machine = machine_class(verbose=True, networking=self.networking, image=self.image, cpus=cpus,
+ memory_mb=memory_mb or 1400)
+ self.machine_class = machine_class
+ if not os.path.exists(self.machine.image_file):
+ self.machine.pull(self.machine.image_file)
+ self.machine.start()
+ self.start_time = time.time()
+ self.duration = None
+ self.ssh_address = f"{self.machine.ssh_address}:{self.machine.ssh_port}"
+ self.web_address = f"{self.machine.web_address}:{self.machine.web_port}"
+ self.running_test = None
+
+ def reset(self):
+ # It is important to re-use self.networking here, so that the
+ # machine keeps its browser and control port.
+ self.machine.kill()
+ self.machine = self.machine_class(verbose=True, networking=self.networking, image=self.image)
+ self.machine.start()
+
+ def kill(self):
+ assert self.running_test is None, "can't kill global machine with running test"
+ self.machine.kill()
+ self.network.kill()
+ self.duration = round(time.time() - self.start_time)
+ self.machine = None
+ self.ssh_address = None
+ self.web_address = None
+
+ def is_available(self):
+ return self.machine and self.running_test is None
+
+
+def check_valid(filename):
+ name = os.path.basename(filename)
+ allowed = string.ascii_letters + string.digits + '-_'
+ if not all(c in allowed for c in name):
+ return None
+ return name.replace("-", "_")
+
+
+def build_command(filename, test, opts):
+ cmd = [filename]
+ if opts.trace:
+ cmd.append("-t")
+ if opts.verbosity:
+ cmd.append("-v")
+ if not opts.fetch:
+ cmd.append("--nonet")
+ if opts.list:
+ cmd.append("-l")
+ if opts.coverage:
+ cmd.append("--coverage")
+ cmd.append(test)
+ return cmd
+
+
+def get_affected_tests(test_dir, base_branch, test_files):
+ if not base_branch:
+ return []
+
+ changed_tests = []
+
+ # Detect affected tests from changed test files
+ diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, test_dir])
+ # Never consider 'test/verify/check-example' to be affected - our tests for tests count on that
+ # This file provides only examples, there is no place for it being flaky, no need to retry
+ changed_tests = [test.decode("utf-8") for test in diff_out.strip().splitlines() if not test.endswith(b"check-example")]
+
+ # If more than 3 test files were changed don't consider any of them as affected
+ # as it might be a PR that changes more unrelated things.
+ if len(changed_tests) > 3:
+ # If 'test/verify/check-testlib' is affected, keep just that one - our tests for tests count on that
+ if "test/verify/check-testlib" in changed_tests:
+ changed_tests = ["test/verify/check-testlib"]
+ else:
+ changed_tests = []
+
+ # Detect affected tests from changed pkg/* subdirectories in cockpit
+ # If affected tests get detected from pkg/* changes, don't apply the
+ # "only do this for max. 3 check-* changes" (even if the PR also changes ≥ 3 check-*)
+ # (this does not apply to other projects)
+ diff_out = subprocess.check_output(["git", "diff", "--name-only", "origin/" + base_branch, "--", "pkg/"])
+
+ # Drop changes in css files - this does not affect tests thus no reason to retry
+ files = [f.decode("utf-8") for f in diff_out.strip().splitlines() if not f.endswith(b"css")]
+
+ changed_pkgs = {"check-" + pkg.split('/')[1] for pkg in files}
+ changed_tests.extend([test for test in test_files if any(pkg in test for pkg in changed_pkgs)])
+
+ return changed_tests
+
+
+def detect_tests(test_files, image, opts):
+ """Detect tests to be run
+
+ Builds the list of tests we'll run in separate machines (destructive tests)
+ and the ones we can run on the same machine (nondestructive)
+ """
+
+ destructive_tests = []
+ nondestructive_tests = []
+ seen_classes = {}
+ machine_class = None
+ test_id = 1
+
+ for filename in test_files:
+ name = check_valid(filename)
+ if not name or not os.path.isfile(filename):
+ continue
+ loader = importlib.machinery.SourceFileLoader(name, filename)
+ module = importlib.util.module_from_spec(importlib.util.spec_from_loader(loader.name, loader))
+ loader.exec_module(module)
+ for test_suite in unittest.TestLoader().loadTestsFromModule(module):
+ for test in test_suite:
+ if hasattr(test, "machine_class") and test.machine_class is not None:
+ if machine_class is not None and machine_class != test.machine_class:
+ raise ValueError(f"only one unique machine_class can be used per project, provided with {machine_class} and {test.machine_class}")
+
+ machine_class = test.machine_class
+
+ # ensure that test classes are unique, so that they can be selected properly
+ cls = test.__class__.__name__
+ if seen_classes.get(cls) not in [None, filename]:
+ raise ValueError("test class %s in %s already defined in %s" % (cls, filename, seen_classes[cls]))
+ seen_classes[cls] = filename
+
+ test_method = getattr(test.__class__, test._testMethodName)
+ test_str = f"{cls}.{test._testMethodName}"
+ # most tests should take much less than 10mins, so default to that;
+ # longer tests can be annotated with @timeout(seconds)
+ # check the test function first, fall back to the class'es timeout
+ if opts.tests and not any(t in test_str for t in opts.tests):
+ continue
+ if test_str in opts.exclude:
+ continue
+ test_timeout = testlib.get_decorator(test_method, test, "timeout", 600)
+ nd = testlib.get_decorator(test_method, test, "nondestructive")
+ rwa = not testlib.get_decorator(test_method, test, "no_retry_when_changed")
+ todo = testlib.get_decorator(test_method, test, "todo")
+ if getattr(test.__class__, "provision", None):
+ # each additionally provisioned VM costs destructive test capacity
+ cost = len(test.__class__.provision)
+ else:
+ cost = 1
+ test = Test(test_id, build_command(filename, test_str, opts), test_timeout, nd, rwa, todo, cost=cost)
+ if nd:
+ nondestructive_tests.append(test)
+ else:
+ if not opts.nondestructive:
+ destructive_tests.append(test)
+ test_id += 1
+
+ # sort non destructive tests by class/test name, to avoid spurious errors where failures depend on the order of
+ # execution but let's make sure we always test them both ways around; hash the image name, which is
+ # robust, reproducible, and provides an even distribution of both directions
+ nondestructive_tests.sort(key=lambda t: t.command[-1], reverse=bool(binascii.crc32(image.encode()) & 1))
+
+ return (nondestructive_tests, destructive_tests, machine_class)
+
+
+def list_tests(opts):
+ test_files = glob.glob(os.path.join(opts.test_dir, opts.test_glob))
+ nondestructive_tests, destructive_tests, _ = detect_tests(test_files, "dummy", opts)
+ names = {t.command[-1] for t in nondestructive_tests + destructive_tests}
+ for n in sorted(names):
+ print(n)
+
+
+def run(opts, image):
+ fail_count = 0
+ start_time = time.time()
+
+ if opts.coverage:
+ prepare_for_code_coverage()
+
+ test_files = glob.glob(os.path.join(opts.test_dir, opts.test_glob))
+ changed_tests = get_affected_tests(opts.test_dir, opts.base, test_files)
+ nondestructive_tests, destructive_tests, machine_class = detect_tests(test_files, image, opts)
+ nondestructive_tests_len = len(nondestructive_tests)
+ destructive_tests_len = len(destructive_tests)
+
+ if opts.machine:
+ assert not destructive_tests
+
+ print(f"1..{nondestructive_tests_len + destructive_tests_len}")
+ flush_stdout()
+
+ running_tests = []
+ global_machines = []
+
+ if not opts.machine:
+ # Create appropriate number of nondestructive machines; prioritize the nondestructive tests, to get
+ # them out of the way as fast as possible, then let the destructive ones start as soon as
+ # a given nondestructive runner is done.
+ num_global = min(nondestructive_tests_len, opts.jobs)
+
+ for _ in range(num_global):
+ global_machines.append(GlobalMachine(restrict=not opts.enable_network, cpus=opts.nondestructive_cpus,
+ memory_mb=opts.nondestructive_memory_mb,
+ machine_class=machine_class or testvm.VirtMachine))
+
+ # test scheduling loop
+ while True:
+ made_progress = False
+
+ # mop up finished tests
+ logging.debug("test loop: %d running tests", len(running_tests))
+ for test in running_tests.copy():
+ poll_result = test.poll()
+ if poll_result is not None:
+ made_progress = True
+ running_tests.remove(test)
+ test_machine = test.machine_id # test_finish() resets it
+ retry_reason, test_result = test.finish(changed_tests, opts)
+ fail_count += test_result
+ logging.debug("test %s finished; result %s retry reason %s", test, test_result, retry_reason)
+
+ if test_machine is not None and not opts.machine:
+ # unassign from global machine
+ global_machines[test_machine].running_test = None
+
+ # sometimes our global machine gets messed up; also, tests that time out don't run cleanup handlers
+ # restart it to avoid an unbounded number of test retries and follow-up errors
+ if not opts.machine and (poll_result == 124 or (retry_reason and "test harness" in retry_reason)):
+ # try hard to keep the test output consistent
+ sys.stderr.write("\nRestarting global machine %s\n" % test_machine)
+ sys.stderr.flush()
+ global_machines[test_machine].reset()
+
+ # run again if needed
+ if retry_reason:
+ if test.nondestructive:
+ nondestructive_tests.insert(0, test)
+ else:
+ destructive_tests.insert(0, test)
+
+ if opts.machine:
+ if not running_tests and nondestructive_tests:
+ test = nondestructive_tests.pop(0)
+ logging.debug("Static machine is free, assigning next test %s", test)
+ test.assign_machine(-1, opts.machine, opts.browser)
+ test.start()
+ running_tests.append(test)
+ made_progress = True
+ else:
+ # find free global machines, and either assign a new non destructive test, or kill them to free resources
+ for (idx, machine) in enumerate(global_machines):
+ if machine.is_available():
+ if nondestructive_tests:
+ test = nondestructive_tests.pop(0)
+ logging.debug("Global machine %s is free, assigning next test %s", idx, test)
+ machine.running_test = test
+ test.assign_machine(idx, machine.ssh_address, machine.web_address)
+ test.start()
+ running_tests.append(test)
+ else:
+ logging.debug("Global machine %s is free, and no more non destructive tests; killing", idx)
+ machine.kill()
+
+ made_progress = True
+
+ def running_cost():
+ return sum(test.cost for test in running_tests)
+
+ # fill the remaining available job slots with destructive tests; run tests with a cost higher than #jobs by themselves
+ while destructive_tests and (running_cost() + destructive_tests[0].cost <= opts.jobs or len(running_tests) == 0):
+ test = destructive_tests.pop(0)
+ logging.debug("%d running tests with total cost %d, starting next destructive test %s",
+ len(running_tests), running_cost(), test)
+ test.start()
+ running_tests.append(test)
+ made_progress = True
+
+ # are we done?
+ if not running_tests:
+ assert not nondestructive_tests, f"nondestructive_tests should be empty: {[str(t) for t in nondestructive_tests]}"
+ assert not destructive_tests, f"destructive_tests should be empty: {[str(t) for t in destructive_tests]}"
+ break
+
+ # Sleep if we didn't make progress
+ if not made_progress:
+ time.sleep(0.5)
+
+ # Create coverage report
+ if opts.coverage:
+ create_coverage_report()
+
+ # print summary
+ duration = int(time.time() - start_time)
+ hostname = socket.gethostname().split(".")[0]
+
+ nondestructive_details = []
+ if not opts.machine:
+ for (idx, machine) in enumerate(global_machines):
+ nondestructive_details.append(f"{idx}: {machine.duration}s")
+
+ details = f"[{duration}s on {hostname}, {destructive_tests_len} destructive tests, {nondestructive_tests_len} nondestructive tests: {', '.join(nondestructive_details)}]"
+ print()
+ if fail_count > 0:
+ print(f"# {fail_count} TESTS FAILED {details}")
+ else:
+ print(f"# TESTS PASSED {details}")
+ flush_stdout()
+
+ return fail_count
+
+
+def main():
+ parser = testlib.arg_parser(enable_sit=False)
+ parser.add_argument('-j', '--jobs', type=int,
+ default=int(os.environ.get("TEST_JOBS", 1)), help="Number of concurrent jobs")
+ parser.add_argument('--thorough', action='store_true',
+ help='Thorough mode, no skipping known issues')
+ parser.add_argument('-n', '--nondestructive', action='store_true',
+ help='Only consider @nondestructive tests')
+ parser.add_argument('--machine', metavar="hostname[:port]",
+ default=None, help="Run tests against an already running machine; implies --nondestructive")
+ parser.add_argument('--browser', metavar="hostname[:port]",
+ default=None, help="When using --machine, use this cockpit web address")
+ parser.add_argument('--test-dir', default=os.environ.get("TEST_DIR", testvm.TEST_DIR),
+ help="Directory in which to glob for test files; default: %(default)s")
+ parser.add_argument('--test-glob', default="check-*",
+ help="Pattern with which to glob in the test directory; default: %(default)s")
+ parser.add_argument('--exclude', action="append", default=[], metavar="TestClass.testName",
+ help="Exclude test (exact match only); can be specified multiple times")
+ parser.add_argument('--nondestructive-cpus', type=int, default=None,
+ help="Number of CPUs for nondestructive test global machines")
+ parser.add_argument('--nondestructive-memory-mb', type=int, default=None,
+ help="RAM size for nondestructive test global machines")
+ parser.add_argument('--base', default=os.environ.get("BASE_BRANCH"),
+ help="Retry affected tests compared to given base branch; default: %(default)s")
+ parser.add_argument('--track-naughties', action='store_true',
+ help='Update the occurrence of naughties on cockpit-project/bots')
+ parser.add_argument('--no-retry-fail', action='store_true',
+ help="Don't retry failed tests")
+ opts = parser.parse_args()
+
+ if opts.machine:
+ if opts.jobs > 1:
+ parser.error("--machine cannot be used with concurrent jobs")
+ if not opts.browser:
+ parser.error("--browser must be specified together with --machine")
+ opts.nondestructive = True
+
+ # Tell any subprocesses what we are testing
+ if "TEST_REVISION" not in os.environ:
+ r = subprocess.run(["git", "rev-parse", "HEAD"],
+ universal_newlines=True, check=False, stdout=subprocess.PIPE)
+ if r.returncode == 0:
+ os.environ["TEST_REVISION"] = r.stdout.strip()
+
+ os.environ["TEST_BROWSER"] = os.environ.get("TEST_BROWSER", "chromium")
+
+ image = testvm.DEFAULT_IMAGE
+ testvm.DEFAULT_IMAGE = image
+ os.environ["TEST_OS"] = image
+
+ # Make sure tests can make relative imports
+ sys.path.append(os.path.realpath(opts.test_dir))
+
+ if opts.list:
+ list_tests(opts)
+ return 0
+
+ return run(opts, image)
+
+
+if __name__ == '__main__':
+ # logging.basicConfig(level=logging.DEBUG)
+ sys.exit(main())
diff --git a/test/common/storagelib.py b/test/common/storagelib.py
new file mode 100644
index 0000000..efc738b
--- /dev/null
+++ b/test/common/storagelib.py
@@ -0,0 +1,669 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2015 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+import json
+import os.path
+import re
+import textwrap
+
+from testlib import Error, MachineCase, wait
+
+
+def from_udisks_ascii(codepoints):
+ return ''.join(map(chr, codepoints[:-1]))
+
+
+class StorageHelpers:
+ """Mix-in class for using in tests that derive from something else than MachineCase or StorageCase"""
+
+ def inode(self, f):
+ return self.machine.execute("stat -L '%s' -c %%i" % f)
+
+ def retry(self, setup, check, teardown):
+ def step():
+ if setup:
+ setup()
+ if check():
+ return True
+ if teardown:
+ teardown()
+ return False
+
+ self.browser.wait(step)
+
+ def add_ram_disk(self, size=50):
+ """Add per-test RAM disk
+
+ The disk gets removed automatically when the test ends. This is safe for @nondestructive tests.
+
+ Return the device name.
+ """
+ # sanity test: should not yet be loaded
+ self.machine.execute("test ! -e /sys/module/scsi_debug")
+ self.machine.execute(f"modprobe scsi_debug dev_size_mb={size}")
+ dev = self.machine.execute('while true; do O=$(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block 2>/dev/null || true); '
+ '[ -n "$O" ] && break || sleep 0.1; done; echo "/dev/$O"').strip()
+ # don't use addCleanup() here, this is often busy and needs to be cleaned up late; done in MachineCase.nonDestructiveSetup()
+
+ return dev
+
+ def add_loopback_disk(self, size=50, name=None):
+ """Add per-test loopback disk
+
+ The disk gets removed automatically when the test ends. This is safe for @nondestructive tests.
+
+ Unlike add_ram_disk(), this can be called multiple times, and
+ is less size constrained. The backing file starts out sparse,
+ so this can be used to create massive block devices, as long
+ as you are careful to not actually use much of it.
+
+ However, loopback devices look quite special to the OS, so
+ they are not a very good simulation of a "real" disk.
+
+ Return the device name.
+
+ """
+ # HACK: https://bugzilla.redhat.com/show_bug.cgi?id=1969408
+ # It would be nicer to remove $F immediately after the call to
+ # losetup, but that will break some versions of lvm2.
+ backf = self.machine.execute("mktemp /var/tmp/loop.XXXX").strip()
+ dev = self.machine.execute(f"truncate --size={size}MB {backf}; "
+ f"losetup -P --show {name if name else '--find'} {backf}").strip()
+ # If this device had partions in its last incarnation on this
+ # machine, they might come back for unknown reasons, in a
+ # non-functional state. Running partprobe will get rid of
+ # them.
+ self.machine.execute("partprobe '%s'" % dev)
+ # right after unmounting the device is often still busy, so retry a few times
+ self.addCleanup(self.machine.execute, f"until losetup -d {dev}; do sleep 1; done; rm {backf}", timeout=10)
+ self.addCleanup(self.machine.execute, f"findmnt -n -o TARGET {dev} | xargs --no-run-if-empty umount;")
+
+ return dev
+
+ def add_targetd_loopback_disk(self, index, size=50):
+ """Add per-test loopback device that can be forcefully removed.
+ """
+
+ m = self.machine
+ model = f"disk{index}"
+ wwn = f"naa.5000{index:012x}"
+
+ m.execute(f"rm -f /var/tmp/targetd.{model}")
+ m.execute(f"targetcli /backstores/fileio create name={model} size={size}M file_or_dev=/var/tmp/targetd.{model}")
+ m.execute(f"targetcli /loopback create {wwn}")
+ m.execute(f"targetcli /loopback/{wwn}/luns create /backstores/fileio/{model}")
+
+ self.addCleanup(m.execute, f"targetcli /loopback delete {wwn}")
+ self.addCleanup(m.execute, f"targetcli /backstores/fileio delete {model}")
+ self.addCleanup(m.execute, f"rm -f /var/tmp/targetd.{model}")
+
+ dev = m.execute(f'for dev in /sys/block/*; do if [ -f $dev/device/model ] && [ "$(cat $dev/device/model | tr -d [:space:])" == "{model}" ]; then echo /dev/$(basename $dev); fi; done').strip()
+ if dev == "":
+ raise Error("Device not found")
+ return dev
+
+ def force_remove_disk(self, device):
+ """Act like the given device gets physically removed.
+
+ This circumvents all the normal EBUSY failures, and thus can be used for testing
+ the cleanup after a forceful removal.
+ """
+ self.machine.execute(f'echo 1 > /sys/block/{os.path.basename(device)}/device/delete')
+ # the removal trips up PCP and our usage graphs
+ self.allow_browser_errors("direct: instance name lookup failed.*")
+
+ def addCleanupVG(self, vgname):
+ """Ensure the given VG is removed after the test"""
+
+ self.addCleanup(self.machine.execute, f"if [ -d /dev/{vgname} ]; then vgremove --force {vgname}; fi")
+
+ # Dialogs
+
+ def dialog_wait_open(self):
+ self.browser.wait_visible('#dialog')
+
+ def dialog_wait_alert(self, text):
+ self.browser.wait_in_text('#dialog .pf-v5-c-alert__title', text)
+
+ def dialog_wait_title(self, text):
+ self.browser.wait_in_text('#dialog .pf-v5-c-modal-box__title', text)
+
+ def dialog_field(self, field):
+ return f'#dialog [data-field="{field}"]'
+
+ def dialog_val(self, field):
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "text-input-checked":
+ if self.browser.is_present(sel + " input[type=checkbox]:not(:checked)"):
+ return False
+ else:
+ return self.browser.val(sel + " input[type=text]")
+ elif ftype == "select":
+ return self.browser.attr(sel, "data-value")
+ else:
+ return self.browser.val(sel)
+
+ def dialog_set_val(self, field, val):
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "checkbox":
+ self.browser.set_checked(sel, val)
+ elif ftype == "select-spaces":
+ for label in val:
+ self.browser.set_checked(f'{sel} :contains("{label}") input', val)
+ elif ftype == "size-slider":
+ self.browser.set_val(sel + " .size-unit select", "1000000")
+ self.browser.set_input_text(sel + " .size-text input", str(val))
+ elif ftype == "select":
+ self.browser._wait_present(sel + f" select option[value='{val}']:not([disabled])")
+ self.browser.set_val(sel + " select", val)
+ elif ftype == "select-radio":
+ self.browser.click(sel + f" input[data-data='{val}']")
+ elif ftype == "text-input":
+ self.browser.set_input_text(sel, val)
+ elif ftype == "text-input-checked":
+ if not val:
+ self.browser.set_checked(sel + " input[type=checkbox]", val=False)
+ else:
+ self.browser.set_checked(sel + " input[type=checkbox]", val=True)
+ self.browser.set_input_text(sel + " [type=text]", val)
+ elif ftype == "combobox":
+ self.browser.click(sel + " button.pf-v5-c-select__toggle-button")
+ self.browser.click(sel + f" .pf-v5-c-select__menu li:contains('{val}') button")
+ else:
+ self.browser.set_val(sel, val)
+
+ def dialog_combobox_choices(self, field):
+ return self.browser.call_js_func("""(function (sel) {
+ var lis = ph_find(sel).querySelectorAll('li');
+ var result = [];
+ for (i = 0; i < lis.length; ++i)
+ result.push(lis[i].textContent);
+ return result;
+ })""", self.dialog_field(field))
+
+ def dialog_is_present(self, field, label):
+ return self.browser.is_present(f'{self.dialog_field(field)} :contains("{label}") input')
+
+ def dialog_wait_val(self, field, val, unit=None):
+ if unit is None:
+ unit = "1000000"
+
+ sel = self.dialog_field(field)
+ ftype = self.browser.attr(sel, "data-field-type")
+ if ftype == "size-slider":
+ self.browser.wait_val(sel + " .size-unit select", unit)
+ self.browser.wait_val(sel + " .size-text input", str(val))
+ elif ftype == "select":
+ self.browser.wait_attr(sel, "data-value", val)
+ else:
+ self.browser.wait_val(sel, val)
+
+ def dialog_wait_error(self, field, val):
+ # XXX - allow for more than one error
+ self.browser.wait_in_text('#dialog .pf-v5-c-form__helper-text .pf-m-error', val)
+
+ def dialog_wait_not_present(self, field):
+ self.browser.wait_not_present(self.dialog_field(field))
+
+ def dialog_wait_apply_enabled(self):
+ self.browser.wait_attr('#dialog button.apply:nth-of-type(1)', "disabled", None)
+
+ def dialog_wait_apply_disabled(self):
+ self.browser.wait_visible('#dialog button.apply:nth-of-type(1)[disabled]')
+
+ def dialog_apply(self):
+ self.browser.click('#dialog button.apply:nth-of-type(1)')
+
+ def dialog_apply_secondary(self):
+ self.browser.click('#dialog button.apply:nth-of-type(2)')
+
+ def dialog_cancel(self):
+ self.browser.click('#dialog button.cancel')
+
+ def dialog_wait_close(self):
+ # file system operations often take longer than 10s
+ with self.browser.wait_timeout(max(self.browser.cdp.timeout, 60)):
+ self.browser.wait_not_present('#dialog')
+
+ def dialog_check(self, expect):
+ for f in expect:
+ if not self.dialog_val(f) == expect[f]:
+ return False
+ return True
+
+ def dialog_set_vals(self, values):
+ # Sometimes a certain field needs to be set before other
+ # fields come into existence and thus the order matters that
+ # we set the fields in. The tests however just give us a
+ # unordered 'dict'. Instead of changing the tests, we figure
+ # out the right order dynamically here by just setting what we
+ # can and then starting over. As long as we make progress in
+ # each iteration, everything is good.
+ failed = {}
+ last_error = Exception
+ for f in values:
+ try:
+ self.dialog_set_val(f, values[f])
+ except Error as e:
+ failed[f] = values[f]
+ last_error = e
+ if failed:
+ if len(failed) < len(values):
+ self.dialog_set_vals(failed)
+ else:
+ raise last_error
+
+ def dialog(self, values, expect=None, secondary=False):
+ if expect is None:
+ expect = {}
+ self.dialog_wait_open()
+ for f in expect:
+ self.dialog_wait_val(f, expect[f])
+ self.dialog_set_vals(values)
+ if secondary:
+ self.dialog_apply_secondary()
+ else:
+ self.dialog_apply()
+ self.dialog_wait_close()
+
+ def confirm(self):
+ self.dialog({})
+
+ # There is some asynchronous activity in the storage stack. (It
+ # used to be much worse, but it has improved over the years, yay!)
+ #
+ # The tests deal with that by waiting for the right conditions,
+ # which sometimes means opening a dialog a couple of times until
+ # it has the right contents, or applying it a couple of times
+ # until it works.
+
+ def dialog_open_with_retry(self, trigger, expect):
+ def setup():
+ trigger()
+ self.dialog_wait_open()
+
+ def check():
+ if callable(expect):
+ return expect()
+ else:
+ return self.dialog_check(expect)
+
+ def teardown():
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ self.retry(setup, check, teardown)
+
+ def dialog_apply_with_retry(self, expected_errors=None):
+ def step():
+ try:
+ self.dialog_apply()
+ self.dialog_wait_close()
+ except Error:
+ if expected_errors is None:
+ return False
+ err = self.browser.text('#dialog')
+ print(err)
+ for exp in expected_errors:
+ if exp in err:
+ return False
+ raise
+ return True
+ self.browser.wait(step)
+
+ def dialog_with_retry(self, trigger, values, expect):
+ self.dialog_open_with_retry(trigger, expect)
+ if values:
+ for f in values:
+ self.dialog_set_val(f, values[f])
+ self.dialog_apply()
+ else:
+ self.dialog_cancel()
+ self.dialog_wait_close()
+
+ def dialog_with_error_retry(self, trigger, errors, values=None, first_setup=None, retry_setup=None, setup=None):
+ def doit():
+ nonlocal first_setup
+ trigger()
+ self.dialog_wait_open()
+ if values:
+ self.dialog_set_vals(values)
+ if first_setup:
+ first_setup()
+ first_setup = None
+ elif retry_setup:
+ retry_setup()
+ elif setup:
+ setup()
+ self.dialog_apply()
+ try:
+ self.dialog_wait_close()
+ return True
+ except Exception:
+ dialog_text = self.browser.text('#dialog .pf-v5-c-alert__title')
+ for err in errors:
+ if err in dialog_text:
+ print("WARNING: retrying dialog")
+ self.dialog_cancel()
+ self.dialog_wait_close()
+ return False
+ raise
+ self.browser.wait(doit)
+
+ def udisks_objects(self):
+ return json.loads(self.machine.execute(["python3", "-c", textwrap.dedent("""
+ import dbus, json
+ print(json.dumps(dbus.SystemBus().call_blocking(
+ "org.freedesktop.UDisks2",
+ "/org/freedesktop/UDisks2",
+ "org.freedesktop.DBus.ObjectManager",
+ "GetManagedObjects", "", [])))""")]))
+
+ def configuration_field(self, dev, tab, field):
+ managerObjects = self.udisks_objects()
+ for path in managerObjects:
+ if "org.freedesktop.UDisks2.Block" in managerObjects[path]:
+ iface = managerObjects[path]["org.freedesktop.UDisks2.Block"]
+ if from_udisks_ascii(iface["Device"]) == dev or from_udisks_ascii(iface["PreferredDevice"]) == dev:
+ for entry in iface["Configuration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print(f"{path}/{tab}/{field} = {from_udisks_ascii(entry[1][field])}")
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_configuration(self, dev, tab, field, text):
+ self.assertIn(text, self.configuration_field(dev, tab, field))
+
+ def assert_not_in_configuration(self, dev, tab, field, text):
+ self.assertNotIn(text, self.configuration_field(dev, tab, field))
+
+ def child_configuration_field(self, dev, tab, field):
+ udisks_objects = self.udisks_objects()
+ for path in udisks_objects:
+ if "org.freedesktop.UDisks2.Encrypted" in udisks_objects[path]:
+ block_iface = udisks_objects[path]["org.freedesktop.UDisks2.Block"]
+ crypto_iface = udisks_objects[path]["org.freedesktop.UDisks2.Encrypted"]
+ if from_udisks_ascii(block_iface["Device"]) == dev or from_udisks_ascii(block_iface["PreferredDevice"]) == dev:
+ for entry in crypto_iface["ChildConfiguration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print("%s/child/%s/%s = %s" % (path, tab, field,
+ from_udisks_ascii(entry[1][field])))
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_child_configuration(self, dev, tab, field, text):
+ self.assertIn(text, self.child_configuration_field(dev, tab, field))
+
+ def lvol_child_configuration_field(self, lvol, tab, field):
+ udisk_objects = self.udisks_objects()
+ for path in udisk_objects:
+ if "org.freedesktop.UDisks2.LogicalVolume" in udisk_objects[path]:
+ iface = udisk_objects[path]["org.freedesktop.UDisks2.LogicalVolume"]
+ if iface["Name"] == lvol:
+ for entry in iface["ChildConfiguration"]:
+ if entry[0] == tab:
+ if field in entry[1]:
+ print("%s/child/%s/%s = %s" % (path, tab, field,
+ from_udisks_ascii(entry[1][field])))
+ return from_udisks_ascii(entry[1][field])
+ return ""
+
+ def assert_in_lvol_child_configuration(self, lvol, tab, field, text):
+ self.assertIn(text, self.lvol_child_configuration_field(lvol, tab, field))
+
+ def setup_systemd_password_agent(self, password):
+ # This sets up a systemd password agent that replies to all
+ # queries with the given password.
+
+ self.write_file("/usr/local/bin/test-password-agent",
+ f"""#!/bin/sh
+# Sleep a bit to avoid starting this agent too quickly over and over,
+# and so that other agents get a chance as well.
+sleep 30
+
+for s in $(grep -h ^Socket= /run/systemd/ask-password/ask.* | sed 's/^Socket=//'); do
+ printf '%s' '{password}' | /usr/lib/systemd/systemd-reply-password 1 $s
+done
+""", perm="0755")
+
+ self.write_file("/etc/systemd/system/test-password-agent.service",
+ """
+[Unit]
+Description=Test Password Agent
+DefaultDependencies=no
+Conflicts=shutdown.target emergency.service
+Before=shutdown.target
+[Service]
+ExecStart=/usr/local/bin/test-password-agent
+""")
+
+ self.write_file("/etc/systemd/system/test-password-agent.path",
+ """
+[Unit]
+Description=Test Password Agent Directory Watch
+DefaultDependencies=no
+Conflicts=shutdown.target emergency.service
+Before=paths.target shutdown.target cryptsetup.target
+[Path]
+DirectoryNotEmpty=/run/systemd/ask-password
+MakeDirectory=yes
+""")
+ self.machine.execute("ln -s ../test-password-agent.path /etc/systemd/system/sysinit.target.wants/")
+
+ def encrypt_root(self, passphrase):
+ m = self.machine
+
+ # Set up a password agent in the old root and then arrange for
+ # it to be included in the initrd. This will unlock the new
+ # encrypted root during boot.
+ #
+ # The password agent and its initrd configuration will be
+ # copied to the new root, so it will stay in place also when
+ # the initrd is regenerated again from within the new root.
+
+ self.setup_systemd_password_agent(passphrase)
+ install_items = [
+ '/etc/systemd/system/sysinit.target.wants/test-password-agent.path',
+ '/etc/systemd/system/test-password-agent.path',
+ '/etc/systemd/system/test-password-agent.service',
+ '/usr/local/bin/test-password-agent',
+ ]
+ m.write("/etc/dracut.conf.d/01-askpass.conf",
+ f'install_items+=" {" ".join(install_items)} "')
+
+ # The first step is to move /boot to a new unencrypted
+ # partition on the new disk but keep it mounted at /boot.
+ # This helps when running grub2-install and grub2-mkconfig,
+ # which will look at /boot and do the right thing.
+ #
+ # Then we copy (most of) the old root to the new disk, into a
+ # logical volume sitting on top of a LUKS container.
+ #
+ # The kernel command line is changed to use the new root
+ # filesystem, and grub is installed on the new disk. The boot
+ # configuration of the VM has been changed to boot from the
+ # new disk.
+ #
+ # At that point the new root can be booted by the existing
+ # initrd, but the initrd will prompt for the passphrase (as
+ # expected). Thus, the initrd is regenerated to include the
+ # password agent from above.
+ #
+ # Before the reboot, we destroy the original disk to make
+ # really sure that it wont be used anymore.
+
+ info = m.add_disk("6G", serial="NEWROOT", boot_disk=True)
+ dev = "/dev/" + info["dev"]
+ wait(lambda: m.execute(f"test -b {dev} && echo present").strip() == "present")
+ m.execute(f"""
+set -x
+parted -s {dev} mktable msdos
+parted -s {dev} mkpart primary ext4 1M 500M
+parted -s {dev} mkpart primary ext4 500M 100%
+echo {passphrase} | cryptsetup luksFormat --pbkdf-memory=300 {dev}2
+luks_uuid=$(blkid -p {dev}2 -s UUID -o value)
+echo {passphrase} | cryptsetup luksOpen --pbkdf-memory=300 {dev}2 luks-$luks_uuid
+vgcreate root /dev/mapper/luks-$luks_uuid
+lvcreate root -n root -l100%VG
+mkfs.ext4 /dev/root/root
+mkdir /new-root
+mount /dev/root/root /new-root
+mkfs.ext4 {dev}1
+# don't move the EFI partition
+if mountpoint /boot/efi; then umount /boot/efi; fi
+mkdir /new-root/boot
+mount {dev}1 /new-root/boot
+tar --selinux --one-file-system -cf - --exclude /boot --exclude='/var/tmp/*' --exclude='/var/cache/*' \
+ --exclude='/var/lib/mock/*' --exclude='/var/lib/containers/*' --exclude='/new-root/*' \
+ / | tar --selinux -C /new-root -xf -
+tar --one-file-system -C /boot -cf - . | tar -C /new-root/boot -xf -
+umount /new-root/boot
+mount {dev}1 /boot
+echo "(hd0) {dev}" >/boot/grub2/device.map
+sed -i -e 's,/boot/,/,' /boot/loader/entries/*
+uuid=$(blkid -p /dev/root/root -s UUID -o value)
+buuid=$(blkid -p {dev}1 -s UUID -o value)
+echo "UUID=$uuid / auto defaults 0 0" >/new-root/etc/fstab
+echo "UUID=$buuid /boot auto defaults 0 0" >>/new-root/etc/fstab
+dracut --regenerate-all --force
+grub2-install {dev}
+( # HACK - grub2-mkconfig messes with /boot/loader/entries/ and /etc/kernel/cmdline
+ mv /boot/loader/entries /boot/loader/entries.stowed
+ ! test -f /etc/kernel/cmdline || mv /etc/kernel/cmdline /etc/kernel/cmdline.stowed
+ grub2-mkconfig -o /boot/grub2/grub.cfg
+ mv /boot/loader/entries.stowed /boot/loader/entries
+ ! test -f /etc/kernel/cmdline.stowed || mv /etc/kernel/cmdline.stowed /etc/kernel/cmdline
+)
+grubby --update-kernel=ALL --args="root=UUID=$uuid rootflags=defaults rd.luks.uuid=$luks_uuid rd.lvm.lv=root/root"
+! test -f /etc/kernel/cmdline || cp /etc/kernel/cmdline /new-root/etc/kernel/cmdline
+""", timeout=300)
+ m.spawn("dd if=/dev/zero of=/dev/vda bs=1M count=100; reboot", "reboot", check=False)
+ m.wait_reboot(300)
+ self.assertEqual(m.execute("findmnt -n -o SOURCE /").strip(), "/dev/mapper/root-root")
+
+ # Cards and tables
+
+ def card(self, title):
+ return f"[data-test-card-title='{title}']"
+
+ def card_parent_link(self):
+ return ".pf-v5-c-breadcrumb__item:nth-last-child(2) > a"
+
+ def card_header(self, title):
+ return self.card(title) + " .pf-v5-c-card__header"
+
+ def card_row(self, title, index=None, name=None, location=None):
+ if index is not None:
+ return self.card(title) + f" tbody tr:nth-child({index})"
+ elif name is not None:
+ name = name.replace("/dev/", "")
+ return self.card(title) + f" tbody [data-test-row-name='{name}']"
+ else:
+ return self.card(title) + f" tbody [data-test-row-location='{location}']"
+
+ def click_card_row(self, title, index=None, name=None, location=None):
+ # We need to click on a <td> element since that's where the handlers are...
+ self.browser.click(self.card_row(title, index, name, location) + " td:nth-child(1)")
+
+ def card_row_col(self, title, row_index=None, col_index=None, row_name=None, row_location=None):
+ return self.card_row(title, row_index, row_name, row_location) + f" td:nth-child({col_index})"
+
+ def card_desc(self, card_title, desc_title):
+ return self.card(card_title) + f" [data-test-desc-title='{desc_title}'] [data-test-value=true]"
+
+ def card_desc_action(self, card_title, desc_title):
+ return self.card(card_title) + f" [data-test-desc-title='{desc_title}'] [data-test-action=true] button"
+
+ def card_button(self, card_title, button_title):
+ return self.card(card_title) + f" button:contains('{button_title}')"
+
+ def dropdown_toggle(self, parent):
+ return parent + " .pf-v5-c-menu-toggle"
+
+ def dropdown_action(self, parent, title):
+ return parent + f" .pf-v5-c-menu button:contains('{title}')"
+
+ def dropdown_description(self, parent, title):
+ return parent + f" .pf-v5-c-menu button:contains('{title}') .pf-v5-c-menu__item-description"
+
+ def click_dropdown(self, parent, title):
+ self.browser.click(self.dropdown_toggle(parent))
+ self.browser.click(self.dropdown_action(parent, title))
+
+ def click_card_dropdown(self, card_title, button_title):
+ self.click_dropdown(self.card_header(card_title), button_title)
+
+ def click_devices_dropdown(self, title):
+ self.click_card_dropdown("Storage", title)
+
+ def check_dropdown_action_disabled(self, parent, title, expected_text):
+ self.browser.click(self.dropdown_toggle(parent))
+ self.browser.wait_visible(self.dropdown_action(parent, title) + "[disabled]")
+ self.browser.wait_text(self.dropdown_description(parent, title), expected_text)
+ self.browser.click(self.dropdown_toggle(parent))
+
+ def wait_mounted(self, card_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_not_in_text(self.card_desc(card_title, "Mount point"),
+ "The filesystem is not mounted.")
+
+ def wait_not_mounted(self, card_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_in_text(self.card_desc(card_title, "Mount point"),
+ "The filesystem is not mounted.")
+
+ def wait_card_button_disabled(self, card_title, button_title):
+ with self.browser.wait_timeout(30):
+ self.browser.wait_visible(self.card_button(card_title, button_title) + ":disabled")
+
+
+class StorageCase(MachineCase, StorageHelpers):
+
+ def setUp(self):
+
+ if self.image in ["fedora-coreos", "rhel4edge"]:
+ self.skipTest("No udisks/cockpit-storaged on OSTree images")
+
+ super().setUp()
+
+ ver = self.machine.execute("busctl --system get-property org.freedesktop.UDisks2 /org/freedesktop/UDisks2/Manager org.freedesktop.UDisks2.Manager Version || true")
+ m = re.match('s "(.*)"', ver)
+ if m:
+ self.storaged_version = list(map(int, m.group(1).split(".")))
+ else:
+ self.storaged_version = [0]
+
+ crypto_types = self.machine.execute("busctl --system get-property org.freedesktop.UDisks2 /org/freedesktop/UDisks2/Manager org.freedesktop.UDisks2.Manager SupportedEncryptionTypes || true")
+ if "luks2" in crypto_types:
+ self.default_crypto_type = "luks2"
+ else:
+ self.default_crypto_type = "luks1"
+
+ if self.image.startswith("rhel-8") or self.image.startswith("centos-8"):
+ # HACK: missing /etc/crypttab file upsets udisks: https://github.com/storaged-project/udisks/pull/835
+ self.machine.write("/etc/crypttab", "")
+
+ # starting out with empty PCP logs and pmlogger not running causes these metrics channel messages
+ self.allow_journal_messages("pcp-archive: no such metric: disk.*")
+
+ # UDisks2 invalidates the Size property and cockpit-bridge
+ # gets it immediately. But sometimes the interface is already
+ # gone.
+ self.allow_journal_messages("org.freedesktop.UDisks2: couldn't get property org.freedesktop.UDisks2.Filesystem Size .* No such interface.*")
diff --git a/test/common/tap-cdp b/test/common/tap-cdp
new file mode 100755
index 0000000..a70d530
--- /dev/null
+++ b/test/common/tap-cdp
@@ -0,0 +1,119 @@
+#!/usr/bin/python3
+
+#
+# Copyright (C) 2017 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+#
+
+import argparse
+import os
+import re
+import subprocess
+import sys
+
+import cdp
+
+tap_line_re = re.compile(r'^(ok [0-9]+|not ok [0-9]+|bail out!|[0-9]+\.\.[0-9]+|# )', re.IGNORECASE)
+
+parser = argparse.ArgumentParser(description="A CDP driver for QUnit which outputs TAP")
+parser.add_argument("server", help="path to the test-server and the test page to run", nargs=argparse.REMAINDER)
+
+# Strip prefix from url
+# We need this to compensate for automake test generation behavior:
+# The tests are called with the path (relative to the build directory) of the testfile,
+# but from the build directory. Some tests make assumptions regarding the structure of the
+# filename. In order to make sure that they receive the same name, regardless of actual
+# build directory location, we need to strip that prefix (path from build to source directory)
+# from the filename
+parser.add_argument("--strip", dest="strip", help="strip prefix from test file paths")
+
+opts = parser.parse_args()
+
+# argparse sometimes forgets to remove this on argparse.REMAINDER args
+if opts.server[0] == '--':
+ opts.server = opts.server[1:]
+
+# The test file is the last argument, but 'server' might contain arbitrary
+# amount of options. We cannot express this with argparse, so take it apart
+# manually.
+opts.test = opts.server[-1]
+opts.server = opts.server[:-1]
+
+if opts.strip and opts.test.startswith(opts.strip):
+ opts.test = opts.test[len(opts.strip):]
+
+cdp = cdp.CDP("C.utf8")
+
+try:
+ cdp.browser.path(cdp.show_browser)
+except SystemError:
+ print('1..0 # skip web browser not found')
+ sys.exit(0)
+
+# pass the address through a separate fd, so that we can see g_debug() messages (which go to stdout)
+(addr_r, addr_w) = os.pipe()
+env = os.environ.copy()
+env["TEST_SERVER_ADDRESS_FD"] = str(addr_w)
+
+server = subprocess.Popen(opts.server,
+ stdin=subprocess.DEVNULL,
+ pass_fds=(addr_w,),
+ close_fds=True,
+ env=env)
+os.close(addr_w)
+address = os.read(addr_r, 1000).decode()
+os.close(addr_r)
+
+cdp.invoke("Page.navigate", url=address + '/' + opts.test)
+
+success = True
+ignore_resource_errors = False
+
+for t, message in cdp.read_log():
+
+ # fail on browser level errors
+ if t == 'cdp':
+ if message['level'] == "error":
+ if ignore_resource_errors and "Failed to load resource" in message["text"]:
+ continue
+ success = False
+ break
+ else:
+ continue
+
+ if message == 'cockpittest-tap-done':
+ break
+ elif message == 'cockpittest-tap-error':
+ success = False
+ break
+ elif message == 'cockpittest-tap-expect-resource-error':
+ ignore_resource_errors = True
+ continue
+
+ # TAP lines go to stdout, everything else to stderr
+ if tap_line_re.match(message):
+ if message.startswith('not ok'):
+ success = False
+ print(message)
+ else:
+ print(message, file=sys.stderr)
+
+
+server.terminate()
+server.wait()
+cdp.kill()
+
+if not success:
+ sys.exit(1)
diff --git a/test/common/test-functions.js b/test/common/test-functions.js
new file mode 100644
index 0000000..c2c28bd
--- /dev/null
+++ b/test/common/test-functions.js
@@ -0,0 +1,360 @@
+/* eslint no-unused-vars: 0 */
+
+/*
+ * These are routines used by our testing code.
+ *
+ * jQuery is not necessarily present. Don't rely on it
+ * for routine operations.
+ */
+
+function ph_select(sel) {
+ if (!window.Sizzle) {
+ return Array.from(document.querySelectorAll(sel));
+ }
+
+ if (sel.includes(":contains(")) {
+ if (!window.Sizzle) {
+ throw new Error("Using ':contains' when window.Sizzle is not available.");
+ }
+ return window.Sizzle(sel);
+ } else {
+ return Array.from(document.querySelectorAll(sel));
+ }
+}
+
+function ph_only(els, sel) {
+ if (els.length === 0)
+ throw new Error(sel + " not found");
+ if (els.length > 1)
+ throw new Error(sel + " is ambiguous");
+ return els[0];
+}
+
+function ph_find (sel) {
+ const els = ph_select(sel);
+ return ph_only(els, sel);
+}
+
+function ph_count(sel) {
+ const els = ph_select(sel);
+ return els.length;
+}
+
+function ph_count_check(sel, expected_num) {
+ return (ph_count(sel) == expected_num);
+}
+
+function ph_val (sel) {
+ const el = ph_find(sel);
+ if (el.value === undefined)
+ throw new Error(sel + " does not have a value");
+ return el.value;
+}
+
+function ph_set_val (sel, val) {
+ const el = ph_find(sel);
+ if (el.value === undefined)
+ throw new Error(sel + " does not have a value");
+ el.value = val;
+ const ev = new Event("change", { bubbles: true, cancelable: false });
+ el.dispatchEvent(ev);
+}
+
+function ph_has_val (sel, val) {
+ return ph_val(sel) == val;
+}
+
+function ph_collected_text_is (sel, val) {
+ const els = ph_select(sel);
+ const rest = els.map(el => {
+ if (el.textContent === undefined)
+ throw new Error(sel + " can not have text");
+ return el.textContent.replaceAll("\xa0", " ");
+ }).join("");
+ return rest === val;
+}
+
+function ph_text (sel) {
+ const el = ph_find(sel);
+ if (el.textContent === undefined)
+ throw new Error(sel + " can not have text");
+ // 0xa0 is a non-breakable space, which is a rendering detail of Chromium
+ // and awkward to handle in tests; turn it into normal spaces
+ return el.textContent.replaceAll("\xa0", " ");
+}
+
+function ph_attr (sel, attr) {
+ return ph_find(sel).getAttribute(attr);
+}
+
+function ph_set_attr (sel, attr, val) {
+ const el = ph_find(sel);
+ if (val === null || val === undefined)
+ el.removeAttribute(attr);
+ else
+ el.setAttribute(attr, val);
+
+ const ev = new Event("change", { bubbles: true, cancelable: false });
+ el.dispatchEvent(ev);
+}
+
+function ph_has_attr (sel, attr, val) {
+ return ph_attr(sel, attr) == val;
+}
+
+function ph_attr_contains (sel, attr, val) {
+ const a = ph_attr(sel, attr);
+ return a && a.indexOf(val) > -1;
+}
+
+function ph_mouse(sel, type, x, y, btn, ctrlKey, shiftKey, altKey, metaKey) {
+ const el = ph_find(sel);
+
+ /* The element has to be visible, and not collapsed */
+ if (el.offsetWidth <= 0 && el.offsetHeight <= 0 && el.tagName != 'svg')
+ throw new Error(sel + " is not visible");
+
+ /* The event has to actually work */
+ let processed = false;
+ function handler() {
+ processed = true;
+ }
+
+ el.addEventListener(type, handler, true);
+
+ let elp = el;
+ let left = elp.offsetLeft || 0;
+ let top = elp.offsetTop || 0;
+ while (elp.offsetParent) {
+ elp = elp.offsetParent;
+ left += elp.offsetLeft;
+ top += elp.offsetTop;
+ }
+
+ let detail = 0;
+ if (["click", "mousedown", "mouseup"].indexOf(type) > -1)
+ detail = 1;
+ else if (type === "dblclick")
+ detail = 2;
+
+ const ev = new MouseEvent(type, {
+ bubbles: true,
+ cancelable: true,
+ view: window,
+ detail,
+ screenX: left + x,
+ screenY: top + y,
+ clientX: left + x,
+ clientY: top + y,
+ button: btn,
+ ctrlKey: ctrlKey || false,
+ shiftKey: shiftKey || false,
+ altKey: altKey || false,
+ metaKey: metaKey || false
+ });
+
+ el.dispatchEvent(ev);
+
+ el.removeEventListener(type, handler, true);
+
+ /* It really had to work */
+ if (!processed)
+ throw new Error(sel + " is disabled or somehow doesn't process events");
+}
+
+function ph_get_checked (sel) {
+ const el = ph_find(sel);
+ if (el.checked === undefined)
+ throw new Error(sel + " is not checkable");
+
+ return el.checked;
+}
+
+function ph_set_checked (sel, val) {
+ const el = ph_find(sel);
+ if (el.checked === undefined)
+ throw new Error(sel + " is not checkable");
+
+ if (el.checked != val)
+ ph_mouse(sel, "click", 0, 0, 0);
+}
+
+function ph_is_visible (sel) {
+ const el = ph_find(sel);
+ return el.tagName == "svg" || ((el.offsetWidth > 0 || el.offsetHeight > 0) && !(el.style.visibility == "hidden" || el.style.display == "none"));
+}
+
+function ph_is_present(sel) {
+ const els = ph_select(sel);
+ return els.length > 0;
+}
+
+function ph_in_text (sel, text) {
+ return ph_text(sel).indexOf(text) != -1;
+}
+
+function ph_text_is (sel, text) {
+ return ph_text(sel) == text;
+}
+
+function ph_text_matches (sel, pattern) {
+ return ph_text(sel).match(pattern);
+}
+
+function ph_go(href) {
+ if (href.indexOf("#") === 0) {
+ window.location.hash = href;
+ } else {
+ if (window.name.indexOf("cockpit1") !== 0)
+ throw new Error("ph_go() called in non cockpit window");
+ const control = {
+ command: "jump",
+ location: href
+ };
+ window.parent.postMessage("\n" + JSON.stringify(control), "*");
+ }
+}
+
+function ph_focus(sel) {
+ ph_find(sel).focus();
+}
+
+function ph_scrollIntoViewIfNeeded(sel) {
+ ph_find(sel).scrollIntoViewIfNeeded();
+}
+
+function ph_blur(sel) {
+ ph_find(sel).blur();
+}
+
+function ph_blur_active() {
+ const elt = window.document.activeElement;
+ if (elt)
+ elt.blur();
+}
+
+class PhWaitCondTimeout extends Error {
+ constructor(description) {
+ if (description && description.apply)
+ description = description.apply();
+ if (description)
+ super(description);
+ else
+ super("condition did not become true");
+ }
+}
+
+function ph_wait_cond(cond, timeout, error_description) {
+ return new Promise((resolve, reject) => {
+ // poll every 100 ms for now; FIXME: poll less often and re-check on mutations using
+ // https://developer.mozilla.org/en-US/docs/Web/API/MutationObserver
+ let stepTimer = null;
+ let last_err = null;
+ const tm = window.setTimeout(() => {
+ if (stepTimer)
+ window.clearTimeout(stepTimer);
+ reject(last_err || new PhWaitCondTimeout(error_description));
+ }, timeout);
+ function step() {
+ try {
+ if (cond()) {
+ window.clearTimeout(tm);
+ resolve();
+ return;
+ }
+ } catch (err) {
+ last_err = err;
+ }
+ stepTimer = window.setTimeout(step, 100);
+ }
+ step();
+ });
+}
+
+function currentFrameAbsolutePosition() {
+ let currentWindow = window;
+ let currentParentWindow;
+ const positions = [];
+ let rect;
+
+ while (currentWindow !== window.top) {
+ currentParentWindow = currentWindow.parent;
+ for (let idx = 0; idx < currentParentWindow.frames.length; idx++)
+ if (currentParentWindow.frames[idx] === currentWindow) {
+ for (const frameElement of currentParentWindow.document.getElementsByTagName('iframe')) {
+ if (frameElement.contentWindow === currentWindow) {
+ rect = frameElement.getBoundingClientRect();
+ positions.push({ x: rect.x, y: rect.y });
+ }
+ }
+ currentWindow = currentParentWindow;
+ break;
+ }
+ }
+
+ return positions.reduce((accumulator, currentValue) => {
+ return {
+ x: accumulator.x + currentValue.x,
+ y: accumulator.y + currentValue.y
+ };
+ }, { x: 0, y: 0 });
+}
+
+function flatten(array_of_arrays) {
+ if (array_of_arrays.length > 0)
+ return Array.prototype.concat.apply([], array_of_arrays);
+ else
+ return [];
+}
+
+function ph_selector_clips(sels) {
+ const f = currentFrameAbsolutePosition();
+ const elts = flatten(sels.map(ph_select));
+ return elts.map(e => {
+ const r = e.getBoundingClientRect();
+ return { x: r.x + f.x, y: r.y + f.y, width: r.width, height: r.height, scale: 1 };
+ });
+}
+
+function ph_element_clip(sel) {
+ ph_find(sel); // just to make sure it is not ambiguous
+ return ph_selector_clips([sel])[0];
+}
+
+function ph_count_animations(sel) {
+ return ph_find(sel).getAnimations({ subtree: true }).length;
+}
+
+function ph_set_texts(new_texts) {
+ for (const sel in new_texts) {
+ const elts = ph_select(sel);
+ if (elts.length == 0)
+ throw new Error(sel + " not found");
+ for (let elt of elts) {
+ // We have to be careful to not replace any actual nodes
+ // in the DOM since that would cause React to fail later
+ // when it tries to remove some of its nodes that are no
+ // longer in the DOM. This means that setting the
+ // "textContent" property is out, for example.
+ //
+ // Instead, we insist on finding an actual "Text" node
+ // that we then modify. If the given selector results in
+ // elements that have other elements in them, we refuse to
+ // mock them.
+ //
+ // However, for convenience, this function digs into
+ // elements that have exactly one other child element.
+ while (elt.children.length == 1)
+ elt = elt.children[0];
+ if (elt.children.length != 0)
+ throw new Error(sel + " can not be mocked since it contains more than text");
+ let subst = new_texts[sel];
+ for (const n of elt.childNodes) {
+ if (n.nodeType == 3) { // 3 == TEXT
+ n.data = subst;
+ subst = "";
+ }
+ }
+ }
+ }
+}
diff --git a/test/common/testlib.py b/test/common/testlib.py
new file mode 100644
index 0000000..cbb9bb7
--- /dev/null
+++ b/test/common/testlib.py
@@ -0,0 +1,2535 @@
+# This file is part of Cockpit.
+#
+# Copyright (C) 2013 Red Hat, Inc.
+#
+# Cockpit is free software; you can redistribute it and/or modify it
+# under the terms of the GNU Lesser General Public License as published by
+# the Free Software Foundation; either version 2.1 of the License, or
+# (at your option) any later version.
+#
+# Cockpit is distributed in the hope that it will be useful, but
+# WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+# Lesser General Public License for more details.
+#
+# You should have received a copy of the GNU Lesser General Public License
+# along with Cockpit; If not, see <http://www.gnu.org/licenses/>.
+
+"""Tools for writing Cockpit test cases."""
+
+import argparse
+import base64
+import errno
+import fnmatch
+import functools
+import glob
+import io
+import json
+import os
+import re
+import shutil
+import socket
+import subprocess
+import sys
+import tempfile
+import time
+import traceback
+import unittest
+from time import sleep
+from typing import Any, Callable, Dict, List, Optional, Union
+
+import cdp
+import testvm
+from lcov import write_lcov
+from lib.constants import OSTREE_IMAGES
+
+try:
+ from PIL import Image
+except ImportError:
+ Image = None
+
+BASE_DIR = os.path.realpath(f'{__file__}/../../..')
+TEST_DIR = f'{BASE_DIR}/test'
+BOTS_DIR = f'{BASE_DIR}/bots'
+
+os.environ["PATH"] = "{0}:{1}:{2}".format(os.environ.get("PATH"), BOTS_DIR, TEST_DIR)
+
+# Be careful when changing this string, check in cockpit-project/bots where it is being used
+UNEXPECTED_MESSAGE = "FAIL: Test completed, but found unexpected "
+PIXEL_TEST_MESSAGE = "Some pixel tests have failed"
+
+__all__ = (
+ # Test definitions
+ 'test_main',
+ 'arg_parser',
+ 'Browser',
+ 'MachineCase',
+ 'nondestructive',
+ 'no_retry_when_changed',
+ 'onlyImage',
+ 'skipImage',
+ 'skipDistroPackage',
+ 'skipOstree',
+ 'skipBrowser',
+ 'todo',
+ 'todoPybridge',
+ 'todoPybridgeRHEL8',
+ 'timeout',
+ 'Error',
+
+ 'sit',
+ 'wait',
+ 'opts',
+ 'TEST_DIR',
+ 'UNEXPECTED_MESSAGE',
+ 'PIXEL_TEST_MESSAGE'
+)
+
+# Command line options
+opts = argparse.Namespace()
+opts.sit = False
+opts.trace = False
+opts.attachments = None
+opts.revision = None
+opts.address = None
+opts.jobs = 1
+opts.fetch = True
+opts.coverage = False
+
+# Browser layouts
+#
+# A browser can be switched into a number of different layouts, such
+# as "desktop" and "mobile". A default set of layouts is defined
+# here, but projects can override this with a file called
+# "test/browser-layouts.json".
+#
+# Each layout defines the size of the shell (where the main navigation
+# is) and also the size of the content iframe (where the actual page
+# like "Networking" or "Overview" is displayed).
+#
+# When the browser layout is switched (by calling Browset.set_layout),
+# this will either set the shell size or the content size, depending
+# on which frame is current (as set by Browser.enter_page or
+# Browser.leave_page).
+#
+# This makes sure that pixel tests for the whole content iframe are
+# always the exact size as specified in the layout definition, and
+# don't change size when the navigation stuff in the shell changes.
+#
+# The browser starts out in the first layout of this list, which is
+# "desktop" by default.
+
+default_layouts = [
+ {
+ "name": "desktop",
+ "theme": "light",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+ {
+ "name": "medium",
+ "theme": "light",
+ "is_mobile": False,
+ "shell_size": [1280, 768],
+ "content_size": [1040, 698]
+ },
+ {
+ "name": "mobile",
+ "theme": "light",
+ "shell_size": [414, 1920],
+ "content_size": [414, 1856]
+ },
+ {
+ "name": "dark",
+ "theme": "dark",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+ {
+ "name": "rtl",
+ "theme": "light",
+ "shell_size": [1920, 1200],
+ "content_size": [1680, 1130]
+ },
+]
+
+
+def attach(filename: str, move: bool = False):
+ """Put a file into the attachments directory.
+
+ :param filename: file to put in attachments directory
+ :param move: set this to true to move dynamically generated files which
+ are not touched by destructive tests. (default False)
+ """
+ if not opts.attachments:
+ return
+ dest = os.path.join(opts.attachments, os.path.basename(filename))
+ if os.path.exists(filename) and not os.path.exists(dest):
+ if move:
+ shutil.move(filename, dest)
+ else:
+ shutil.copy(filename, dest)
+
+
+def unique_filename(base, ext):
+ for i in range(20):
+ if i == 0:
+ f = f"{base}.{ext}"
+ else:
+ f = f"{base}-{i}.{ext}"
+ if not os.path.exists(f):
+ return f
+ return f"{base}.{ext}"
+
+
+class Browser:
+ def __init__(self, address, label, machine, pixels_label=None, coverage_label=None, port=None):
+ if ":" in address:
+ self.address, _, self.port = address.rpartition(":")
+ else:
+ self.address = address
+ self.port = 9090
+ if port is not None:
+ self.port = port
+ self.default_user = "admin"
+ self.label = label
+ self.pixels_label = pixels_label
+ self.used_pixel_references = set()
+ self.coverage_label = coverage_label
+ self.machine = machine
+ path = os.path.dirname(__file__)
+ sizzle_js = os.path.join(path, "../../node_modules/sizzle/dist/sizzle.js")
+ helpers = [os.path.join(path, "test-functions.js")]
+ if os.path.exists(sizzle_js):
+ helpers.append(sizzle_js)
+ self.cdp = cdp.CDP("C.utf8", verbose=opts.trace, trace=opts.trace,
+ inject_helpers=helpers,
+ start_profile=coverage_label is not None)
+ self.password = "foobar"
+ self.timeout_factor = int(os.getenv("TEST_TIMEOUT_FACTOR", "1"))
+ self.failed_pixel_tests = 0
+ self.allow_oops = False
+ self.body_clip = None
+ try:
+ with open(f'{TEST_DIR}/browser-layouts.json') as fp:
+ self.layouts = json.load(fp)
+ except FileNotFoundError:
+ self.layouts = default_layouts
+ # Firefox CDP does not support setting EmulatedMedia
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1549434
+ if self.cdp.browser.name != "chromium":
+ self.layouts = [layout for layout in self.layouts if layout["theme"] != "dark"]
+ self.current_layout = None
+
+ def allow_download(self) -> None:
+ """Allow browser downloads"""
+ if self.cdp.browser.name == "chromium":
+ self.cdp.invoke("Page.setDownloadBehavior", behavior="allow", downloadPath=self.cdp.download_dir)
+
+ def open(self, href: str, cookie: Optional[Dict[str, str]] = None, tls: bool = False):
+ """Load a page into the browser.
+
+ :param href: the path of the Cockpit page to load, such as "/users". Either PAGE or URL needs to be given.
+ :param cookie: a dictionary object representing a cookie.
+ :param tls: load the page using https (default False)
+
+ Raises:
+ Error: When a timeout occurs waiting for the page to load.
+ """
+ if href.startswith("/"):
+ schema = tls and "https" or "http"
+ href = "%s://%s:%s%s" % (schema, self.address, self.port, href)
+
+ if not self.current_layout and os.environ.get("TEST_SHOW_BROWSER") in [None, "pixels"]:
+ self.current_layout = self.layouts[0]
+ size = self.current_layout["shell_size"]
+ self._set_window_size(size[0], size[1])
+ if cookie:
+ self.cdp.invoke("Network.setCookie", **cookie)
+
+ self.switch_to_top()
+ opts = {}
+ if self.cdp.browser.name == "firefox":
+ # by default, Firefox optimizes this away if the current and the given href URL
+ # are the same (Like in TestKeys.testAuthorizedKeys).
+ # Force a reload in this case, to make tests and the waitPageLoad below predictable
+ # But that option has the inverse effect with Chromium (argh)
+ opts["transitionType"] = "reload"
+ elif self.cdp.browser.name == 'chromium':
+ # Chromium also optimizes this away, but doesn't have a knob to force loading
+ # so load the blank page first
+ self.cdp.invoke("Page.navigate", url="about:blank")
+ self.cdp.invoke("waitPageLoad", timeout=5)
+ self.cdp.invoke("Page.navigate", url=href, **opts)
+ self.cdp.invoke("waitPageLoad", timeout=self.cdp.timeout)
+
+ def set_user_agent(self, ua: str):
+ """Set the user agent of the browser
+
+ :param ua: user agent string
+ :type ua: str
+ """
+ self.cdp.invoke("Emulation.setUserAgentOverride", userAgent=ua)
+
+ def reload(self, ignore_cache: bool = False):
+ """Reload the current page
+
+ :param ignore_cache: if true browser cache is ignored (default False)
+ :type ignore_cache: bool
+ """
+
+ self.switch_to_top()
+ self.wait_js_cond("ph_select('iframe.container-frame').every(function (e) { return e.getAttribute('data-loaded'); })")
+ self.cdp.invoke("reloadPageAndWait", ignoreCache=ignore_cache)
+
+ self.machine.allow_restart_journal_messages()
+
+ def switch_to_frame(self, name: str):
+ """Switch to frame in browser tab
+
+ Each page has a main frame and can have multiple subframes, usually
+ iframes.
+
+ :param name: frame name
+ """
+ self.cdp.set_frame(name)
+
+ def switch_to_top(self):
+ """Switch to the main frame
+
+ Switch to the main frame from for example an iframe.
+ """
+ self.cdp.set_frame(None)
+
+ def upload_file(self, selector: str, file: str):
+ r = self.cdp.invoke("Runtime.evaluate", expression='document.querySelector(%s)' % jsquote(selector))
+ objectId = r["result"]["objectId"]
+ self.cdp.invoke("DOM.setFileInputFiles", files=[file], objectId=objectId)
+
+ def raise_cdp_exception(self, func, arg, details, trailer=None):
+ # unwrap a typical error string
+ if details.get("exception", {}).get("type") == "string":
+ msg = details["exception"]["value"]
+ elif details.get("text", None):
+ msg = details.get("text", None)
+ else:
+ msg = str(details)
+ if trailer:
+ msg += "\n" + trailer
+ raise Error("%s(%s): %s" % (func, arg, msg))
+
+ def inject_js(self, code: str):
+ """Execute JS code that does not return anything
+
+ :param code: a string containing JavaScript code
+ :type code: str
+ """
+ self.cdp.invoke("Runtime.evaluate", expression=code, trace=code,
+ silent=False, awaitPromise=True, returnByValue=False, no_trace=True)
+
+ def eval_js(self, code: str, no_trace: bool = False) -> Optional[Any]:
+ """Execute JS code that returns something
+
+ :param code: a string containing JavaScript code
+ :param no_trace: do not print information about unknown return values (default False)
+ """
+ result = self.cdp.invoke("Runtime.evaluate", expression=code, trace=code,
+ silent=False, awaitPromise=True, returnByValue=True, no_trace=no_trace)
+ if "exceptionDetails" in result:
+ self.raise_cdp_exception("eval_js", code, result["exceptionDetails"])
+ _type = result.get("result", {}).get("type")
+ if _type == 'object' and result["result"].get("subtype", "") == "error":
+ raise Error(result["result"]["description"])
+ if _type == "undefined":
+ return None
+ if _type and "value" in result["result"]:
+ return result["result"]["value"]
+
+ if opts.trace:
+ print("eval_js(%s): cannot interpret return value %s" % (code, result))
+ return None
+
+ def call_js_func(self, func: str, *args: Any) -> Optional[Any]:
+ """Call a JavaScript function
+
+ :param func: JavaScript function to call
+ :param args: arguments for the JavaScript function
+ """
+ return self.eval_js("%s(%s)" % (func, ','.join(map(jsquote, args))))
+
+ def set_mock(self, mock: Dict[str, str], base: Optional[str] = ""):
+ """Replace some DOM elements with mock text
+
+ The 'mock' parameter is a dictionary from CSS selectors to the
+ text that the elements matching the selector should be
+ replaced with.
+
+ XXX - There is no way to easily undo the effects of this
+ function. There is no coordination with React. This
+ will improve as necessary.
+
+ :param mock: the mock data, see above
+ :param base: if given, all selectors are relative to this one
+ """
+ self.call_js_func('ph_set_texts', {base + " " + k: v for k, v in mock.items()})
+
+ def cookie(self, name: str):
+ """Retrieve a browser cookie by name
+
+ :param name: the name of the cookie
+ :type name: str
+ """
+ cookies = self.cdp.invoke("Network.getCookies")
+ for c in cookies["cookies"]:
+ if c["name"] == name:
+ return c
+ return None
+
+ def go(self, url_hash: str):
+ self.call_js_func('ph_go', url_hash)
+
+ def mouse(self, selector: str, event: str, x: int = 0, y: int = 0, btn: int = 0, ctrlKey: bool = False, shiftKey: bool = False, altKey: bool = False, metaKey: bool = False):
+ """Simulate a browser mouse event
+
+ :param selector: the element to interact with
+ :param type: the mouse event to simulate, for example mouseenter, mouseleave, mousemove, click
+ :param x: the x coordinate
+ :param y: the y coordinate
+ :param btn: mouse button to click https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent/buttons
+ :param crtlKey: press the ctrl key
+ :param shiftKey: press the shift key
+ :param altKey: press the alt key
+ :param metaKey: press the meta key
+ """
+ self.wait_visible(selector)
+ self.call_js_func('ph_mouse', selector, event, x, y, btn, ctrlKey, shiftKey, altKey, metaKey)
+
+ def click(self, selector: str):
+ """Click on a ui element
+
+ :param selector: the selector to click on
+ """
+ self.mouse(selector + ":not([disabled]):not([aria-disabled=true])", "click", 0, 0, 0)
+
+ def val(self, selector: str):
+ """Get the value attribute of a selector.
+
+ :param selector: the selector to get the value of
+ """
+ self.wait_visible(selector)
+ return self.call_js_func('ph_val', selector)
+
+ def set_val(self, selector: str, val):
+ """Set the value attribute of a non disabled DOM element.
+
+ This also emits a change DOM change event.
+
+ :param selector: the selector to set the value of
+ :param val: the value to set
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_val', selector, val)
+
+ def text(self, selector: str):
+ """Get an element's textContent value.
+
+ :param selector: the selector to get the value of
+ """
+ self.wait_visible(selector)
+ return self.call_js_func('ph_text', selector)
+
+ def attr(self, selector: str, attr):
+ """Get the value of a given attribute of an element.
+
+ :param selector: the selector to get the attribute of
+ :param attr: the DOM element attribute
+ """
+ self._wait_present(selector)
+ return self.call_js_func('ph_attr', selector, attr)
+
+ def set_attr(self, selector, attr, val):
+ """Set an attribute value of an element.
+
+ :param selector: the selector
+ :param attr: the element attribute
+ :param val: the value of the attribute
+ """
+ self._wait_present(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_attr', selector, attr, val)
+
+ def get_checked(self, selector: str):
+ """Get checked state of a given selector.
+
+ :param selector: the selector
+ :return: the checked state
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ return self.call_js_func('ph_get_checked', selector)
+
+ def set_checked(self, selector: str, val):
+ """Set checked state of a given selector.
+
+ :param selector: the selector
+ :param val: boolean value to enable or disable checkbox
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_set_checked', selector, val)
+
+ def focus(self, selector: str):
+ """Set focus on selected element.
+
+ :param selector: the selector
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_focus', selector)
+
+ def blur(self, selector: str):
+ """Remove keyboard focus from selected element.
+
+ :param selector: the selector
+ """
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ self.call_js_func('ph_blur', selector)
+
+ # TODO: Unify them so we can have only one
+ def key_press(self, keys: str, modifiers: int = 0, use_ord: bool = False):
+ if self.cdp.browser.name == "chromium":
+ self._key_press_chromium(keys, modifiers, use_ord)
+ else:
+ self._key_press_firefox(keys, modifiers, use_ord)
+
+ def _key_press_chromium(self, keys: str, modifiers: int = 0, use_ord=False):
+ for key in keys:
+ args = {"type": "keyDown", "modifiers": modifiers}
+
+ # If modifiers are used we need to pass windowsVirtualKeyCode which is
+ # basically the asci decimal representation of the key
+ args["text"] = key
+ if use_ord:
+ args["windowsVirtualKeyCode"] = ord(key)
+ elif (not key.isalnum() and ord(key) < 32) or modifiers != 0:
+ args["windowsVirtualKeyCode"] = ord(key.upper())
+ else:
+ args["key"] = key
+
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ def _key_press_firefox(self, keys: str, modifiers: int = 0, use_ord: bool = False):
+ # https://python-reference.readthedocs.io/en/latest/docs/str/ASCII.html
+ # Both line feed and carriage return are normalized to Enter (https://html.spec.whatwg.org/multipage/form-elements.html)
+ keyMap = {
+ 8: "Backspace", # Backspace key
+ 9: "Tab", # Tab key
+ 10: "Enter", # Enter key (normalized from line feed)
+ 13: "Enter", # Enter key (normalized from carriage return)
+ 27: "Escape", # Escape key
+ 37: "ArrowLeft", # Arrow key left
+ 40: "ArrowDown", # Arrow key down
+ 45: "Insert", # Insert key
+ }
+ for key in keys:
+ args = {"type": "keyDown", "modifiers": modifiers}
+
+ args["key"] = key
+ if ord(key) < 32 or use_ord:
+ args["key"] = keyMap[ord(key)]
+
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+ args["type"] = "keyUp"
+ self.cdp.invoke("Input.dispatchKeyEvent", **args)
+
+ def select_from_dropdown(self, selector: str, value):
+ self.wait_visible(selector + ':not([disabled]):not([aria-disabled=true])')
+ text_selector = f"{selector} option[value='{value}']"
+ self._wait_present(text_selector)
+ self.set_val(selector, value)
+ self.wait_val(selector, value)
+
+ def select_PF4(self, selector: str, value):
+ self.click(f"{selector}:not([disabled]):not([aria-disabled=true])")
+ select_entry = f"{selector} + ul button:contains('{value}')"
+ self.click(select_entry)
+ if self.is_present(f"{selector}.pf-m-typeahead"):
+ self.wait_val(f"{selector} > div input[type=text]", value)
+ else:
+ self.wait_text(f"{selector} .pf-v5-c-select__toggle-text", value)
+
+ def set_input_text(self, selector: str, val: str, append: bool = False, value_check: bool = True, blur: bool = True):
+ self.focus(selector)
+ if not append:
+ self.key_press("a", 2) # Ctrl + a
+ if val == "":
+ self.key_press("\b") # Backspace
+ else:
+ self.key_press(val)
+ if blur:
+ self.blur(selector)
+
+ if value_check:
+ self.wait_val(selector, val)
+
+ def set_file_autocomplete_val(self, group_identifier: str, location: str):
+ self.set_input_text(f"{group_identifier} .pf-v5-c-select__toggle-typeahead input", location)
+ # click away the selection list, to force a state update
+ self.click(f"{group_identifier} .pf-v5-c-select__toggle-typeahead")
+ self.wait_not_present(f"{group_identifier} .pf-v5-c-select__menu")
+
+ def wait_timeout(self, timeout: int):
+ browser = self
+
+ class WaitParamsRestorer():
+ def __init__(self, timeout):
+ self.timeout = timeout
+
+ def __enter__(self):
+ pass
+
+ def __exit__(self, type_, value, traceback):
+ browser.cdp.timeout = self.timeout
+ r = WaitParamsRestorer(self.cdp.timeout)
+ self.cdp.timeout = timeout
+ return r
+
+ def wait(self, predicate: Callable):
+ for _ in range(self.cdp.timeout * self.timeout_factor * 5):
+ val = predicate()
+ if val:
+ return val
+ time.sleep(0.2)
+ raise Error('timed out waiting for predicate to become true')
+
+ def wait_js_cond(self, cond: str, error_description: str = "null"):
+ count = 0
+ timeout = self.cdp.timeout * self.timeout_factor
+ start = time.time()
+ while True:
+ count += 1
+ try:
+ result = self.cdp.invoke("Runtime.evaluate",
+ expression="ph_wait_cond(() => %s, %i, %s)" % (cond, timeout * 1000, error_description),
+ silent=False, awaitPromise=True, trace="wait: " + cond)
+ if "exceptionDetails" in result:
+ if self.cdp.browser.name == "firefox" and count < 20 and "ph_wait_cond is not defined" in result["exceptionDetails"].get("text", ""):
+ time.sleep(0.1)
+ continue
+ trailer = "\n".join(self.cdp.get_js_log())
+ self.raise_cdp_exception("timeout\nwait_js_cond", cond, result["exceptionDetails"], trailer)
+ if timeout > 0:
+ duration = time.time() - start
+ percent = int(duration / timeout * 100)
+ if percent >= 50:
+ print(f"WARNING: Waiting for {cond} took {duration:.1f} seconds, which is {percent}% of the timeout.")
+ return
+ except RuntimeError as e:
+ data = e.args[0]
+ if count < 20 and isinstance(data, dict) and "response" in data and data["response"].get("message") in ["Execution context was destroyed.", "Cannot find context with specified id"]:
+ time.sleep(1)
+ else:
+ raise e
+
+ def wait_js_func(self, func: str, *args: Any):
+ self.wait_js_cond("%s(%s)" % (func, ','.join(map(jsquote, args))))
+
+ def is_present(self, selector: str) -> Optional[bool]:
+ return self.call_js_func('ph_is_present', selector)
+
+ def _wait_present(self, selector: str):
+ self.wait_js_func('ph_is_present', selector)
+
+ def wait_not_present(self, selector: str):
+ self.wait_js_func('!ph_is_present', selector)
+
+ def is_visible(self, selector: str) -> Optional[bool]:
+ return self.call_js_func('ph_is_visible', selector)
+
+ def wait_visible(self, selector: str):
+ self._wait_present(selector)
+ self.wait_js_func('ph_is_visible', selector)
+
+ def wait_val(self, selector: str, val: str):
+ self.wait_visible(selector)
+ self.wait_js_func('ph_has_val', selector, val)
+
+ def wait_not_val(self, selector: str, val: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_has_val', selector, val)
+
+ def wait_attr(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('ph_has_attr', selector, attr, val)
+
+ def wait_attr_contains(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('ph_attr_contains', selector, attr, val)
+
+ def wait_attr_not_contains(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('!ph_attr_contains', selector, attr, val)
+
+ def wait_not_attr(self, selector, attr, val):
+ self._wait_present(selector)
+ self.wait_js_func('!ph_has_attr', selector, attr, val)
+
+ def wait_not_visible(self, selector: str):
+ self.wait_js_func('!ph_is_visible', selector)
+
+ def wait_in_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_cond("ph_in_text(%s,%s)" % (jsquote(selector), jsquote(text)),
+ error_description="() => 'actual text: ' + ph_text(%s)" % jsquote(selector))
+
+ def wait_not_in_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_in_text', selector, text)
+
+ def wait_collected_text(self, selector: str, text: str):
+ self.wait_js_func('ph_collected_text_is', selector, text)
+
+ def wait_text(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_cond("ph_text_is(%s,%s)" % (jsquote(selector), jsquote(text)),
+ error_description="() => 'actual text: ' + ph_text(%s)" % jsquote(selector))
+
+ def wait_text_not(self, selector: str, text: str):
+ self.wait_visible(selector)
+ self.wait_js_func('!ph_text_is', selector, text)
+
+ def wait_text_matches(self, selector: str, pattern: str):
+ self.wait_visible(selector)
+ self.wait_js_func('ph_text_matches', selector, pattern)
+
+ def wait_popup(self, elem_id: str):
+ """Wait for a popup to open.
+
+ :param id: the 'id' attribute of the popup.
+ """
+ self.wait_visible('#' + elem_id)
+
+ def wait_popdown(self, elem_id: str):
+ """Wait for a popup to close.
+
+ :param id: the 'id' attribute of the popup.
+ """
+ self.wait_not_visible('#' + elem_id)
+
+ def wait_language(self, lang: str):
+ parts = lang.split("-")
+ code_1 = parts[0]
+ code_2 = parts[0]
+ if len(parts) > 1:
+ code_2 += "_" + parts[1].upper()
+ self.wait_js_cond("cockpit.language == '%s' || cockpit.language == '%s'" % (code_1, code_2))
+
+ def dialog_cancel(self, sel: str, button: str = "button[data-dismiss='modal']"):
+ self.click(sel + " " + button)
+ self.wait_not_visible(sel)
+
+ def enter_page(self, path: str, host: Optional[str] = None, reconnect: bool = True):
+ """Wait for a page to become current.
+
+ :param path: The identifier the page. This is a string starting with "/"
+ :type path: str
+ :param host: The host to connect too
+ :type host: str
+ :param reconnect: Try to reconnect
+ :type reconnect: bool
+ """
+ assert path.startswith("/")
+ if host:
+ frame = host + path
+ else:
+ frame = "localhost" + path
+ frame = "cockpit1:" + frame
+
+ self.switch_to_top()
+
+ while True:
+ try:
+ self._wait_present("iframe.container-frame[name='%s'][data-loaded]" % frame)
+ self.wait_not_visible(".curtains-ct")
+ self.wait_visible("iframe.container-frame[name='%s']" % frame)
+ break
+ except Error as ex:
+ if reconnect and ex.msg.startswith('timeout'):
+ reconnect = False
+ if self.is_present("#machine-reconnect"):
+ self.click("#machine-reconnect")
+ self.wait_not_visible(".curtains-ct")
+ continue
+ raise
+
+ self.switch_to_frame(frame)
+ self._wait_present("body")
+ self.wait_visible("body")
+
+ def leave_page(self):
+ self.switch_to_top()
+
+ def try_login(self, user: Optional[str] = None, password: Optional[str] = None, superuser: Optional[bool] = True, legacy_authorized: Optional[bool] = None):
+ """Fills in the login dialog and clicks the button.
+
+ This differs from login_and_go() by not expecting any particular result.
+
+ :param user: the username to login with
+ :type user: str
+ :param password: the password of the user
+ :type password: str
+ :param superuser: determines whether the new session will try to get Administrative Access (default true)
+ :type superuser: bool
+ :param legacy_authorized: old versions of the login dialog that still
+ have the "[ ] Reuse my password for magic things" checkbox. Such a
+ dialog is encountered when testing against old bastion hosts, for
+ example.
+ """
+ if user is None:
+ user = self.default_user
+ if password is None:
+ password = self.password
+ self.wait_visible("#login")
+ self.set_val('#login-user-input', user)
+ self.set_val('#login-password-input', password)
+ if legacy_authorized is not None:
+ self.set_checked('#authorized-input', legacy_authorized)
+ if superuser is not None:
+ self.eval_js('window.localStorage.setItem("superuser:%s", "%s");' % (user, "any" if superuser else "none"))
+ self.click('#login-button')
+
+ def login_and_go(self, path: Optional[str] = None, user: Optional[str] = None, host: Optional[str] = None,
+ superuser: bool = True, urlroot: Optional[str] = None, tls: bool = False, password: Optional[str] = None,
+ legacy_authorized: Optional[bool] = None):
+ """Fills in the login dialog, clicks the button and navigates to the given path
+
+ :param user: the username to login with
+ :type user: str
+ :param password: the password of the user
+ :type password: str
+ :param superuser: determines whether the new session will try to get Administrative Access (default true)
+ :type superuser: bool
+ :param legacy_authorized: old versions of the login dialog that still
+ have the "[ ] Reuse my password for magic things" checkbox. Such a
+ dialog is encountered when testing against old bastion hosts, for
+ example.
+ """
+ href = path
+ if not href:
+ href = "/"
+ if urlroot:
+ href = urlroot + href
+ if host:
+ href = "/@" + host + href
+ self.open(href, tls=tls)
+
+ self.try_login(user, password, superuser=superuser, legacy_authorized=legacy_authorized)
+
+ self._wait_present('#content')
+ self.wait_visible('#content')
+ if path:
+ self.enter_page(path.split("#")[0], host=host)
+
+ def logout(self):
+ self.assert_no_oops()
+ self.switch_to_top()
+
+ self.wait_visible("#toggle-menu")
+ if self.is_present("button#machine-reconnect") and self.is_visible("button#machine-reconnect"):
+ # happens when shutting down cockpit or rebooting machine
+ self.click("button#machine-reconnect")
+ else:
+ # happens when cockpit is still running
+ self.open_session_menu()
+ try:
+ self.click('#logout')
+ except RuntimeError as e:
+ # logging out does destroy the current frame context, it races with the CDP driver finishing the command
+ if "Execution context was destroyed" not in str(e):
+ raise
+ self.wait_visible('#login')
+
+ self.machine.allow_restart_journal_messages()
+
+ def relogin(self, path: Optional[str] = None, user: Optional[str] = None, password: Optional[str] = None,
+ superuser: Optional[bool] = None, wait_remote_session_machine: Optional[testvm.Machine] = None):
+ self.logout()
+ if wait_remote_session_machine:
+ wait_remote_session_machine.execute("while pgrep -a cockpit-ssh; do sleep 1; done")
+ self.try_login(user, password=password, superuser=superuser)
+ self._wait_present('#content')
+ self.wait_visible('#content')
+ if path:
+ if path.startswith("/@"):
+ host = path[2:].split("/")[0]
+ else:
+ host = None
+ self.enter_page(path.split("#")[0], host=host)
+
+ def open_session_menu(self):
+ self.wait_visible("#toggle-menu")
+ if (self.attr("#toggle-menu", "aria-expanded") != "true"):
+ self.click("#toggle-menu")
+
+ def layout_is_mobile(self):
+ return self.current_layout and self.current_layout["shell_size"][0] < 420
+
+ def open_superuser_dialog(self):
+ if self.layout_is_mobile():
+ self.open_session_menu()
+ self.click("#super-user-indicator-mobile button")
+ else:
+ self.click("#super-user-indicator button")
+
+ def check_superuser_indicator(self, expected: str):
+ if self.layout_is_mobile():
+ self.open_session_menu()
+ self.wait_text("#super-user-indicator-mobile", expected)
+ self.click("#toggle-menu")
+ else:
+ self.wait_text("#super-user-indicator", expected)
+
+ def become_superuser(self, user: Optional[str] = None, password: Optional[str] = None, passwordless: Optional[bool] = False):
+ cur_frame = self.cdp.cur_frame
+ self.switch_to_top()
+
+ self.open_superuser_dialog()
+
+ if passwordless:
+ self.wait_in_text("div[role=dialog]:contains('Administrative access')", "You now have administrative access.")
+ self.click("div[role=dialog] button:contains('Close')")
+ self.wait_not_present("div[role=dialog]:contains('You now have administrative access.')")
+ else:
+ self.wait_in_text("div[role=dialog]:contains('Switch to administrative access')", f"Password for {user or 'admin'}:")
+ self.set_input_text("div[role=dialog]:contains('Switch to administrative access') input", password or "foobar")
+ self.click("div[role=dialog] button:contains('Authenticate')")
+ self.wait_not_present("div[role=dialog]:contains('Switch to administrative access')")
+
+ self.check_superuser_indicator("Administrative access")
+ self.switch_to_frame(cur_frame)
+
+ def drop_superuser(self):
+ cur_frame = self.cdp.cur_frame
+ self.switch_to_top()
+
+ self.open_superuser_dialog()
+ self.click("div[role=dialog]:contains('Switch to limited access') button:contains('Limit access')")
+ self.wait_not_present("div[role=dialog]:contains('Switch to limited access')")
+ self.check_superuser_indicator("Limited access")
+
+ self.switch_to_frame(cur_frame)
+
+ def click_system_menu(self, path: str, enter: bool = True):
+ """Click on a "System" menu entry with given URL path
+
+ Enters the given target frame afterwards, unless enter=False is given
+ (useful for remote hosts).
+ """
+ self.switch_to_top()
+ self.click(f"#host-apps a[href='{path}']")
+ if enter:
+ # strip off parameters after hash
+ self.enter_page(path.split('#')[0].rstrip('/'))
+
+ def get_pf_progress_value(self, progress_bar_sel):
+ """Get numeric value of a PatternFly <ProgressBar> component"""
+ sel = progress_bar_sel + " .pf-v5-c-progress__indicator"
+ self.wait_visible(sel)
+ self.wait_attr_contains(sel, "style", "width:")
+ style = self.attr(sel, "style")
+ m = re.search(r"width: (\d+)%;", style)
+ return int(m.group(1))
+
+ def ignore_ssl_certificate_errors(self, ignore: bool):
+ action = ignore and "continue" or "cancel"
+ if opts.trace:
+ print("-> Setting SSL certificate error policy to %s" % action)
+ self.cdp.command(f"setSSLBadCertificateAction('{action}')")
+
+ def grant_permissions(self, *args: str):
+ """Grant permissions to the browser"""
+ # https://chromedevtools.github.io/devtools-protocol/tot/Browser/#method-grantPermissions
+ self.cdp.invoke("Browser.grantPermissions",
+ origin="http://%s:%s" % (self.address, self.port),
+ permissions=args)
+
+ def snapshot(self, title: str, label: Optional[str] = None):
+ """Take a snapshot of the current screen and save it as a PNG and HTML.
+
+ Arguments:
+ title: Used for the filename.
+ """
+ if self.cdp and self.cdp.valid:
+ self.cdp.command("clearExceptions()")
+
+ filename = unique_filename(f"{label or self.label}-{title}", "png")
+ if self.body_clip:
+ ret = self.cdp.invoke("Page.captureScreenshot", clip=self.body_clip, no_trace=True)
+ else:
+ ret = self.cdp.invoke("Page.captureScreenshot", no_trace=True)
+ if "data" in ret:
+ with open(filename, 'wb') as f:
+ f.write(base64.standard_b64decode(ret["data"]))
+ attach(filename, move=True)
+ print("Wrote screenshot to " + filename)
+ else:
+ print("Screenshot not available")
+
+ filename = unique_filename(f"{label or self.label}-{title}", "html")
+ html = self.cdp.invoke("Runtime.evaluate", expression="document.documentElement.outerHTML",
+ no_trace=True)["result"]["value"]
+ with open(filename, 'wb') as f:
+ f.write(html.encode('UTF-8'))
+ attach(filename, move=True)
+ print("Wrote HTML dump to " + filename)
+
+ def _set_window_size(self, width: int, height: int):
+ self.cdp.invoke("Emulation.setDeviceMetricsOverride",
+ width=width, height=height,
+ deviceScaleFactor=0, mobile=False)
+
+ def _set_emulated_media_theme(self, name: str):
+ # https://bugzilla.mozilla.org/show_bug.cgi?id=1549434
+ if self.cdp.browser.name == "chromium":
+ self.cdp.invoke("Emulation.setEmulatedMedia", features=[{'name': 'prefers-color-scheme', 'value': name}])
+
+ def _set_direction(self, direction: str):
+ cur_frame = self.cdp.cur_frame
+ if self.is_present("#shell-page"):
+ self.switch_to_top()
+ self.set_attr("#shell-page", "dir", direction)
+ self.switch_to_frame(cur_frame)
+ self.set_attr("html", "dir", direction)
+
+ def set_layout(self, name: str):
+ layout = next(lo for lo in self.layouts if lo["name"] == name)
+ if layout != self.current_layout:
+ if layout["name"] == "rtl":
+ self._set_direction("rtl")
+ elif layout["name"] != "rtl" and self.current_layout and self.current_layout["name"] == "rtl":
+ self._set_direction("ltr")
+
+ self.current_layout = layout
+ size = layout["shell_size"]
+ self._set_window_size(size[0], size[1])
+ self._adjust_window_for_fixed_content_size()
+ self._set_emulated_media_theme(layout["theme"])
+
+ def _adjust_window_for_fixed_content_size(self):
+ if self.eval_js("window.name").startswith("cockpit1:"):
+ # Adjust the window size further so that the content is
+ # exactly the expected size. This will make sure that
+ # pixel tests of the content will not be affected by
+ # changes in shell navigation elements around it. It is
+ # important that we do this only after getting the shell
+ # into about the right size so that it switches into the
+ # right layout mode.
+ shell_size = self.current_layout["shell_size"]
+ want_size = self.current_layout["content_size"]
+ have_size = self.eval_js("[ document.body.offsetWidth, document.body.offsetHeight ]")
+ delta = (want_size[0] - have_size[0], want_size[1] - have_size[1])
+ if delta[0] != 0 or delta[1] != 0:
+ self._set_window_size(shell_size[0] + delta[0], shell_size[1] + delta[1])
+
+ def assert_pixels_in_current_layout(self, selector: str, key: str,
+ ignore: Optional[List[str]] = None,
+ mock: Optional[Dict[str, str]] = None,
+ sit_after_mock: bool = False,
+ scroll_into_view: Optional[str] = None,
+ wait_animations: bool = True,
+ wait_delay: float = 0.5):
+ """Compare the given element with its reference in the current layout"""
+
+ if ignore is None:
+ ignore = []
+
+ if not (Image and self.pixels_label):
+ return
+
+ self._adjust_window_for_fixed_content_size()
+ self.call_js_func('ph_scrollIntoViewIfNeeded', scroll_into_view or selector)
+ self.call_js_func('ph_blur_active')
+
+ # Wait for all animations to be over. This is done by
+ # counting them all over and over again until there are zero.
+ # Calling `.finish()` on all animations would miss those that
+ # are created while we wait, and would also fail with an
+ # exception if any unlimited animations are present, like
+ # spinners.
+ #
+ # There is another complication with tooltips. They are shown
+ # on top of certain elements, but are not DOM children of
+ # these elements. Also, Patternfly sometimes creates tooltips
+ # on dialog titles that are too long for the dialog, but only
+ # a little bit after the dialog has appeared.
+ #
+ # We don't want to predict whether tooltips will appear, and
+ # thus we can't wait for them to be present before waiting for
+ # their fade-in animation to be over.
+ #
+ # But we know that tooltips fade in within 300ms, so we just
+ # wait half a second to and side-step all that complexity.
+
+ if wait_animations:
+ time.sleep(wait_delay)
+ self.wait_js_cond('ph_count_animations(%s) == 0' % jsquote(selector))
+
+ if mock is not None:
+ self.set_mock(mock, base=selector)
+ if sit_after_mock:
+ sit()
+
+ rect = self.call_js_func('ph_element_clip', selector)
+
+ def relative_clips(sels):
+ return [(
+ r['x'] - rect['x'],
+ r['y'] - rect['y'],
+ r['x'] - rect['x'] + r['width'],
+ r['y'] - rect['y'] + r['height'])
+ for r in self.call_js_func('ph_selector_clips', sels)]
+
+ reference_dir = os.path.join(TEST_DIR, 'reference')
+ if not os.path.exists(os.path.join(reference_dir, '.git')):
+ raise SystemError("Pixel test references are missing, please run: test/common/pixel-tests pull")
+
+ ignore_rects = relative_clips([f"{selector} {item}" for item in ignore])
+ base = self.pixels_label + "-" + key
+ if self.current_layout != self.layouts[0]:
+ base += "-" + self.current_layout["name"]
+ filename = base + "-pixels.png"
+ ref_filename = os.path.join(reference_dir, filename)
+ self.used_pixel_references.add(ref_filename)
+ ret = self.cdp.invoke("Page.captureScreenshot", clip=rect, no_trace=True)
+ png_now = base64.standard_b64decode(ret["data"])
+ png_ref = os.path.exists(ref_filename) and open(ref_filename, "rb").read()
+ if not png_ref:
+ with open(filename, 'wb') as f:
+ f.write(png_now)
+ attach(filename, move=True)
+ print("New pixel test reference " + filename)
+ self.failed_pixel_tests += 1
+ else:
+ img_now = Image.open(io.BytesIO(png_now)).convert("RGBA")
+ img_ref = Image.open(io.BytesIO(png_ref)).convert("RGBA")
+ img_delta = Image.new("RGBA",
+ (max(img_now.size[0], img_ref.size[0]), max(img_now.size[1], img_ref.size[1])),
+ (255, 0, 0, 255))
+
+ # The current snapshot and the reference don't need to
+ # be perfectly identical. They might differ in the
+ # following ways:
+ #
+ # - A pixel in the reference image might be
+ # transparent. These pixels are ignored.
+ #
+ # - The call to assert_pixels specifies a list of
+ # rectangles (via CSS selectors). Pixels within those
+ # rectangles (and slightly outside) are ignored. Pixels
+ # just outside the rectangles are also ignored to avoid
+ # issues with rounding coordinates.
+ #
+ # - The RGB values of pixels can differ by up to 2.
+ #
+ # - There can be up to 20 different pixels
+ #
+ # Pixels that are different but have been ignored are
+ # marked in the delta image in green.
+
+ def masked(ref):
+ return ref[3] != 255
+
+ def ignorable_coord(x, y):
+ for (x0, y0, x1, y1) in ignore_rects:
+ if x >= x0 - 2 and x < x1 + 2 and y >= y0 - 2 and y < y1 + 2:
+ return True
+ return False
+
+ def ignorable_change(a, b):
+ return abs(a[0] - b[0]) <= 2 and abs(a[1] - b[1]) <= 2 and abs(a[1] - b[1]) <= 2
+
+ def img_eq(ref, now, delta):
+ # This is slow but exactly what we want.
+ # ImageMath might be able to speed this up.
+ data_ref = ref.load()
+ data_now = now.load()
+ data_delta = delta.load()
+ result = True
+ count = 0
+ width, height = delta.size
+ for y in range(height):
+ for x in range(width):
+ if x >= ref.size[0] or x >= now.size[0] or y >= ref.size[1] or y >= now.size[1]:
+ result = False
+ elif data_ref[x, y] != data_now[x, y]:
+ if masked(data_ref[x, y]) or ignorable_coord(x, y) or ignorable_change(data_ref[x, y], data_now[x, y]):
+ data_delta[x, y] = (0, 255, 0, 255)
+ else:
+ data_delta[x, y] = (255, 0, 0, 255)
+ count += 1
+ if count > 20:
+ result = False
+ else:
+ data_delta[x, y] = data_ref[x, y]
+ return result
+
+ if not img_eq(img_ref, img_now, img_delta):
+ if img_now.size == img_ref.size:
+ # Preserve alpha channel so that the 'now'
+ # image can be used as the new reference image
+ # without further changes
+ img_now.putalpha(img_ref.getchannel("A"))
+ img_now.save(filename)
+ attach(filename, move=True)
+ ref_filename_for_attach = base + "-reference.png"
+ img_ref.save(ref_filename_for_attach)
+ attach(ref_filename_for_attach, move=True)
+ delta_filename = base + "-delta.png"
+ img_delta.save(delta_filename)
+ attach(delta_filename, move=True)
+ print("Differences in pixel test " + base)
+ self.failed_pixel_tests += 1
+
+ def assert_pixels(self, selector: str, key: str,
+ ignore: Optional[List[str]] = None,
+ mock: Optional[Dict[str, str]] = None,
+ sit_after_mock: bool = False,
+ skip_layouts: Optional[List[str]] = None,
+ scroll_into_view: Optional[str] = None,
+ wait_animations: bool = True,
+ wait_after_layout_change: bool = False,
+ wait_delay: float = 0.5):
+ """Compare the given element with its reference in all layouts"""
+
+ if ignore is None:
+ ignore = []
+
+ if skip_layouts is None:
+ skip_layouts = []
+
+ if not (Image and self.pixels_label):
+ return
+
+ # If the page overflows make sure to not show a scrollbar
+ # Don't apply this hack for login and terminal and shell as they don't use PF Page
+ if not self.is_present("#shell-page") and not self.is_present("#login-details") and not self.is_present("#system-terminal-page"):
+ self.switch_to_frame(self.cdp.cur_frame)
+ classes = self.attr("main", "class")
+ if "pf-v5-c-page__main" in classes:
+ self.set_attr("main.pf-v5-c-page__main", "class", f"{classes} pixel-test")
+
+ if self.current_layout:
+ previous_layout = self.current_layout["name"]
+ for layout in self.layouts:
+ if layout["name"] not in skip_layouts:
+ self.set_layout(layout["name"])
+ if wait_after_layout_change:
+ time.sleep(wait_delay)
+ self.assert_pixels_in_current_layout(selector, key, ignore=ignore,
+ mock=mock, sit_after_mock=sit_after_mock,
+ scroll_into_view=scroll_into_view,
+ wait_animations=wait_animations,
+ wait_delay=wait_delay)
+
+ self.set_layout(previous_layout)
+
+ def assert_no_unused_pixel_test_references(self):
+ """Check whether all reference images in test/reference have been used."""
+
+ if not (Image and self.pixels_label):
+ return
+
+ pixel_references = set(glob.glob(os.path.join(TEST_DIR, "reference", self.pixels_label + "*-pixels.png")))
+ unused = pixel_references - self.used_pixel_references
+ for u in unused:
+ print("Unused reference image " + os.path.basename(u))
+ self.failed_pixel_tests += 1
+
+ def get_js_log(self):
+ """Return the current javascript log"""
+
+ if self.cdp:
+ return self.cdp.get_js_log()
+ return []
+
+ def copy_js_log(self, title: str, label: Optional[str] = None):
+ """Copy the current javascript log"""
+
+ logs = list(self.get_js_log())
+ if logs:
+ filename = unique_filename(f"{label or self.label}-{title}", "js.log")
+ with open(filename, 'wb') as f:
+ f.write('\n'.join(logs).encode('UTF-8'))
+ attach(filename, move=True)
+ print("Wrote JS log to " + filename)
+
+ def kill(self):
+ self.cdp.kill()
+
+ def write_coverage_data(self):
+ if self.coverage_label and self.cdp and self.cdp.valid:
+ coverage = self.cdp.invoke("Profiler.takePreciseCoverage")
+ write_lcov(coverage['result'], self.coverage_label)
+
+ def assert_no_oops(self):
+ if self.allow_oops:
+ return
+
+ if self.cdp and self.cdp.valid:
+ self.switch_to_top()
+ if self.is_present("#navbar-oops"):
+ assert not self.is_visible("#navbar-oops"), "Cockpit shows an Oops"
+
+
+class MachineCase(unittest.TestCase):
+ image = testvm.DEFAULT_IMAGE
+ libexecdir = None
+ runner = None
+ machine: testvm.Machine
+ machines = Dict[str, testvm.Machine]
+ machine_class = None
+ browser: Browser
+ network = None
+ journal_start = None
+
+ # provision is a dictionary of dictionaries, one for each additional machine to be created, e.g.:
+ # provision = { 'openshift' : { 'image': 'openshift', 'memory_mb': 1024 } }
+ # These will be instantiated during setUp, and replaced with machine objects
+ provision: Optional[Dict[str, Dict[str, Union[str, int]]]] = None
+
+ global_machine = None
+
+ @classmethod
+ def get_global_machine(cls):
+ if cls.global_machine:
+ return cls.global_machine
+ cls.global_machine = cls.new_machine(cls, restrict=True, cleanup=False)
+ if opts.trace:
+ print(f"Starting global machine {cls.global_machine.label}")
+ cls.global_machine.start()
+ return cls.global_machine
+
+ @classmethod
+ def kill_global_machine(cls):
+ if cls.global_machine:
+ cls.global_machine.kill()
+ cls.global_machine = None
+
+ def label(self):
+ return self.__class__.__name__ + '-' + self._testMethodName
+
+ def new_machine(self, image=None, forward=None, restrict=True, cleanup=True, inherit_machine_class=True, **kwargs):
+ machine_class = inherit_machine_class and self.machine_class or testvm.VirtMachine
+
+ if opts.address:
+ if forward:
+ raise unittest.SkipTest("Cannot run this test when specific machine address is specified")
+ machine = testvm.Machine(address=opts.address, image=image or self.image, verbose=opts.trace, browser=opts.browser)
+ if cleanup:
+ self.addCleanup(machine.disconnect)
+ else:
+ if image is None:
+ image = os.path.join(TEST_DIR, "images", self.image)
+ if not os.path.exists(image):
+ raise FileNotFoundError("Can't run tests without a prepared image; use test/image-prepare")
+ if not self.network:
+ network = testvm.VirtNetwork(image=image)
+ if cleanup:
+ self.addCleanup(network.kill)
+ self.network = network
+ networking = self.network.host(restrict=restrict, forward=forward or {})
+ machine = machine_class(verbose=opts.trace, networking=networking, image=image, **kwargs)
+ if opts.fetch and not os.path.exists(machine.image_file):
+ machine.pull(machine.image_file)
+ if cleanup:
+ self.addCleanup(machine.kill)
+ return machine
+
+ def new_browser(self, machine=None, coverage=False):
+ if machine is None:
+ machine = self.machine
+ label = self.label() + "-" + machine.label
+ pixels_label = None
+ if os.environ.get("TEST_BROWSER", "chromium") == "chromium" and not self.is_devel_build():
+ try:
+ with open(f'{TEST_DIR}/reference-image') as fp:
+ reference_image = fp.read().strip()
+ except FileNotFoundError:
+ # no "reference-image" file available; this most likely means that
+ # there are no pixel tests to execute
+ pass
+ else:
+ if machine.image == reference_image:
+ pixels_label = self.label()
+ browser = Browser(machine.web_address,
+ label=label, pixels_label=pixels_label, coverage_label=self.label() if coverage else None,
+ port=machine.web_port, machine=self)
+ self.addCleanup(browser.kill)
+ return browser
+
+ def getError(self):
+ # errors is a list of (method, exception) calls (usually multiple
+ # per method); None exception means success
+ errors = []
+ if hasattr(self._outcome, 'errors'):
+ # Python 3.4 - 3.10 (These two methods have no side effects)
+ result = self.defaultTestResult()
+ errors = result.errors
+ self._feedErrorsToResult(result, self._outcome.errors)
+ elif hasattr(self._outcome, 'result') and hasattr(self._outcome.result, '_excinfo'):
+ # pytest emulating unittest
+ return self._outcome.result._excinfo
+ else:
+ # Python 3.11+ now records errors and failures seperate
+ errors = self._outcome.result.errors + self._outcome.result.failures
+
+ try:
+ return errors[0][1]
+ except IndexError:
+ return None
+
+ def is_nondestructive(self):
+ test_method = getattr(self.__class__, self._testMethodName)
+ return get_decorator(test_method, self.__class__, "nondestructive")
+
+ def is_devel_build(self) -> bool:
+ return os.environ.get('NODE_ENV') == 'development'
+
+ def is_pybridge(self) -> bool:
+ # some tests start e.g. centos-7 as first machine, bridge may not exist there
+ return any('python' in m.execute('head -c 30 /usr/bin/cockpit-bridge || true') for m in self.machines.values())
+
+ def disable_preload(self, *packages, machine=None):
+ if machine is None:
+ machine = self.machine
+ for pkg in packages:
+ machine.write(f"/etc/cockpit/{pkg}.override.json", '{ "preload": [ ] }')
+
+ def enable_preload(self, package: str, *pages: str):
+ pages_str = ', '.join(f'"{page}"' for page in pages)
+ self.machine.write(f"/etc/cockpit/{package}.override.json", f'{{ "preload": [ {pages_str} ] }}')
+
+ def system_before(self, version):
+ try:
+ v = self.machine.execute("""rpm -q --qf '%{V}' cockpit-system ||
+ dpkg-query -W -f '${source:Upstream-Version}' cockpit-system ||
+ (pacman -Q cockpit | cut -f2 -d' ' | cut -f1 -d-)
+ """).split(".")
+ except subprocess.CalledProcessError:
+ return False
+
+ return int(v[0]) < version
+
+ def setUp(self, restrict=True):
+ self.allowed_messages = self.default_allowed_messages
+ self.allowed_console_errors = self.default_allowed_console_errors
+ self.allow_core_dumps = False
+
+ if os.getenv("MACHINE"):
+ # apply env variable together if MACHINE envvar is set
+ opts.address = os.getenv("MACHINE")
+ if self.is_nondestructive():
+ pass
+ elif os.getenv("DESTRUCTIVE") and not self.is_nondestructive():
+ print("Run destructive test, be careful, may lead to upredictable state of machine")
+ else:
+ raise unittest.SkipTest("Skip destructive test by default")
+ if os.getenv("BROWSER"):
+ opts.browser = os.getenv("BROWSER")
+ if os.getenv("TRACE"):
+ opts.trace = True
+ if os.getenv("SIT"):
+ opts.sit = True
+
+ if opts.address and self.provision is not None:
+ raise unittest.SkipTest("Cannot provision multiple machines if a specific machine address is specified")
+
+ self.machines = {}
+ provision = self.provision or {'machine1': {}}
+ self.tmpdir = tempfile.mkdtemp()
+ # automatically cleaned up for @nondestructive tests, but you have to create it yourself
+ self.vm_tmpdir = "/var/lib/cockpittest"
+
+ if self.is_nondestructive() and not opts.address:
+ if self.provision:
+ raise unittest.SkipTest("Cannot provision machines if test is marked as nondestructive")
+ self.machine = self.machines['machine1'] = MachineCase.get_global_machine()
+ else:
+ MachineCase.kill_global_machine()
+ first_machine = True
+ # First create all machines, wait for them later
+ for key in sorted(provision.keys()):
+ options = provision[key].copy()
+ if 'address' in options:
+ del options['address']
+ if 'dns' in options:
+ del options['dns']
+ if 'dhcp' in options:
+ del options['dhcp']
+ if 'restrict' not in options:
+ options['restrict'] = restrict
+ machine = self.new_machine(**options)
+ self.machines[key] = machine
+ if first_machine:
+ first_machine = False
+ self.machine = machine
+ if opts.trace:
+ print(f"Starting {key} {machine.label}")
+ machine.start()
+
+ self.danger_btn_class = '.pf-m-danger'
+ self.primary_btn_class = '.pf-m-primary'
+ self.default_btn_class = '.pf-m-secondary'
+
+ # Now wait for the other machines to be up
+ for key in self.machines.keys():
+ machine = self.machines[key]
+ machine.wait_boot()
+ address = provision[key].get("address")
+ if address is not None:
+ machine.set_address(address)
+ dns = provision[key].get("dns")
+ if address or dns:
+ machine.set_dns(dns)
+ dhcp = provision[key].get("dhcp", False)
+ if dhcp:
+ machine.dhcp_server()
+
+ self.journal_start = self.machine.journal_cursor()
+ self.browser: Browser = self.new_browser(coverage=opts.coverage)
+ # fail tests on criticals
+ self.machine.write("/etc/cockpit/cockpit.conf", "[Log]\nFatal = criticals\n")
+ if self.is_nondestructive():
+ self.nonDestructiveSetup()
+
+ # Pages with debug enabled are huge and loading/executing them is heavy for browsers
+ # To make it easier for browsers and thus make tests quicker, disable packagekit and systemd preloads
+ if self.is_devel_build():
+ self.disable_preload("packagekit", "systemd")
+
+ if self.machine.image.startswith('debian') or self.machine.image.startswith('ubuntu') or self.machine.image == 'arch':
+ self.libexecdir = '/usr/lib/cockpit'
+ else:
+ self.libexecdir = '/usr/libexec'
+
+ def nonDestructiveSetup(self):
+ """generic setUp/tearDown for @nondestructive tests"""
+
+ m = self.machine
+
+ # helps with mapping journal output to particular tests
+ name = "%s.%s" % (self.__class__.__name__, self._testMethodName)
+ m.execute("logger -p user.info 'COCKPITTEST: start %s'" % name)
+ self.addCleanup(m.execute, "logger -p user.info 'COCKPITTEST: end %s'" % name)
+
+ # core dumps get copied per-test, don't clobber subsequent tests with them
+ self.addCleanup(m.execute, "find /var/lib/systemd/coredump -type f -delete")
+
+ # temporary directory in the VM
+ self.addCleanup(m.execute, "if [ -d {0} ]; then findmnt --list --noheadings --output TARGET | grep ^{0} | xargs -r umount; rm -r {0}; fi".format(self.vm_tmpdir))
+
+ # users/groups/home dirs
+ self.restore_file("/etc/passwd")
+ self.restore_file("/etc/group")
+ self.restore_file("/etc/shadow")
+ self.restore_file("/etc/gshadow")
+ self.restore_file("/etc/subuid")
+ self.restore_file("/etc/subgid")
+ self.restore_file("/var/log/wtmp")
+ home_dirs = m.execute("ls /home").strip().split()
+
+ def cleanup_home_dirs():
+ for d in m.execute("ls /home").strip().split():
+ if d not in home_dirs:
+ m.execute("rm -r /home/" + d)
+ self.addCleanup(cleanup_home_dirs)
+
+ if m.image == "arch":
+ # arch configures pam_faillock by default
+ self.addCleanup(m.execute, "rm -rf /run/faillock")
+
+ # cockpit configuration
+ self.restore_dir("/etc/cockpit")
+
+ if not m.ostree_image:
+ # for storage tests
+ self.restore_file("/etc/fstab")
+ self.restore_file("/etc/crypttab")
+
+ # tests expect cockpit.service to not run at start; also, avoid log leakage into the next test
+ self.addCleanup(m.execute, "systemctl stop --quiet cockpit")
+
+ # The sssd daemon seems to get confused when we restore
+ # backups of /etc/group etc and stops following updates to it.
+ # Let's restart the daemon to reset that condition.
+ m.execute("systemctl try-restart sssd || true")
+
+ # reset scsi_debug (see e. g. StorageHelpers.add_ram_disk()
+ # this needs to happen very late in the cleanup, so that test cases can clean up the users of that disk first
+ # right after unmounting the device is often still busy, so retry a few times
+ self.addCleanup(self.machine.execute,
+ "set -e; [ -e /sys/module/scsi_debug ] || exit 0; "
+ "for dev in $(ls /sys/bus/pseudo/drivers/scsi_debug/adapter*/host*/target*/*:*/block); do "
+ " for s in /sys/block/*/slaves/${dev}*; do [ -e $s ] || break; "
+ " d=/dev/$(dirname $(dirname ${s#/sys/block/})); "
+ " while fuser --mount $d --kill; do sleep 0.1; done; "
+ " umount $d || true; dmsetup remove --force $d || true; "
+ " done; "
+ " while fuser --mount /dev/$dev --kill; do sleep 0.1; done; "
+ " umount /dev/$dev || true; "
+ " swapon --show=NAME --noheadings | grep $dev | xargs -r swapoff; "
+ "done; until rmmod scsi_debug; do sleep 0.2; done", stdout=None)
+
+ def terminate_sessions():
+ # on OSTree we don't get "web console" sessions with the cockpit/ws container; just SSH; but also, some tests start
+ # admin sessions without Cockpit
+ self.machine.execute("""for u in $(loginctl --no-legend list-users | awk '{ if ($2 != "root") print $1 }'); do
+ loginctl terminate-user $u 2>/dev/null || true
+ loginctl kill-user $u 2>/dev/null || true
+ pkill -9 -u $u || true
+ while pgrep -u $u; do sleep 0.2; done
+ while mountpoint -q /run/user/$u && ! umount /run/user/$u; do sleep 0.2; done
+ rm -rf /run/user/$u
+ done""")
+
+ # Terminate all other Cockpit sessions
+ sessions = self.machine.execute("loginctl --no-legend list-sessions | awk '/web console/ { print $1 }'").strip().split()
+ for s in sessions:
+ # Don't insist that terminating works, the session might be gone by now.
+ self.machine.execute(f"loginctl kill-session {s} || true; loginctl terminate-session {s} || true")
+
+ # Restart logind to mop up empty "closing" sessions
+ self.machine.execute("systemctl stop systemd-logind")
+
+ # Wait for sessions to be gone
+ sessions = self.machine.execute("loginctl --no-legend list-sessions | awk '/web console/ { print $1 }'").strip().split()
+ for s in sessions:
+ try:
+ m.execute(f"while loginctl show-session {s}; do sleep 0.2; done", timeout=30)
+ except RuntimeError:
+ # show the status in debug logs, to see what's wrong
+ m.execute(f"loginctl session-status {s}; systemd-cgls", stdout=None)
+ raise
+
+ # terminate all systemd user services for users who are not logged in
+ self.machine.execute("systemctl stop user@*.service")
+
+ # Clean up "closing" sessions again, and clean user id cache for non-system users
+ self.machine.execute("systemctl stop systemd-logind; cd /run/systemd/users/; "
+ "for f in $(ls); do [ $f -le 500 ] || rm $f; done")
+
+ self.addCleanup(terminate_sessions)
+
+ def tearDown(self):
+ error = self.getError()
+
+ if error:
+ print(error, file=sys.stderr)
+ try:
+ self.snapshot("FAIL")
+ self.copy_js_log("FAIL")
+ self.copy_journal("FAIL")
+ self.copy_cores("FAIL")
+ except (OSError, RuntimeError):
+ # failures in these debug artifacts should not skip cleanup actions
+ sys.stderr.write("Failed to generate debug artifact:\n")
+ traceback.print_exc(file=sys.stderr)
+
+ if opts.sit:
+ sit(self.machines)
+
+ if self.browser:
+ self.browser.write_coverage_data()
+
+ if self.machine.ssh_reachable:
+ self.check_journal_messages()
+ if not error:
+ self.check_browser_errors()
+ self.check_pixel_tests()
+
+ shutil.rmtree(self.tmpdir, ignore_errors=True)
+
+ def login_and_go(self, path: Optional[str] = None, user: Optional[str] = None, host: Optional[str] = None,
+ superuser: bool = True, urlroot: Optional[str] = None, tls: bool = False,
+ enable_root_login: bool = False):
+ if enable_root_login:
+ self.enable_root_login()
+ self.machine.start_cockpit(tls=tls)
+ # first load after starting cockpit tends to take longer, due to on-demand service start
+ with self.browser.wait_timeout(30):
+ self.browser.login_and_go(path, user=user, host=host, superuser=superuser, urlroot=urlroot, tls=tls)
+
+ def start_machine_troubleshoot(self, new=False, known_host=False, password=None, expect_closed_dialog=True, browser=None):
+ b = browser or self.browser
+
+ b.wait_visible("#machine-troubleshoot")
+ b.click('#machine-troubleshoot')
+
+ b.wait_visible('#hosts_setup_server_dialog')
+ if new:
+ b.click('#hosts_setup_server_dialog button:contains(Add)')
+ if not known_host:
+ b.wait_in_text('#hosts_setup_server_dialog', "You are connecting to")
+ b.wait_in_text('#hosts_setup_server_dialog', "for the first time.")
+ b.click("#hosts_setup_server_dialog button:contains('Trust and add host')")
+ if password:
+ b.wait_in_text('#hosts_setup_server_dialog', "Unable to log in")
+ b.set_input_text('#login-custom-password', password)
+ b.click('#hosts_setup_server_dialog button:contains(Log in)')
+ if expect_closed_dialog:
+ b.wait_not_present('#hosts_setup_server_dialog')
+
+ def add_machine(self, address, known_host=False, password="foobar", browser=None):
+ b = browser or self.browser
+ b.switch_to_top()
+ b.go(f"/@{address}")
+ self.start_machine_troubleshoot(new=True, known_host=known_host, password=password, browser=browser)
+ b.enter_page("/system", host=address)
+
+ # List of allowed journal messages during tests; these need to match the *entire* message
+ default_allowed_messages = [
+ # Reauth stuff
+ '.*Reauthorizing unix-user:.*',
+ '.*user .* was reauthorized.*',
+
+ # Happens when the user logs out during reauthorization
+ "Error executing command as another user: Not authorized",
+ "This incident has been reported.",
+
+ # Reboots are ok
+ "-- Reboot --",
+
+ # Sometimes D-Bus goes away before us during shutdown
+ "Lost the name com.redhat.Cockpit on the session message bus",
+ "GLib-GIO:ERROR:gdbusobjectmanagerserver\\.c:.*:g_dbus_object_manager_server_emit_interfaces_.*: assertion failed \\(error == NULL\\): The connection is closed \\(g-io-error-quark, 18\\)",
+ "Error sending message: The connection is closed",
+
+ # PAM noise
+ "cockpit-session: pam: Creating directory .*",
+ "cockpit-session: pam: Changing password for .*",
+
+ # btmp tracking
+ "cockpit-session: pam: Last failed login:.*",
+ "cockpit-session: pam: There .* failed login attempts? since the last successful login.",
+
+ # pam_lastlog complaints
+ ".*/var/log/lastlog: No such file or directory",
+
+ # ssh messages may be dropped when closing
+ '10.*: dropping message while waiting for child to exit',
+
+ # pkg/packagekit/autoupdates.jsx backend check often gets interrupted by logout
+ "xargs: basename: terminated by signal 13",
+
+ # SELinux messages to ignore
+ "(audit: )?type=1403 audit.*",
+ "(audit: )?type=1404 audit.*",
+ "(audit: )?type=1405 audit.*",
+
+ # apparmor loading
+ "(audit: )?type=1400.*apparmor=\"STATUS\".*",
+
+ # apparmor noise
+ "(audit: )?type=1400.*apparmor=\"ALLOWED\".*",
+
+ # Messages from systemd libraries when they are in debug mode
+ 'Successfully loaded SELinux database in.*',
+ 'calling: info',
+ 'Sent message type=method_call sender=.*',
+ 'Got message type=method_return sender=.*',
+
+ # Various operating systems see this from time to time
+ "Journal file.*truncated, ignoring file.",
+
+ # our core dump retrieval is not entirely reliable
+ "Failed to send coredump datagram:.*",
+
+ # Something crashed, but we don't have more info. Don't fail on that
+ "Failed to get (COMM|EXE).*: No such process",
+
+ # several tests change the host name
+ "sudo: unable to resolve host.*",
+
+ # The usual sudo finger wagging
+ "We trust you have received the usual lecture from the local System",
+ "Administrator. It usually boils down to these three things:",
+ r"#1\) Respect the privacy of others.",
+ r"#2\) Think before you type.",
+ r"#3\) With great power comes great responsibility.",
+ "For security reasons, the password you type will not be visible",
+
+ # starting out with empty PCP logs and pmlogger not running causes these metrics channel messages
+ "(direct|pcp-archive): no such metric: .*: Unknown metric name",
+ "(direct|pcp-archive): instance name lookup failed:.*",
+ "(direct|pcp-archive): couldn't create pcp archive context for.*",
+
+ # timedatex.service shuts down after timeout, runs into race condition with property watching
+ ".*org.freedesktop.timedate1: couldn't get all properties.*Error:org.freedesktop.DBus.Error.NoReply.*",
+
+ # https://github.com/cockpit-project/cockpit/issues/19235
+ "invalid non-UTF8 @data passed as text to web_socket_connection_send.*",
+ ]
+
+ default_allowed_messages += os.environ.get("TEST_ALLOW_JOURNAL_MESSAGES", "").split(",")
+
+ # List of allowed console.error() messages during tests; these match substrings
+ default_allowed_console_errors = [
+ # HACK: These should be fixed, but debugging these is not trivial, and the impact is very low
+ "Warning: .* setState.*on an unmounted component",
+ "Warning: Can't perform a React state update on an unmounted component",
+ "Warning: Cannot update a component.*while rendering a different component",
+ "Warning: A component is changing an uncontrolled input to be controlled",
+ "Warning: A component is changing a controlled input to be uncontrolled",
+ "Warning: Can't call.*on a component that is not yet mounted. This is a no-op",
+ "Warning: Cannot update during an existing state transition",
+ r"Warning: You are calling ReactDOMClient.createRoot\(\) on a container that has already been passed to createRoot",
+
+ # FIXME: PatternFly complains about these, but https://www.a11y-collective.com/blog/the-first-rule-for-using-aria/
+ # and https://www.accessibility-developer-guide.com/knowledge/aria/bad-practices/
+ "aria-label",
+
+ # PackageKit crashes a lot; let that not be the sole reason for failing a test
+ "error: Could not determine kpatch packages:.*PackageKit crashed",
+ ]
+
+ if testvm.DEFAULT_IMAGE.startswith('rhel-8') or testvm.DEFAULT_IMAGE.startswith('centos-8'):
+ # old occasional bugs in tracer, don't happen in newer versions any more
+ default_allowed_console_errors.append('Tracer failed:.*Traceback')
+
+ env_allow = os.environ.get("TEST_ALLOW_BROWSER_ERRORS")
+ if env_allow:
+ default_allowed_console_errors += env_allow.split(",")
+
+ def allow_journal_messages(self, *patterns: str):
+ """Don't fail if the journal contains a entry completely matching the given regexp"""
+ for p in patterns:
+ self.allowed_messages.append(p)
+
+ def allow_hostkey_messages(self):
+ self.allow_journal_messages('.*: .* host key for server is not known: .*',
+ '.*: refusing to connect to unknown host: .*',
+ '.*: .* host key for server has changed to: .*',
+ '.*: host key for this server changed key type: .*',
+ '.*: failed to retrieve resource: hostkey-unknown')
+
+ def allow_restart_journal_messages(self):
+ self.allow_journal_messages(".*Connection reset by peer.*",
+ "connection unexpectedly closed by peer",
+ ".*Broken pipe.*",
+ "g_dbus_connection_real_closed: Remote peer vanished with error: Underlying GIOStream returned 0 bytes on an async read \\(g-io-error-quark, 0\\). Exiting.",
+ "cockpit-session: .*timed out.*",
+ "ignoring failure from session process:.*",
+ "peer did not close io when expected",
+ "request timed out, closing",
+ "PolicyKit daemon disconnected from the bus.",
+ ".*couldn't create polkit session subject: No session for pid.*",
+ "We are no longer a registered authentication agent.",
+ ".*: failed to retrieve resource: terminated",
+ ".*: external channel failed: (terminated|protocol-error)",
+ 'audit:.*denied.*comm="systemd-user-se".*nologin.*',
+ ".*No session for cookie",
+
+ 'localhost: dropping message while waiting for child to exit',
+ '.*: GDBus.Error:org.freedesktop.PolicyKit1.Error.Failed: .*',
+ '.*g_dbus_connection_call_finish_internal.*G_IS_DBUS_CONNECTION.*',
+ '.*Message recipient disconnected from message bus without replying.*',
+ '.*Unable to shutdown socket: Transport endpoint is not connected.*',
+
+ # If restarts or reloads happen really fast, the code in python.js
+ # that figures out which python to use crashes with SIGPIPE,
+ # and this is the resulting message
+ 'which: no python in .*'
+ )
+
+ def check_journal_messages(self, machine=None):
+ """Check for unexpected journal entries."""
+ machine = machine or self.machine
+ # on main machine, only consider journal entries since test case start
+ cursor = (machine == self.machine) and self.journal_start or None
+
+ # Journald does not always set trusted fields like
+ # _SYSTEMD_UNIT or _EXE correctly for the last few messages of
+ # a dying process, so we filter by the untrusted but reliable
+ # SYSLOG_IDENTIFIER instead.
+
+ matches = [
+ "SYSLOG_IDENTIFIER=cockpit-ws",
+ "SYSLOG_IDENTIFIER=cockpit-bridge",
+ "SYSLOG_IDENTIFIER=cockpit/ssh",
+ # also catch GLIB_DOMAIN=<library> which apply to cockpit-ws (but not to -bridge, too much random noise)
+ "_COMM=cockpit-ws",
+ "GLIB_DOMAIN=cockpit-ws",
+ "GLIB_DOMAIN=cockpit-bridge",
+ "GLIB_DOMAIN=cockpit-ssh",
+ "GLIB_DOMAIN=cockpit-pcp"
+ ]
+
+ if not self.allow_core_dumps:
+ matches += ["SYSLOG_IDENTIFIER=systemd-coredump"]
+ self.allowed_messages.append("Resource limits disable core dumping for process.*")
+ # can happen on shutdown when /run/systemd/coredump is gone already
+ self.allowed_messages.append("Failed to connect to coredump service: No such file or directory")
+ self.allowed_messages.append("Failed to connect to coredump service: Connection refused")
+
+ messages = machine.journal_messages(matches, 6, cursor=cursor)
+
+ if "TEST_AUDIT_NO_SELINUX" not in os.environ:
+ messages += machine.audit_messages("14", cursor=cursor) # 14xx is selinux
+
+ self.allowed_messages += self.machine.allowed_messages()
+
+ all_found = True
+ first = None
+ for m in messages:
+ # remove leading/trailing whitespace
+ m = m.strip()
+ # Ignore empty lines
+ if not m:
+ continue
+ found = False
+
+ # When coredump could not be generated, we cannot do much with info about there being a coredump
+ # Ignore this message and all subsequent core dumps
+ # If there is more than just one line about coredump, it will fail and show this messages
+ if m.startswith("Failed to generate stack trace"):
+ self.allowed_messages.append("Process .* of user .* dumped core.*")
+ continue
+
+ for p in self.allowed_messages:
+ match = re.match(p, m)
+ if match and match.group(0) == m:
+ found = True
+ break
+ if not found:
+ all_found = False
+ if not first:
+ first = m
+ print(m)
+ if not all_found:
+ self.copy_js_log("FAIL")
+ self.copy_journal("FAIL")
+ self.copy_cores("FAIL")
+ if not self.getError():
+ # fail test on the unexpected messages
+ raise Error(UNEXPECTED_MESSAGE + "journal messages:\n" + first)
+
+ def allow_browser_errors(self, *patterns):
+ """Don't fail if the test caused a console error contains the given regexp"""
+ for p in patterns:
+ self.allowed_console_errors.append(p)
+
+ def check_browser_errors(self):
+ if not self.browser:
+ return
+ for log in self.browser.get_js_log():
+ if not log.startswith("error: "):
+ continue
+ # errors are fatal in general; they need to be explicitly whitelisted
+ for p in self.allowed_console_errors:
+ if re.search(p, log):
+ break
+ else:
+ raise Error(UNEXPECTED_MESSAGE + "browser errors:\n" + log)
+
+ self.browser.assert_no_oops()
+
+ def check_pixel_tests(self):
+ if self.browser:
+ self.browser.assert_no_unused_pixel_test_references()
+ if self.browser.failed_pixel_tests > 0:
+ raise Error(PIXEL_TEST_MESSAGE)
+
+ def snapshot(self, title: str, label: Optional[str] = None):
+ """Take a snapshot of the current screen and save it as a PNG.
+
+ Arguments:
+ title: Used for the filename.
+ """
+ if self.browser is not None:
+ try:
+ self.browser.snapshot(title, label)
+ except RuntimeError:
+ # this usually runs in exception handlers; raising an exception here skips cleanup handlers, so don't
+ sys.stderr.write("Unexpected exception in snapshot():\n")
+ sys.stderr.write(traceback.format_exc())
+
+ def copy_js_log(self, title, label=None):
+ if self.browser is not None:
+ try:
+ self.browser.copy_js_log(title, label)
+ except RuntimeError:
+ # this usually runs in exception handlers; raising an exception here skips cleanup handlers, so don't
+ sys.stderr.write("Unexpected exception in copy_js_log():\n")
+ sys.stderr.write(traceback.format_exc())
+
+ def copy_journal(self, title: str, label: Optional[str] = None):
+ for _, m in self.machines.items():
+ if m.ssh_reachable:
+ log = unique_filename("%s-%s-%s" % (label or self.label(), m.label, title), "log.gz")
+ with open(log, "w") as fp:
+ m.execute("journalctl|gzip", stdout=fp)
+ print("Journal extracted to %s" % (log))
+ attach(log, move=True)
+
+ def copy_cores(self, title: str, label: Optional[str] = None):
+ if self.allow_core_dumps:
+ return
+ for _, m in self.machines.items():
+ if m.ssh_reachable:
+ directory = "%s-%s-%s.core" % (label or self.label(), m.label, title)
+ dest = os.path.abspath(directory)
+ # overwrite core dumps from previous retries
+ if os.path.exists(dest):
+ shutil.rmtree(dest)
+ m.download_dir("/var/lib/systemd/coredump", dest)
+ try:
+ os.rmdir(dest)
+ except OSError as ex:
+ if ex.errno == errno.ENOTEMPTY:
+ print("Core dumps downloaded to %s" % (dest))
+ # Enable this to temporarily(!) create artifacts for core dumps, if a crash is hard to reproduce
+ # attach(dest, move=True)
+
+ def settle_cpu(self):
+ """Wait until CPU usage in the VM settles down
+
+ Wait until the process with the highest CPU usage drops below 20%
+ usage. Wait for up to a minute, then return. There is no error if the
+ CPU stays busy, as usually a test then should just try to run anyway.
+ """
+ for _ in range(20):
+ # get the CPU percentage of the most busy process
+ busy_proc = self.machine.execute("ps --no-headers -eo pcpu,pid,args | sort -k 1 -n -r | head -n1")
+ if float(busy_proc.split()[0]) < 20.0:
+ break
+ time.sleep(3)
+
+ def sed_file(self, expr: str, path: str, apply_change_action: Optional[str] = None):
+ """sed a file on primary machine
+
+ This is safe for @nondestructive tests, the file will be restored during cleanup.
+
+ The optional apply_change_action will be run both after sedding and after restoring the file.
+ """
+ m = self.machine
+ m.execute(f"sed -i.cockpittest '{expr}' {path}")
+ if apply_change_action:
+ m.execute(apply_change_action)
+
+ if self.is_nondestructive():
+ if apply_change_action:
+ self.addCleanup(m.execute, apply_change_action)
+ self.addCleanup(m.execute, f"mv {path}.cockpittest {path}")
+
+ def file_exists(self, path: str) -> bool:
+ """Check if file exists on test machine"""
+
+ return self.machine.execute(f"if test -e {path}; then echo yes; fi").strip() != ""
+
+ def restore_dir(self, path: str, post_restore_action: Optional[str] = None, reboot_safe: bool = False,
+ restart_unit: Optional[str] = None):
+ """Backup/restore a directory for a nondestructive test
+
+ This takes care to not ever touch the original content on disk, but uses transient overlays.
+ As this uses a bind mount, it does not work for files that get changed atomically (with mv);
+ use restore_file() for these.
+
+ `restart_unit` will be stopped before restoring path, and restarted afterwards if it was running.
+ The optional post_restore_action will run after restoring the original content.
+
+ If the directory needs to survive reboot, `reboot_safe=True` needs to be specified; then this
+ will just backup/restore the directory instead of bind-mounting, which is less robust.
+ """
+ if not self.is_nondestructive() and not self.machine.ostree_image:
+ return # skip for efficiency reasons
+
+ exe = self.machine.execute
+
+ if not self.file_exists(path):
+ self.addCleanup(exe, f"rm -rf '{path}'")
+ return
+
+ backup = os.path.join(self.vm_tmpdir, path.replace('/', '_'))
+ exe(f"mkdir -p {self.vm_tmpdir}; cp -a {path}/ {backup}/")
+
+ if not reboot_safe:
+ exe(f"mount -o bind {backup} {path}")
+
+ if restart_unit:
+ restart_stamp = f"/run/cockpit_restart_{restart_unit}"
+ self.addCleanup(
+ exe,
+ f"if [ -e {restart_stamp} ]; then systemctl start {restart_unit}; rm {restart_stamp}; fi"
+ )
+
+ if post_restore_action:
+ self.addCleanup(exe, post_restore_action)
+
+ if reboot_safe:
+ self.addCleanup(exe, f"rm -rf {path}; mv {backup} {path}")
+ else:
+ # HACK: a lot of tests call this on /home/...; that restoration happens before killing all user
+ # processes in nonDestructiveSetup(), so we have to do it lazily
+ if path.startswith("/home"):
+ cmd = f"umount -lf {path}"
+ else:
+ cmd = f"umount {path} || {{ fuser -uvk {path} {path}/* >&2 || true; sleep 1; umount {path}; }}"
+ self.addCleanup(exe, cmd)
+
+ if restart_unit:
+ self.addCleanup(exe, f"if systemctl --quiet is-active {restart_unit}; then touch {restart_stamp}; fi; "
+ f"systemctl stop {restart_unit}")
+
+ def restore_file(self, path: str, post_restore_action: Optional[str] = None):
+ """Backup/restore a file for a nondestructive test
+
+ This is less robust than restore_dir(), but works for files that need to get changed atomically.
+
+ If path does not currently exist, it will be removed again on cleanup.
+ """
+ if not self.is_nondestructive():
+ return # skip for efficiency reasons
+
+ if post_restore_action:
+ self.addCleanup(self.machine.execute, post_restore_action)
+
+ if self.file_exists(path):
+ backup = os.path.join(self.vm_tmpdir, path.replace('/', '_'))
+ self.machine.execute(f"mkdir -p {self.vm_tmpdir}; cp -a {path} {backup}")
+ self.addCleanup(self.machine.execute, f"mv {backup} {path}")
+ else:
+ self.addCleanup(self.machine.execute, f"rm -f {path}")
+
+ def write_file(self, path: str, content: str, append: bool = False, owner: Optional[str] = None, perm: Optional[str] = None,
+ post_restore_action: Optional[str] = None):
+ """Write a file on primary machine
+
+ This is safe for @nondestructive tests, the file will be removed during cleanup.
+
+ If @append is True, append to existing file instead of replacing it.
+ @owner is the desired file owner as chown shell string (e.g. "admin:nogroup")
+ @perm is the desired file permission as chmod shell string (e.g. "0600")
+ """
+ m = self.machine
+ self.restore_file(path, post_restore_action=post_restore_action)
+ m.write(path, content, append=append, owner=owner, perm=perm)
+
+ def enable_root_login(self):
+ """Enable root login
+
+ By default root login is disabled in cockpit, removing the root entry of /etc/cockpit/disallowed-users allows root to login.
+ """
+
+ # fedora-coreos runs cockpit-ws in a containter so does not install cockpit-ws on the host
+ disallowed_conf = '/etc/cockpit/disallowed-users'
+ if not self.machine.ostree_image and self.file_exists(disallowed_conf):
+ self.sed_file('/root/d', disallowed_conf)
+
+ def setup_provisioned_hosts(self, disable_preload: bool = False):
+ """Setup provisioned hosts for testing
+
+ This sets the hostname of all machines to the name given in the
+ provision dictionary and optionally disabled preload.
+ """
+ for name, m in self.machines.items():
+ m.execute(f"hostnamectl set-hostname {name}")
+ if disable_preload:
+ self.disable_preload("packagekit", "playground", "systemd", machine=m)
+
+ def authorize_pubkey(self, machine, account, pubkey):
+ machine.execute(f"a={account} d=/home/$a/.ssh; mkdir -p $d; chown $a:$a $d; chmod 700 $d")
+ machine.write(f"/home/{account}/.ssh/authorized_keys", pubkey)
+ machine.execute(f"a={account}; chown $a:$a /home/$a/.ssh/authorized_keys")
+
+ def get_pubkey(self, machine, account):
+ return machine.execute(f"cat /home/{account}/.ssh/id_rsa.pub")
+
+ def setup_ssh_auth(self):
+ self.machine.execute("d=/home/admin/.ssh; mkdir -p $d; chown admin:admin $d; chmod 700 $d")
+ self.machine.execute("test -f /home/admin/.ssh/id_rsa || ssh-keygen -f /home/admin/.ssh/id_rsa -t rsa -N ''")
+ self.machine.execute("chown admin:admin /home/admin/.ssh/id_rsa*")
+ pubkey = self.get_pubkey(self.machine, "admin")
+
+ for m in self.machines:
+ self.authorize_pubkey(self.machines[m], "admin", pubkey)
+
+
+###########################
+# Global helper functions
+#
+
+
+def jsquote(js: str) -> str:
+ return json.dumps(js)
+
+
+def get_decorator(method, _class, name, default=None):
+ """Get decorator value of a test method or its class
+
+ Return None if the decorator was not set.
+ """
+ attr = "_testlib__" + name
+ return getattr(method, attr, getattr(_class, attr, default))
+
+
+###########################
+# Test decorators
+#
+
+def skipBrowser(reason: str, *browsers: str):
+ """Decorator for skipping a test on given browser(s)
+
+ Skips a test for provided *reason* on *browsers*.
+ """
+ browser = os.environ.get("TEST_BROWSER", "chromium")
+ if browser in browsers:
+ return unittest.skip(f"{browser}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipImage(reason: str, *images: str):
+ """Decorator for skipping a test for given image(s)
+
+ Skip a test for a provided *reason* for given *images*. These
+ support Unix shell style patterns via fnmatch.fnmatch.
+
+ Example: @skipImage("no btrfs support on RHEL", "rhel-*")
+ """
+ if any(fnmatch.fnmatch(testvm.DEFAULT_IMAGE, img) for img in images):
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def onlyImage(reason: str, *images: str):
+ """Decorator to only run a test on given image(s)
+
+ Only run this test on provided *images* for *reason*. These
+ support Unix shell style patterns via fnmatch.fnmatch.
+ """
+ if not any(fnmatch.fnmatch(testvm.DEFAULT_IMAGE, arg) for arg in images):
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipOstree(reason: str):
+ """Decorator for skipping a test on OSTree images
+
+ Skip test for *reason* on OSTree images defined in OSTREE_IMAGES in bots/lib/constants.py.
+ """
+ if testvm.DEFAULT_IMAGE in OSTREE_IMAGES:
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: {reason}")
+ return lambda testEntity: testEntity
+
+
+def skipDistroPackage():
+ """For tests which apply to BaseOS packages
+
+ With that, tests can evolve with latest code, without constantly breaking them when
+ running against older package versions in the -distropkg tests.
+ """
+ if 'distropkg' in testvm.DEFAULT_IMAGE:
+ return unittest.skip(f"{testvm.DEFAULT_IMAGE}: Do not test BaseOS packages")
+ return lambda testEntity: testEntity
+
+
+def nondestructive(testEntity):
+ """Tests decorated as nondestructive will all run against the same VM
+
+ Can be used on test classes and individual test methods.
+ """
+ setattr(testEntity, '_testlib__nondestructive', True)
+ return testEntity
+
+
+def no_retry_when_changed(testEntity):
+ """Tests decorated with no_retry_when_changed will only run once if they've been changed
+
+ Tests that have been changed are expected to succeed 3 times, if the test
+ takes a long time, this prevents timeouts. Can be used on test classes and
+ individual methods.
+ """
+ setattr(testEntity, '_testlib__no_retry_when_changed', True)
+ return testEntity
+
+
+def todo(reason: str = ''):
+ """Tests decorated with @todo are expected to fail.
+
+ An optional reason can be given, and will appear in the TAP output if run
+ via run-tests.
+ """
+ def wrapper(testEntity):
+ setattr(testEntity, '_testlib__todo', reason)
+ return testEntity
+ return wrapper
+
+
+def todoPybridge(reason: Optional[str] = None):
+ if not reason:
+ reason = 'still fails with python bridge'
+
+ def wrap(test_method):
+ @functools.wraps(test_method)
+ def wrapped_test(self):
+ is_pybridge = self.is_pybridge()
+ try:
+ test_method(self)
+ if is_pybridge:
+ return self.fail(reason)
+ return None
+ # only accept our testlib Errors, plus RuntimeError for TestSuperuserDashboardOldMachine
+ except (Error, RuntimeError):
+ if is_pybridge:
+ traceback.print_exc()
+ return self.skipTest(reason)
+ raise
+
+ return wrapped_test
+
+ return wrap
+
+
+def todoPybridgeRHEL8(reason: Optional[str] = None):
+ if testvm.DEFAULT_IMAGE.startswith('rhel-8') or testvm.DEFAULT_IMAGE.startswith('centos-8'):
+ return todoPybridge(reason or 'known fail on el8 with python bridge')
+ return lambda testEntity: testEntity
+
+
+def timeout(seconds: int):
+ """Change default test timeout of 600s, for long running tests
+
+ Can be applied to an individual test method or the entire class. This only
+ applies to test/common/run-tests, not to calling check-* directly.
+ """
+ def wrapper(testEntity):
+ setattr(testEntity, '_testlib__timeout', seconds)
+ return testEntity
+ return wrapper
+
+
+class TapRunner:
+ def __init__(self, verbosity=1):
+ self.verbosity = verbosity
+
+ def runOne(self, test):
+ result = unittest.TestResult()
+ print('# ----------------------------------------------------------------------')
+ print('#', test)
+ try:
+ unittest.TestSuite([test]).run(result)
+ except KeyboardInterrupt:
+ result.addError(test, sys.exc_info())
+ return result
+ except Exception:
+ result.addError(test, sys.exc_info())
+ sys.stderr.write(f"Unexpected exception while running {test}\n")
+ sys.stderr.write(traceback.format_exc())
+ return result
+ else:
+ result.printErrors()
+
+ if result.skipped:
+ print(f"# Result {test} skipped: {result.skipped[0][1]}")
+ elif result.wasSuccessful():
+ print(f"# Result {test} succeeded")
+ else:
+ for failure in result.failures:
+ print(failure[1])
+ for error in result.errors:
+ print(error[1])
+ print(f"# Result {test} failed")
+ return result
+
+ def run(self, testable):
+ tests = []
+
+ # The things to test
+ def collapse(test, tests):
+ if isinstance(test, unittest.TestCase):
+ tests.append(test)
+ else:
+ for t in test:
+ collapse(t, tests)
+ collapse(testable, tests)
+ test_count = len(tests)
+
+ # For statistics
+ start = time.time()
+ failures = 0
+ skips = []
+ while tests:
+ # The next test to test
+ test = tests.pop(0)
+ result = self.runOne(test)
+ if not result.wasSuccessful():
+ failures += 1
+ skips += result.skipped
+
+ # Report on the results
+ duration = int(time.time() - start)
+ hostname = socket.gethostname().split(".")[0]
+ details = f"[{duration}s on {hostname}]"
+
+ MachineCase.kill_global_machine()
+
+ # Return 77 if all tests were skipped
+ if len(skips) == test_count:
+ sys.stdout.write("# SKIP {0}\n".format(", ".join([f"{s[0]!s} {s[1]}" for s in skips])))
+ return 77
+ if failures:
+ sys.stdout.write("# {0} TEST{1} FAILED {2}\n".format(failures, "S" if failures > 1 else "", details))
+ return 1
+ else:
+ sys.stdout.write("# {0} TEST{1} PASSED {2}\n".format(test_count, "S" if test_count > 1 else "", details))
+ return 0
+
+
+def print_tests(tests):
+ for test in tests:
+ if isinstance(test, unittest.TestSuite):
+ print_tests(test)
+ elif isinstance(test, unittest.loader._FailedTest):
+ name = test.id().replace("unittest.loader._FailedTest.", "")
+ print(f"Error: '{name}' does not match a test", file=sys.stderr)
+ else:
+ print(test.id().replace("__main__.", ""))
+
+
+def arg_parser(enable_sit=True):
+ parser = argparse.ArgumentParser(description='Run Cockpit test(s)')
+ parser.add_argument('-v', '--verbose', dest="verbosity", action='store_const',
+ const=2, help='Verbose output')
+ parser.add_argument('-t', "--trace", dest='trace', action='store_true',
+ help='Trace machine boot and commands')
+ parser.add_argument('-q', '--quiet', dest='verbosity', action='store_const',
+ const=0, help='Quiet output')
+ if enable_sit:
+ parser.add_argument('-s', "--sit", dest='sit', action='store_true',
+ help="Sit and wait after test failure")
+ parser.add_argument('--nonet', dest="fetch", action="store_false",
+ help="Don't go online to download images or data")
+ parser.add_argument('--enable-network', dest='enable_network', action='store_true',
+ help="Enable network access for tests")
+ parser.add_argument('--coverage', action='store_true',
+ help="Collect code coverage data")
+ parser.add_argument("-l", "--list", action="store_true", help="Print the list of tests that would be executed")
+ # TMT compatibility, pass testnames as whitespace separated list
+ parser.add_argument('tests', nargs='*', default=os.getenv("TEST_NAMES").split() if os.getenv("TEST_NAMES") else [])
+
+ parser.set_defaults(verbosity=1, fetch=True)
+ return parser
+
+
+def test_main(options=None, suite=None, attachments=None, **kwargs):
+ """
+ Run all test cases, as indicated by arguments.
+
+ If no arguments are given on the command line, all test cases are
+ executed. Otherwise only the given test cases are run.
+ """
+
+ global opts
+
+ # Turn off python stdout buffering
+ buf_arg = 0
+ os.environ['PYTHONUNBUFFERED'] = '1'
+ buf_arg = 1
+ sys.stdout.flush()
+ sys.stdout = os.fdopen(sys.stdout.fileno(), 'w', buf_arg)
+
+ standalone = options is None
+ parser = arg_parser()
+ parser.add_argument('--machine', metavar="hostname[:port]", dest="address",
+ default=None, help="Run this test against an already running machine")
+ parser.add_argument('--browser', metavar="hostname[:port]", dest="browser",
+ default=None, help="When using --machine, use this cockpit web address")
+
+ if standalone:
+ options = parser.parse_args()
+
+ # Sit should always imply verbose
+ if options.sit:
+ options.verbosity = 2
+
+ # Have to copy into opts due to python globals across modules
+ for (key, value) in vars(options).items():
+ setattr(opts, key, value)
+
+ opts.address = getattr(opts, "address", None)
+ opts.browser = getattr(opts, "browser", None)
+ opts.attachments = os.environ.get("TEST_ATTACHMENTS", attachments)
+ if opts.attachments:
+ os.makedirs(opts.attachments, exist_ok=True)
+
+ import __main__
+ if len(opts.tests) > 0:
+ if suite:
+ parser.error("tests may not be specified when running a predefined test suite")
+ suite = unittest.TestLoader().loadTestsFromNames(opts.tests, module=__main__)
+ elif not suite:
+ suite = unittest.TestLoader().loadTestsFromModule(__main__)
+
+ if options.list:
+ print_tests(suite)
+ return 0
+
+ attach(os.path.join(TEST_DIR, "common/pixeldiff.html"))
+ attach(os.path.join(TEST_DIR, "common/link-patterns.json"))
+
+ runner = TapRunner(verbosity=opts.verbosity)
+ ret = runner.run(suite)
+ if not standalone:
+ return ret
+ sys.exit(ret)
+
+
+class Error(Exception):
+ def __init__(self, msg):
+ self.msg = msg
+
+ def __str__(self):
+ return self.msg
+
+
+def wait(func: Callable, msg: Optional[str] = None, delay: int = 1, tries: int = 60):
+ """Wait for FUNC to return something truthy, and return that.
+
+ FUNC is called repeatedly until it returns a true value or until a
+ timeout occurs. In the latter case, a exception is raised that
+ describes the situation. The exception is either the last one
+ thrown by FUNC, or includes MSG, or a default message.
+
+ :param func: The function to call
+ :param msg: A error message to use when the timeout occurs. Defaults
+ to a generic message.
+ :param delay: How long to wait between calls to FUNC, in seconds. (default 1)
+ :param tries: How often to call FUNC. (defaults 60)
+ :raises Error: When a timeout occurs.
+ """
+
+ t = 0
+ while t < tries:
+ try:
+ val = func()
+ if val:
+ return val
+ except Exception:
+ if t == tries - 1:
+ raise
+ else:
+ pass
+ t = t + 1
+ sleep(delay)
+ raise Error(msg or "Condition did not become true.")
+
+
+def sit(machines=None):
+ """
+ Wait until the user confirms to continue.
+
+ The current test case is suspended so that the user can inspect
+ the browser.
+ """
+
+ for (_, machine) in (machines or {}).items():
+ sys.stderr.write(machine.diagnose())
+ print("Press RET to continue...")
+ sys.stdin.readline()
diff --git a/test/reference-image b/test/reference-image
new file mode 100644
index 0000000..eb90814
--- /dev/null
+++ b/test/reference-image
@@ -0,0 +1 @@
+fedora-39
diff --git a/test/run b/test/run
new file mode 100755
index 0000000..b50fe1e
--- /dev/null
+++ b/test/run
@@ -0,0 +1,17 @@
+#! /bin/bash
+# This is the expected entry point for Cockpit CI; will be called without
+# arguments but with an appropriate $TEST_OS
+
+set -eu
+
+export RUN_TESTS_OPTIONS=--track-naughties
+
+TEST_SCENARIO=${TEST_SCENARIO:-}
+
+if [ "$TEST_SCENARIO" == "devel" ]; then
+ export TEST_COVERAGE=yes
+fi
+
+make codecheck
+make check
+make po/podman.pot
diff --git a/test/static-code b/test/static-code
new file mode 100755
index 0000000..4734a7c
--- /dev/null
+++ b/test/static-code
@@ -0,0 +1,200 @@
+#!/bin/bash
+# run static code checks like eslint, flake8, mypy, ruff, vulture.
+
+set -eu
+
+# requires: .flake8
+# requires: pyproject.toml
+# requires: containers/flatpak/test/ruff.toml
+# requires: pkg/ruff.toml
+# requires: test/common/ruff.toml
+# requires: test/example/ruff.toml
+# requires: test/verify/ruff.toml
+# requires: tools/vulture-suppressions/ruff.toml
+
+# we consider any function named test_* to be a test case
+# each test is considered to succeed if it exits with no output
+# exit with status 77 is a skip, with the message in the output
+# otherwise, any output is a failure, even if exit status is 0
+
+# note: `set -e` is not active during the tests.
+
+find_scripts() {
+ # Helper to find all scripts in the tree
+ (
+ # Any non-binary file which contains a given shebang
+ git grep --cached -lIz '^#!.*'"$1"
+ shift
+ # Any file matching the provided globs
+ git ls-files -z "$@"
+ ) | sort -z | uniq -z
+}
+
+find_python_files() {
+ find_scripts 'python3' '*.py'
+}
+
+test_flake8() {
+ command -v flake8 >/dev/null || skip 'no flake8'
+ find_python_files | xargs -r -0 flake8
+}
+
+test_ruff() {
+ command -v ruff >/dev/null || skip 'no ruff'
+ find_python_files | xargs -r -0 ruff check --no-cache
+}
+
+if [ "${WITH_PARTIAL_TREE:-0}" = 0 ]; then
+ mypy_strict_files='
+ src/cockpit/__init__.py
+ src/cockpit/_version.py
+ src/cockpit/jsonutil.py
+ src/cockpit/protocol.py
+ src/cockpit/transports.py
+ '
+ test_mypy() {
+ command -v mypy >/dev/null || skip 'no mypy'
+ for pkg in systemd_ctypes ferny bei; do
+ test -e "src/cockpit/_vendor/${pkg}/__init__.py" || skip "no ${pkg}"
+ done
+ mypy --no-error-summary src/cockpit test/pytest
+ # test scripts individually, to avoid clashing on `__main__`
+ # also skip integration tests, they are too big and not annotated
+ find_scripts 'python3' "*.none" | grep -zv 'test/' | xargs -r -0 -n1 mypy --no-error-summary
+ mypy --no-error-summary --strict $mypy_strict_files
+ }
+
+ test_vulture() {
+ # vulture to find unused variables/functions
+ command -v vulture >/dev/null || skip 'no vulture'
+ find_python_files | xargs -r -0 vulture
+ }
+fi
+
+
+test_js_translatable_strings() {
+ # Translatable strings must be marked with _(""), not _('')
+
+ ! git grep -n -E "(gettext|_)\(['\`]" -- {src,pkg}/'*'.{js,jsx}
+}
+
+if [ "${WITH_PARTIAL_TREE:-0}" = 0 ]; then
+ test_eslint() {
+ test -x node_modules/.bin/eslint -a -x /usr/bin/node || skip 'no eslint'
+ find_scripts 'node' '*.js' '*.jsx' | xargs -0 node_modules/.bin/eslint
+ }
+fi
+
+test_stylelint() {
+ test -x node_modules/.bin/stylelint -a -x /usr/bin/node || skip 'no stylelint'
+ git ls-files -z '*.css' '*.scss' | xargs -r -0 node_modules/.bin/stylelint
+}
+
+test_no_translatable_attr() {
+ # Use of translatable attribute in HTML: should be 'translate' instead
+
+ ! git grep -n 'translatable=["'\'']yes' -- pkg doc
+}
+
+test_unsafe_security_policy() {
+ # It's dangerous to have 'unsafe-inline' or 'unsafe-eval' in our
+ # content-security-policy entries.
+
+ git grep -lIz -E 'content-security-policy.*(\*|unsafe)' 'pkg/*/manifest.json' | while read -d '' filename; do
+ if test ! -f "$(dirname ${filename})/content-security-policy.override"; then
+ echo "${filename} contains unsafe content security policy"
+ fi
+ done
+}
+
+test_json_verify() {
+ # Check all JSON files for validity
+
+ git ls-files -z '*.json' | while read -d '' filename; do
+ python3 -m json.tool "${filename}" /dev/null 2>&1 | sed "s@^@${filename}: @"
+ done
+}
+
+test_html_verify() {
+ # Check all HTML files for syntactic validity
+
+ git ls-files -z 'pkg/*.html' | while read -d '' filename; do
+ if ! python3 -c "import xml.etree.ElementTree as ET; ET.parse('${filename}')"; then
+ echo "${filename} contains invalid XML"
+ fi
+ done
+}
+
+test_include_config_h() {
+ # Every C file should #include "config.h" at the top
+
+ git ls-files -cz '*.c' | while read -d '' filename; do
+ if sed -n '/^#include "config.h"$/q1; /^\s*#/q;' "${filename}"; then
+ printf '%s: #include "config.h" is not the first line\n' "${filename}"
+ fi
+ done
+}
+
+### end of tests. start of machinery.
+
+skip() {
+ printf "%s\n" "$*"
+ exit 77
+}
+
+main() {
+ if [ $# = 0 ]; then
+ tap=''
+ elif [ $# = 1 -a "$1" = "--tap" ]; then
+ tap='1'
+ else
+ printf "usage: %s [--tap]\n" "$0" >&2
+ exit 1
+ fi
+
+ cd "${0%/*}/.."
+ if [ ! -e .git ]; then
+ echo '1..0 # SKIP not in a git checkout'
+ exit 0
+ fi
+
+ exit_status=0
+ counter=0
+
+ tests=($(compgen -A function 'test_'))
+ [ -n "${tap}" ] && printf "1..%d\n" "${#tests[@]}"
+
+ for test_function in "${tests[@]}"; do
+ path="/static-code/$(echo ${test_function} | tr '_' '-')"
+ counter=$((counter + 1))
+ fail=''
+ skip=''
+
+ # run the test, capturing its output and exit status
+ output="$(${test_function} 2>&1)" && test_status=0 || test_status=$?
+
+ if [ "${test_status}" = 77 ]; then
+ if [ -z "${tap}" ]; then
+ printf >&2 "WARNING: skipping %s: %s\n" "${path}" "${output}"
+ fi
+ skip=" # SKIP ${output}"
+ output=''
+ elif [ "${test_status}" != 0 -o -n "${output}" ]; then
+ exit_status=1
+ fail=1
+ fi
+
+ # Only print output on failures or --tap mode
+ [ -n "${tap}" -o -n "${fail}" ] || continue
+
+ # excluding the plan, this is the only output that we ever generate
+ printf "%s %d %s%s\n" "${fail:+not }ok" "${counter}" "${path}" "${skip}"
+ if [ -n "${output}" ]; then
+ printf "%s\n" "${output}" | sed -e 's/^/# /'
+ fi
+ done
+
+ exit "${exit_status}"
+}
+
+main "$@"
diff --git a/test/vm.install b/test/vm.install
new file mode 100755
index 0000000..f6c52cf
--- /dev/null
+++ b/test/vm.install
@@ -0,0 +1,38 @@
+#!/bin/sh
+# image-customize script to prepare a bots VM for cockpit-podman testing
+set -eu
+
+if grep -q ID.*debian /usr/lib/os-release; then
+ # Debian does not enable user namespaces by default
+ echo kernel.unprivileged_userns_clone = 1 > /etc/sysctl.d/00-local-userns.conf
+ systemctl restart systemd-sysctl
+
+ # disable services that get in the way of /var/lib/containers
+ if systemctl is-enabled docker.service; then
+ systemctl disable docker.service
+ fi
+fi
+
+# don't force https:// (self-signed cert)
+printf "[WebService]\\nAllowUnencrypted=true\\n" > /etc/cockpit/cockpit.conf
+
+if type firewall-cmd >/dev/null 2>&1; then
+ firewall-cmd --add-service=cockpit --permanent
+fi
+
+. /usr/lib/os-release
+
+# Remove extra images, tests assume our specific set
+# Since 4.0 podman now ships the pause image
+podman images --format '{{.Repository}}:{{.Tag}}' | grep -Ev 'localhost/test-|pause|cockpit/ws' | xargs -r podman rmi -f
+
+# tests reset podman, save the images
+mkdir -p /var/lib/test-images
+for img in $(podman images --format '{{.Repository}}:{{.Tag}}'); do
+ fname="$(echo "$img" | tr -dc '[a-zA-Z-]')"
+ podman save -o "/var/lib/test-images/${fname}.tar" "$img"
+done
+
+# 15minutes after boot tmp files are removed and podman stores some tmp lock files
+systemctl disable --now systemd-tmpfiles-clean.timer
+systemctl --global disable systemd-tmpfiles-clean.timer
diff --git a/tools/node-modules b/tools/node-modules
new file mode 100755
index 0000000..a8519e1
--- /dev/null
+++ b/tools/node-modules
@@ -0,0 +1,198 @@
+#!/bin/sh
+
+# shellcheck disable=SC3043 # local is not POSIX, but every shell has it
+# shellcheck disable=SC3013,SC3045 # ditto for test {-nt,-t}
+
+GITHUB_REPO='node-cache'
+SUBDIR='node_modules'
+
+V="${V-0}" # default to friendly messages
+
+set -eu
+cd "${0%/*}/.."
+# shellcheck source-path=SCRIPTDIR/..
+. test/common/git-utils.sh
+
+cmd_remove() {
+ # if we did this for ourselves the rm is enough, but it might be the case
+ # that someone actually used git-submodule to fetch this, so clean up after
+ # that as well. NB: deinit nicely recreates the empty directory for us.
+ message REMOVE node_modules
+ rm -rf node_modules
+ git submodule deinit node_modules
+ rm -rf -- "$(git rev-parse --absolute-git-dir)/modules/node_modules"
+}
+
+cmd_checkout() {
+ # we default to check out the node_modules corresponding to the gitlink in the index
+ local force=""
+ if [ "${1-}" = "--force" ]; then
+ force="1"
+ shift
+ fi
+
+ local sha="${1-$(get_index_gitlink node_modules)}"
+
+ # fetch by sha to prevent us from downloading something we don't want
+ fetch_sha_to_cache "${sha}"
+
+ # verify that our package.json is equal to the one the cached node_modules
+ # was created with, unless --force is given
+ if [ -z "$force" ]; then
+ if ! cmp_from_cache "${sha}" '.package.json' 'package.json'; then
+ cat >&2 <<EOF
+
+*** node_modules ${sha} doesn't match our package.json
+*** refusing to automatically check out node_modules
+
+Options:
+
+ - tools/node-modules checkout --force # disable this check
+
+ - tools/node-modules install # npm install with our package.json
+
+$0: *** aborting
+
+EOF
+ exit 1
+ fi
+ fi
+
+ # we're actually going to do this; let's remove the old one
+ cmd_remove
+
+ # and check out the new one
+ # we need to use the tag name here, unfortunately
+ clone_from_cache "${sha}"
+}
+
+cmd_install() {
+ test -e bots || test/common/make-bots
+
+ # We first read the result directly into the cache, then we unpack it.
+ tree="$(bots/npm download < package.json | tar_to_cache)"
+ commit="$(sha256sum package.json | git_cache commit-tree "${tree}")"
+ git_cache tag "sha-${commit}" "${commit}"
+ cmd_checkout "${commit}"
+
+ cat <<EOF
+Next steps:
+
+ - git add node_modules && git commit
+ - tools/node-modules push
+
+EOF
+}
+
+cmd_push() {
+ # push via the cache: the shared history with the remote helps to thin out the pack we send
+ tag="sha-$(git -C node_modules rev-parse HEAD)"
+ message PUSH "${GITHUB_REPO} ${tag}"
+ git_cache push "${SSH_REMOTE}" "${tag}"
+}
+
+cmd_verify() {
+ test -e bots || test/common/make-bots
+
+ # Verifies that the package.json and node_modules of the given commit match.
+ commit="$(git rev-parse "$1:node_modules")"
+ fetch_sha_to_cache "${commit}"
+
+ committed_tree="$(git_cache rev-parse "${commit}^{tree}")"
+ expected_tree="$(git cat-file blob "$1:package.json" | bots/npm download | tar_to_cache)"
+
+ if [ "${committed_tree}" != "${expected_tree}" ]; then
+ exec >&2
+ printf "\nCommit %s package.json and node_modules aren't in sync!\n\n" "$1"
+ git --no-pager show --stat "$1"
+
+ printf "\nThe above commit refers to the following node_modules commit:\n\n"
+ git_cache --no-pager show --no-patch "${commit}"
+
+ printf "\nOur attempt to recreate that commit differs as follows:\n\n"
+ git_cache --no-pager diff --stat "${commit}" "${expected_tree}" --
+ git_cache --no-pager diff "${commit}" "${expected_tree}" -- .package-lock.json
+ exit 1
+ fi
+}
+
+# called from Makefile.am
+cmd_make_package_lock_json() {
+ # Run from make to ensure package-lock.json is up to date
+
+ # package-lock.json is used as the stamp file for all things that use
+ # node_modules, so this is the main bit of glue that drives the entire process
+
+ # We try our best not to touch package-lock.json unless it actually changes
+
+ # This isn't going to work for a tarball, but as long as
+ # package-lock.json is already there, and newer than package.json,
+ # we're OK
+ if [ ! -e .git ]; then
+ if [ package-lock.json -nt package.json ]; then
+ exit 0
+ fi
+
+ echo "*** Can't update node modules unless running from git" >&2
+ exit 1
+ fi
+
+ # Otherwise, our main goal is to ensure that the node_modules from
+ # the index is the one that we actually have.
+ local sha
+ sha="$(get_index_gitlink node_modules)"
+ if [ ! -e node_modules/.git ]; then
+ # nothing there yet...
+ cmd_checkout
+ elif [ "$(git -C node_modules rev-parse HEAD)" != "${sha}" ]; then
+ # wrong thing there...
+ cmd_checkout
+ fi
+
+ # This check is more about catching local changes to package.json than
+ # about validating something we just checked out:
+ if ! cmp -s node_modules/.package.json package.json; then
+ cat 2>&1 <<EOF
+*** package.json is out of sync with node_modules
+*** If you modified package.json, please run:
+***
+*** tools/node-modules install
+***
+*** and add the result to the index.
+EOF
+ exit 1
+ fi
+
+ # Only copy the package-lock.json if it differs from the one we have
+ if ! cmp -s node_modules/.package-lock.json package-lock.json; then
+ message COPY package-lock.json
+ cp node_modules/.package-lock.json package-lock.json
+ fi
+
+ # We're now in a situation where:
+ # - the checked out node_modules is equal to the gitlink in the index
+ # - the package.json in the tree is equal to the one in node_modules
+ # - ditto package-lock.json
+ exit 0
+}
+
+main() {
+ if [ $# = 0 ]; then
+ # don't list the "private" ones
+ echo 'This command requires a subcommand: remove checkout install push verify'
+ exit 1
+ fi
+
+ local fname
+ fname="$(printf 'cmd_%s' "$1" | tr '-' '_')"
+ if ! type -t "${fname}" | grep -q function; then
+ echo "Unknown subcommand '$1'"
+ exit 1
+ fi
+
+ shift
+ [ -n "${quiet}" ] || set -x
+ "${fname}" "$@"
+}
+
+main "$@"