├── .devcontainer └── devcontainer.json ├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug.yaml │ ├── config.yml │ ├── enhancement.yaml │ ├── experimental.yaml │ └── feature.yaml ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml ├── workflows │ ├── release-cli.yml │ ├── release.yml │ └── validate.yml ├── xk6-dashboard-social.png ├── xk6-dashboard-social.svg └── zizmor.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yaml ├── .vscode └── settings.json ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── Makefile ├── README.md ├── cmd ├── k6-web-dashboard │ ├── README.md │ └── main.go └── root.go ├── dashboard ├── aggregate.go ├── aggregate_test.go ├── assets.go ├── assets │ ├── .gitignore │ ├── lerna.json │ ├── package.json │ ├── packages │ │ ├── config │ │ │ ├── .eslintrc.json │ │ │ ├── .gitignore │ │ │ ├── .prettierrc.json │ │ │ ├── README.md │ │ │ ├── dist │ │ │ │ └── config.json │ │ │ ├── main.js │ │ │ ├── package.json │ │ │ └── src │ │ │ │ └── config.js │ │ ├── model │ │ │ ├── .eslintrc.json │ │ │ ├── .prettierrc.json │ │ │ ├── dist │ │ │ │ ├── index.d.ts │ │ │ │ └── index.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── Digest.ts │ │ │ │ ├── Event.ts │ │ │ │ ├── Metrics.ts │ │ │ │ ├── Samples.ts │ │ │ │ ├── Summary.ts │ │ │ │ ├── UnitType.ts │ │ │ │ └── index.ts │ │ │ └── tsconfig.json │ │ ├── report │ │ │ ├── .eslintrc.json │ │ │ ├── .gitignore │ │ │ ├── .prettierignore │ │ │ ├── .prettierrc.json │ │ │ ├── .testcontext.js │ │ │ ├── .testdata.ndjson.gz │ │ │ ├── dist │ │ │ │ ├── assets │ │ │ │ │ ├── circle-5a1f9f5e.svg │ │ │ │ │ └── logo-fd36a8d6.svg │ │ │ │ └── index.html │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── src │ │ │ │ ├── App │ │ │ │ │ ├── App.css.ts │ │ │ │ │ ├── App.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── assets │ │ │ │ │ └── icons │ │ │ │ │ │ ├── circle.svg │ │ │ │ │ │ └── logo.svg │ │ │ │ ├── components │ │ │ │ │ ├── Chart │ │ │ │ │ │ ├── Chart.css.ts │ │ │ │ │ │ ├── Chart.hooks.ts │ │ │ │ │ │ ├── Chart.tsx │ │ │ │ │ │ ├── Chart.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Flex │ │ │ │ │ │ ├── Flex.css.ts │ │ │ │ │ │ ├── Flex.tsx │ │ │ │ │ │ ├── Flex.types.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Grid │ │ │ │ │ │ ├── Grid.css.ts │ │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ │ ├── Grid.types.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Header │ │ │ │ │ │ ├── Header.css.ts │ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ │ ├── Header.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Panel.tsx │ │ │ │ │ ├── Section │ │ │ │ │ │ ├── Section.css.ts │ │ │ │ │ │ ├── Section.tsx │ │ │ │ │ │ ├── Section.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Summary │ │ │ │ │ │ ├── Summary.css.ts │ │ │ │ │ │ ├── Summary.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Tab │ │ │ │ │ │ ├── Tab.css.ts │ │ │ │ │ │ ├── Tab.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ └── Table │ │ │ │ │ │ ├── Table.css.ts │ │ │ │ │ │ ├── Table.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ ├── main.tsx │ │ │ │ ├── theme │ │ │ │ │ ├── colors.css.ts │ │ │ │ │ ├── global.css.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sizes.css.ts │ │ │ │ │ ├── theme.css.ts │ │ │ │ │ └── typography.css.ts │ │ │ │ ├── types │ │ │ │ │ └── config.ts │ │ │ │ ├── typings │ │ │ │ │ ├── assets.d.ts │ │ │ │ │ └── xk6-dashboard-model.d.ts │ │ │ │ └── utils │ │ │ │ │ ├── colors.ts │ │ │ │ │ ├── digest.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── object.ts │ │ │ │ │ └── theme.ts │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ ├── vite.config.ts │ │ │ └── yarn.lock │ │ ├── ui │ │ │ ├── .eslintrc.json │ │ │ ├── .gitignore │ │ │ ├── .prettierignore │ │ │ ├── .prettierrc.json │ │ │ ├── README.md │ │ │ ├── dist │ │ │ │ ├── assets │ │ │ │ │ ├── dark_mode-530eef3b.svg │ │ │ │ │ ├── expand_less-2d2317d9.svg │ │ │ │ │ ├── expand_more-c6b4db32.svg │ │ │ │ │ ├── hour_glass-20497ed9.svg │ │ │ │ │ ├── index-8105e2d0.css │ │ │ │ │ ├── index-beb72b0a.js │ │ │ │ │ ├── info-54caaa0b.svg │ │ │ │ │ ├── light_mode-5b543e8c.svg │ │ │ │ │ ├── logo-fd36a8d6.svg │ │ │ │ │ ├── options-ee7c6312.svg │ │ │ │ │ ├── question-abe8d232.svg │ │ │ │ │ ├── rewind_time-def68db1.svg │ │ │ │ │ ├── spinner-8a2a69f5.svg │ │ │ │ │ └── stop_watch-624e074a.svg │ │ │ │ ├── index.html │ │ │ │ └── xk6-dashboard.svg │ │ │ ├── index.html │ │ │ ├── package.json │ │ │ ├── public │ │ │ │ └── xk6-dashboard.svg │ │ │ ├── src │ │ │ │ ├── App │ │ │ │ │ ├── App.css.ts │ │ │ │ │ ├── App.tsx │ │ │ │ │ └── index.ts │ │ │ │ ├── assets │ │ │ │ │ └── icons │ │ │ │ │ │ ├── dark_mode.svg │ │ │ │ │ │ ├── expand_less.svg │ │ │ │ │ │ ├── expand_more.svg │ │ │ │ │ │ ├── hour_glass.svg │ │ │ │ │ │ ├── info.svg │ │ │ │ │ │ ├── light_mode.svg │ │ │ │ │ │ ├── logo.svg │ │ │ │ │ │ ├── options.svg │ │ │ │ │ │ ├── question.svg │ │ │ │ │ │ ├── rewind_time.svg │ │ │ │ │ │ ├── spinner.svg │ │ │ │ │ │ └── stop_watch.svg │ │ │ │ ├── components │ │ │ │ │ ├── Button │ │ │ │ │ │ ├── Button.css.ts │ │ │ │ │ │ ├── Button.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Card │ │ │ │ │ │ ├── Card.css.ts │ │ │ │ │ │ ├── Card.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Chart │ │ │ │ │ │ ├── Chart.css.ts │ │ │ │ │ │ ├── Chart.hooks.ts │ │ │ │ │ │ ├── Chart.tsx │ │ │ │ │ │ ├── Chart.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── ClickAwayListener.tsx │ │ │ │ │ ├── Collapse │ │ │ │ │ │ ├── Collapse.css.ts │ │ │ │ │ │ ├── Collapse.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Divider │ │ │ │ │ │ ├── Divider.css.ts │ │ │ │ │ │ ├── Divider.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Flex │ │ │ │ │ │ ├── Flex.css.ts │ │ │ │ │ │ ├── Flex.tsx │ │ │ │ │ │ ├── Flex.types.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Footer │ │ │ │ │ │ ├── Footer.css.ts │ │ │ │ │ │ └── Footer.tsx │ │ │ │ │ ├── Grid │ │ │ │ │ │ ├── Grid.css.ts │ │ │ │ │ │ ├── Grid.tsx │ │ │ │ │ │ ├── Grid.types.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Header │ │ │ │ │ │ ├── Header.css.ts │ │ │ │ │ │ ├── Header.tsx │ │ │ │ │ │ ├── Header.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Icon │ │ │ │ │ │ ├── Icon.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── IconButton │ │ │ │ │ │ ├── IconButton.css.ts │ │ │ │ │ │ ├── IconButton.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── LoadingContainer │ │ │ │ │ │ ├── LoadingContainer.css.ts │ │ │ │ │ │ ├── LoadingContainer.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Menu │ │ │ │ │ │ ├── Menu.css.ts │ │ │ │ │ │ ├── Menu.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Nav │ │ │ │ │ │ ├── Nav.css.ts │ │ │ │ │ │ ├── Nav.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Panel.tsx │ │ │ │ │ ├── Paper │ │ │ │ │ │ ├── Paper.css.ts │ │ │ │ │ │ ├── Paper.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Progress │ │ │ │ │ │ ├── Progress.css.ts │ │ │ │ │ │ ├── Progress.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Section │ │ │ │ │ │ ├── Section.css.ts │ │ │ │ │ │ ├── Section.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Stat │ │ │ │ │ │ ├── Stat.css.ts │ │ │ │ │ │ ├── Stat.tsx │ │ │ │ │ │ ├── Stat.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Summary │ │ │ │ │ │ ├── Summary.css.ts │ │ │ │ │ │ ├── Summary.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Table │ │ │ │ │ │ ├── Table.css.ts │ │ │ │ │ │ ├── Table.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── Tabs │ │ │ │ │ │ ├── Tabs.css.ts │ │ │ │ │ │ ├── Tabs.tsx │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── TimeRangeResetButton.tsx │ │ │ │ │ └── Tooltip │ │ │ │ │ │ ├── Tooltip.css.ts │ │ │ │ │ │ ├── Tooltip.tsx │ │ │ │ │ │ ├── Tooltip.utils.ts │ │ │ │ │ │ └── index.ts │ │ │ │ ├── hooks │ │ │ │ │ ├── index.ts │ │ │ │ │ └── useForkRef.ts │ │ │ │ ├── main.tsx │ │ │ │ ├── store │ │ │ │ │ ├── digest.tsx │ │ │ │ │ ├── theme.tsx │ │ │ │ │ └── timeRange.tsx │ │ │ │ ├── theme │ │ │ │ │ ├── animation.css.ts │ │ │ │ │ ├── colors.css.ts │ │ │ │ │ ├── global.css.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── sizes.css.ts │ │ │ │ │ ├── theme.css.ts │ │ │ │ │ └── typography.css.ts │ │ │ │ ├── types │ │ │ │ │ ├── config.ts │ │ │ │ │ └── theme.ts │ │ │ │ ├── typings │ │ │ │ │ ├── assets.d.ts │ │ │ │ │ └── xk6-dashboard-model.d.ts │ │ │ │ └── utils │ │ │ │ │ ├── chart.ts │ │ │ │ │ ├── index.ts │ │ │ │ │ ├── object.ts │ │ │ │ │ └── theme.ts │ │ │ ├── tsconfig.json │ │ │ ├── tsconfig.node.json │ │ │ └── vite.config.ts │ │ └── view │ │ │ ├── .eslintrc.json │ │ │ ├── .prettierrc.json │ │ │ ├── dist │ │ │ ├── config.d.ts │ │ │ ├── config.js │ │ │ ├── index.d.ts │ │ │ └── index.js │ │ │ ├── package.json │ │ │ ├── src │ │ │ ├── Config.ts │ │ │ ├── SeriesPlot.ts │ │ │ ├── SummaryTable.ts │ │ │ ├── format.ts │ │ │ ├── helper.ts │ │ │ ├── index.ts │ │ │ └── tooltip.ts │ │ │ └── tsconfig.json │ └── yarn.lock ├── assets_test.go ├── builtin.go ├── command.go ├── command_test.go ├── customize.go ├── customize_test.go ├── event.go ├── event_test.go ├── extension.go ├── extension_test.go ├── helper_test.go ├── meter.go ├── meter_test.go ├── options.go ├── options_test.go ├── process.go ├── record.go ├── record_test.go ├── registry.go ├── registry_test.go ├── replay.go ├── replay_test.go ├── report.go ├── report_test.go ├── sse.go ├── sse_test.go ├── testdata │ ├── assets │ │ └── packages │ │ │ ├── config │ │ │ └── dist │ │ │ │ └── config.json │ │ │ ├── report │ │ │ └── dist │ │ │ │ └── index.html │ │ │ └── ui │ │ │ └── dist │ │ │ └── index.html │ ├── customize │ │ ├── config-bad.json │ │ ├── config-custom.js │ │ ├── config.json │ │ └── config │ │ │ └── config.json │ ├── result.json │ ├── result.json.gz │ ├── result.ndjson │ └── result.ndjson.gz ├── web.go └── web_test.go ├── docs ├── .dashboard-custom.js ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md └── how-it-works.md ├── go.mod ├── go.sum ├── magefiles ├── go.mod ├── go.sum ├── go.work ├── go.work.sum ├── magefile.go └── magexk6.go ├── register.go ├── register_test.go ├── releases ├── template.md ├── v0.5.5.md ├── v0.6.0.md ├── v0.6.1.md ├── v0.7.0.md ├── v0.7.1.md ├── v0.7.2.md ├── v0.7.3.md ├── v0.7.4.md ├── v0.7.5.md └── v0.7.6.md ├── screenshot ├── k6-dashboard-cumulative.png ├── k6-dashboard-custom.png ├── k6-dashboard-html-report-screen-view.png ├── k6-dashboard-html-report.html ├── k6-dashboard-html-report.png ├── k6-dashboard-overview-cumulative.png ├── k6-dashboard-overview-dark.png ├── k6-dashboard-overview-light.png ├── k6-dashboard-overview-snapshot.png ├── k6-dashboard-report.pdf ├── k6-dashboard-report.png ├── k6-dashboard-snapshot.png ├── k6-dashboard-summary-dark.png ├── k6-dashboard-summary-light.png ├── k6-dashboard-summary.png ├── k6-dashboard-timings-cumulative.png ├── k6-dashboard-timings-dark.png ├── k6-dashboard-timings-light.png └── k6-dashboard-timings-snapshot.png ├── script.js └── scripts ├── demo ├── demo-browser.js ├── demo-grpc.js ├── demo-http.js ├── demo-rest.js ├── demo-ws.js ├── demo.js ├── hello.proto └── smurfs.js └── test.js /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xk6-dashboard", 3 | "image": "mcr.microsoft.com/devcontainers/base:1-bookworm", 4 | "customizations": { 5 | "vscode": { 6 | "settings": { 7 | "go.lintTool": "golangci-lint", 8 | "go.lintFlags": ["--fast"] 9 | }, 10 | "extensions": [ 11 | "EditorConfig.EditorConfig", 12 | "esbenp.prettier-vscode", 13 | "github.vscode-github-actions", 14 | "github.vscode-pull-request-github", 15 | "jetmartin.bats", 16 | "mads-hartmann.bash-ide-vscode", 17 | "foxundermoon.shell-format" 18 | ] 19 | } 20 | }, 21 | 22 | "features": { 23 | "ghcr.io/devcontainers/features/github-cli:1": {}, 24 | "ghcr.io/devcontainers/features/go:1": { 25 | "version": "1.24", 26 | "golangciLintVersion": "2.1.6" 27 | }, 28 | "ghcr.io/devcontainers/features/node:1": { "version": "22" }, 29 | "ghcr.io/guiyomh/features/goreleaser:0": { "version": "2.9.0" }, 30 | "ghcr.io/michidk/devcontainers-features/bun:1": { "version": "1.2.12" }, 31 | "ghcr.io/szkiba/devcontainer-features/gosec:1": { "version": "2.22.4" }, 32 | "ghcr.io/szkiba/devcontainer-features/govulncheck:1": { 33 | "version": "1.1.4" 34 | }, 35 | "ghcr.io/szkiba/devcontainer-features/cdo:1": { "version": "0.1.2" }, 36 | "ghcr.io/szkiba/devcontainer-features/mdcode:1": { "version": "0.2.0" }, 37 | "ghcr.io/szkiba/devcontainer-features/bats:1": { "version": "1.11.1" }, 38 | "ghcr.io/grafana/devcontainer-features/xk6:1": { "version": "0.19.2" } 39 | }, 40 | 41 | "remoteEnv": { 42 | "GH_TOKEN": "${localEnv:GH_TOKEN}", 43 | "GITHUB_TOKEN": "${localEnv:GITHUB_TOKEN}", 44 | "XK6_EARLY_ACCESS": "true" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = false 9 | insert_final_newline = true 10 | 11 | [*.{js,ts,yml,yaml,json,sh,bats}] 12 | indent_size = 2 13 | trim_trailing_whitespace = true 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | name: Bug report 6 | description: Use this template for reporting bugs. Please search existing issues first. 7 | labels: [bug] 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Brief summary 12 | validations: 13 | required: true 14 | - type: markdown 15 | attributes: 16 | value: '## Environment' 17 | - type: input 18 | attributes: 19 | label: k6 version 20 | validations: 21 | required: true 22 | - type: input 23 | attributes: 24 | label: xk6-dashboard version 25 | validations: 26 | required: true 27 | - type: input 28 | attributes: 29 | label: OS 30 | description: e.g. Windows 10, Arch Linux, macOS 11, etc. 31 | validations: 32 | required: true 33 | - type: input 34 | attributes: 35 | label: Docker version and image (if applicable) 36 | - type: markdown 37 | attributes: 38 | value: '## Detailed issue description' 39 | - type: textarea 40 | attributes: 41 | label: Steps to reproduce the problem 42 | validations: 43 | required: true 44 | - type: textarea 45 | attributes: 46 | label: Expected behaviour 47 | validations: 48 | required: true 49 | - type: textarea 50 | attributes: 51 | label: Actual behaviour 52 | validations: 53 | required: true 54 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | blank_issues_enabled: true 6 | contact_links: 7 | - name: Community Forum 8 | url: https://community.grafana.com/c/grafana-k6/extensions/ 9 | about: Please ask and answer questions here. 10 | - name: Documentation 11 | url: https://github.com/grafana/k6-docs 12 | about: Please add any documentation related issues here. 13 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/enhancement.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | name: Enhancement 6 | description: Use this template for suggesting enhancements to existing features. 7 | labels: [enhancement] 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Enhancement Description 12 | description: A clear and concise description of the problem or missing capability 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Suggested Solution (optional) 18 | description: If you have a solution in mind, please describe it. 19 | - type: textarea 20 | attributes: 21 | label: Already existing or connected issues / PRs (optional) 22 | description: If you have found some issues or pull requests that are related to your new issue, please link them here. 23 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/experimental.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | name: Experimental Module 6 | description: Use this template to propose requirements to become an experimental module. 7 | labels: ["experimental module"] 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Requirement Description 12 | description: A clear and concise description of the problem or missing capability 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Suggested Solution (optional) 18 | description: If you have a solution in mind, please describe it. 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | name: Feature Request 6 | description: Use this template for suggesting new features. 7 | labels: [feature] 8 | body: 9 | - type: textarea 10 | attributes: 11 | label: Feature Description 12 | description: A clear and concise description of the problem or missing capability 13 | validations: 14 | required: true 15 | - type: textarea 16 | attributes: 17 | label: Suggested Solution (optional) 18 | description: If you have a solution in mind, please describe it. 19 | - type: textarea 20 | attributes: 21 | label: Already existing or connected issues / PRs (optional) 22 | description: If you have found some issues or pull requests that are related to your new issue, please link them here. 23 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## What? 2 | 3 | 4 | 5 | ## Why? 6 | 7 | 8 | 9 | ## Checklist 10 | 11 | 15 | 16 | - [ ] I have performed a self-review of my code. 17 | - [ ] I have added tests for my changes. 18 | - [ ] I have run linter locally (`mage lint`) and all checks pass. 19 | - [ ] I have run tests locally (`mage test`) and all tests pass. 20 | - [ ] I have commented on my code, particularly in hard-to-understand areas. 21 | 22 | 23 | ## Related PR(s)/Issue(s) 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | version: 2 6 | updates: 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "weekly" 11 | -------------------------------------------------------------------------------- /.github/workflows/release-cli.yml: -------------------------------------------------------------------------------- 1 | name: release-cli 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | tags: 8 | - "v*" 9 | 10 | jobs: 11 | release: 12 | permissions: 13 | contents: write 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout 17 | uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | persist-credentials: false 21 | - name: Set up Go 22 | uses: actions/setup-go@v5 23 | with: 24 | go-version: ${{vars.GO_VERSION}} 25 | cache: false 26 | - name: Run GoReleaser 27 | uses: goreleaser/goreleaser-action@9c156ee8a17a598857849441385a2041ef570552 # v6.3.0 28 | with: 29 | distribution: goreleaser 30 | version: "${{vars.GORELEASER_VERSION}}" 31 | args: release --clean 32 | env: 33 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 34 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: {} 4 | 5 | on: 6 | push: 7 | tags: ["v*.*.*"] 8 | 9 | jobs: 10 | release: 11 | name: Release 12 | uses: grafana/xk6/.github/workflows/extension-release.yml@v0.19.3 13 | permissions: 14 | contents: write 15 | with: 16 | go-version: ${{vars.GO_VERSION}} 17 | k6-version: ${{vars.K6_VERSION}} 18 | xk6-version: ${{vars.XK6_VERSION}} 19 | os: ${{vars.OS}} 20 | arch: ${{vars.ARCH}} 21 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: Validate 2 | 3 | permissions: {} 4 | 5 | on: 6 | workflow_dispatch: 7 | push: 8 | branches: ["main", "master"] 9 | pull_request: 10 | branches: ["main", "master"] 11 | 12 | jobs: 13 | validate: 14 | name: Validate 15 | uses: grafana/xk6/.github/workflows/extension-validate.yml@v0.19.3 16 | permissions: 17 | pages: write 18 | id-token: write 19 | with: 20 | go-version: ${{vars.GO_VERSION}} 21 | go-versions: ${{vars.GO_VERSIONS}} 22 | golangci-lint-version: ${{vars.GOLANGCI_LINT_VERSION}} 23 | platforms: ${{vars.PLATFORMS}} 24 | k6-versions: ${{vars.K6_VERSIONS}} 25 | xk6-version: ${{vars.XK6_VERSION}} 26 | -------------------------------------------------------------------------------- /.github/xk6-dashboard-social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/.github/xk6-dashboard-social.png -------------------------------------------------------------------------------- /.github/zizmor.yml: -------------------------------------------------------------------------------- 1 | rules: 2 | unpinned-uses: 3 | config: 4 | policies: 5 | "*": hash-pin 6 | actions/*: any 7 | grafana/*: any 8 | forbidden-uses: 9 | config: 10 | deny: 11 | # Policy-banned by our security team due to CVE-2025-30066 & CVE-2025-30154. 12 | # https://www.cisa.gov/news-events/alerts/2025/03/18/supply-chain-compromise-third-party-tj-actionschanged-files-cve-2025-30066-and-reviewdogaction 13 | # https://nvd.nist.gov/vuln/detail/cve-2025-30066 14 | # https://nvd.nist.gov/vuln/detail/cve-2025-30154 15 | - reviewdog/* 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /k6 2 | /k6.exe 3 | /k6-web-dashboard 4 | /k6-web-dashboard.exe 5 | /build 6 | /.hintrc 7 | /coverage.txt 8 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | version: "2" 2 | linters: 3 | default: all 4 | disable: 5 | # k6 extensions must be registered from the init() function. 6 | - gochecknoinits 7 | 8 | # The constructor of k6 extensions must return an interface. 9 | - ireturn 10 | 11 | # In many cases (e.g. options) it is normal usage not to specify all structure fields. 12 | - exhaustruct 13 | 14 | # Many go standard library API functions have typical parameter names shorter than 3 characters. 15 | # It is better to use the usual parameter names than to create one that conforms to the rule. 16 | - varnamelen 17 | 18 | # Except for general-purpose public APIs, 19 | # wrapping errors is more inconvenient and error prone than useful. 20 | - wrapcheck 21 | 22 | # xk6-dashboard is not a library and therefore cannot be tested via the public API. 23 | - testpackage 24 | settings: 25 | depguard: 26 | rules: 27 | prevent_accidental_imports: 28 | allow: 29 | - $gostd 30 | - github.com/stretchr/testify/require 31 | - github.com/stretchr/testify/assert 32 | - github.com/sirupsen/logrus 33 | - go.k6.io/k6 34 | - github.com/grafana/sobek 35 | - github.com/grafana/xk6-dashboard 36 | - github.com/r3labs/sse/v2 37 | - github.com/spf13/afero 38 | - github.com/pkg/browser 39 | - github.com/tidwall/gjson 40 | - github.com/spf13/cobra 41 | issues: 42 | max-issues-per-linter: 0 43 | max-same-issues: 0 44 | formatters: 45 | enable: 46 | - gci 47 | - gofmt 48 | - gofumpt 49 | - goimports 50 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | project_name: k6-web-dashboard 3 | before: 4 | hooks: 5 | - go mod tidy 6 | dist: build/dist 7 | builds: 8 | - env: 9 | - CGO_ENABLED=0 10 | goos: ["darwin", "linux", "windows"] 11 | goarch: ["amd64"] 12 | ldflags: 13 | - "-s -w -X {{.ModulePath}}/cmd.version={{.Version}} -X {{.ModulePath}}/cmd.appname={{.ProjectName}}" 14 | dir: cmd/k6-web-dashboard 15 | source: 16 | enabled: false 17 | 18 | archives: 19 | - id: bundle 20 | formats: ["tar.gz"] 21 | format_overrides: 22 | - goos: windows 23 | formats: ["zip"] 24 | 25 | checksum: 26 | name_template: "{{ .ProjectName }}_{{ .Version }}_checksums.txt" 27 | 28 | snapshot: 29 | version_template: "{{ incpatch .Version }}-next+{{.ShortCommit}}{{if .IsGitDirty}}.dirty{{else}}{{end}}" 30 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["ms-vscode-remote.remote-containers"] 3 | } 4 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @grafana/k6-extensions 2 | -------------------------------------------------------------------------------- /cmd/k6-web-dashboard/main.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | // Package main contains web-dashboard CLI tool. 6 | package main 7 | 8 | import ( 9 | "github.com/grafana/xk6-dashboard/cmd" 10 | "github.com/spf13/cobra" 11 | ) 12 | 13 | func main() { 14 | cmd := cmd.NewRootCommand() 15 | cobra.CheckErr(cmd.Execute()) 16 | } 17 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | // Package cmd contains CLI tool's root command definition. 6 | package cmd 7 | 8 | import ( 9 | "context" 10 | "strings" 11 | 12 | "github.com/grafana/xk6-dashboard/dashboard" 13 | "github.com/spf13/cobra" 14 | "go.k6.io/k6/cmd/state" 15 | ) 16 | 17 | //nolint:gochecknoglobals 18 | var ( 19 | appname = "k6-web-dashboard" // set by goreleaser 20 | version = "dev" // set by goreleaser 21 | ) 22 | 23 | // NewRootCommand build k6-web-dashboard command. 24 | func NewRootCommand() *cobra.Command { 25 | cmd := dashboard.NewCommand(state.NewGlobalState(context.TODO())) 26 | cmd.Use = strings.ReplaceAll(cmd.Use, dashboard.OutputName, appname) 27 | cmd.Short = strings.ReplaceAll(cmd.Short, dashboard.OutputName, appname) 28 | cmd.Long = strings.ReplaceAll(cmd.Long, "k6 "+dashboard.OutputName, appname) 29 | cmd.Version = version 30 | cmd.DisableAutoGenTag = true 31 | 32 | for _, c := range cmd.Commands() { 33 | c.Example = strings.ReplaceAll(c.Example, "k6 "+dashboard.OutputName, appname) 34 | } 35 | 36 | return cmd 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/assets.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | package dashboard 6 | 7 | import ( 8 | "embed" 9 | "encoding/json" 10 | "io/fs" 11 | 12 | "github.com/sirupsen/logrus" 13 | ) 14 | 15 | type assets struct { 16 | config json.RawMessage 17 | ui fs.FS 18 | report fs.FS 19 | } 20 | 21 | //go:embed assets/packages/ui/dist assets/packages/report/dist assets/packages/config/dist 22 | var assetsFS embed.FS 23 | 24 | const assetsPackages = "assets/packages/" 25 | 26 | func newAssets() *assets { 27 | return newAssetsFrom(assetsFS) 28 | } 29 | 30 | func newCustomizedAssets(proc *process) *assets { 31 | assets := newAssetsFrom(assetsFS) 32 | 33 | custom, err := customize(assets.config, proc) 34 | if err != nil { 35 | logrus.Fatal(err) 36 | } 37 | 38 | assets.config = custom 39 | 40 | return assets 41 | } 42 | 43 | func newAssetsFrom(efs embed.FS) *assets { 44 | config, err := efs.ReadFile(assetsPackages + "config/dist/config.json") 45 | if err != nil { 46 | panic(err) 47 | } 48 | 49 | return &assets{ 50 | ui: assetDir(assetsPackages+"ui/dist", efs), 51 | report: assetDir(assetsPackages+"report/dist", efs), 52 | config: config, 53 | } 54 | } 55 | 56 | func assetDir(dirname string, parent fs.FS) fs.FS { 57 | subfs, err := fs.Sub(parent, dirname) 58 | if err != nil { 59 | panic(err) 60 | } 61 | 62 | return subfs 63 | } 64 | -------------------------------------------------------------------------------- /dashboard/assets/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | # 4 | # SPDX-License-Identifier: AGPL-3.0-only 5 | # SPDX-License-Identifier: MIT 6 | 7 | node_modules/ 8 | package-lock.json 9 | 10 | logs 11 | *.log 12 | npm-debug.log* 13 | yarn-debug.log* 14 | yarn-error.log* 15 | .DS_Store -------------------------------------------------------------------------------- /dashboard/assets/lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "node_modules/lerna/schemas/lerna-schema.json", 3 | "npmClient": "yarn", 4 | "version": "independent" 5 | } 6 | -------------------------------------------------------------------------------- /dashboard/assets/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "xk6-dashboard-assets", 3 | "version": "1.0.0", 4 | "description": "xk6-dashboard embeddable assets", 5 | "license": "MIT", 6 | "private": true, 7 | "scripts": { 8 | "build": "lerna run build" 9 | }, 10 | "devDependencies": { 11 | "lerna": "^7.1.5" 12 | }, 13 | "workspaces": [ 14 | "packages/*" 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /dashboard/assets/packages/config/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "prettier"], 7 | "plugins": ["prettier"], 8 | "parserOptions": { 9 | "ecmaVersion": "latest", 10 | "sourceType": "module" 11 | }, 12 | "rules": { 13 | "prettier/prettier": "error" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dashboard/assets/packages/config/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | # Logs 6 | logs 7 | *.log 8 | *-debug.log* 9 | 10 | node_modules 11 | *.local 12 | coverage 13 | -------------------------------------------------------------------------------- /dashboard/assets/packages/config/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 128, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false, 9 | "bracketSameLine": true, 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/config/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/dashboard/assets/packages/config/README.md -------------------------------------------------------------------------------- /dashboard/assets/packages/config/main.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import fs from "fs" 6 | import path from "path" 7 | import config from "./src/config.js" 8 | 9 | import { ConfigBuilder } from "@xk6-dashboard/view/config" 10 | 11 | // eslint-disable-next-line no-undef 12 | const file = process.argv[2] 13 | 14 | const dir = path.dirname(file) 15 | 16 | if (!fs.existsSync(dir)) { 17 | fs.mkdirSync(dir) 18 | } 19 | 20 | fs.writeFileSync(file, JSON.stringify(ConfigBuilder.build(config), null, 2)) 21 | -------------------------------------------------------------------------------- /dashboard/assets/packages/config/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xk6-dashboard/config", 3 | "private": true, 4 | "version": "0.0.1", 5 | "types": "./src/config.d.ts", 6 | "files": [ 7 | "dist/config.json" 8 | ], 9 | "exports": { 10 | ".": "./dist/config.json" 11 | }, 12 | "scripts": { 13 | "build": "node main.js dist/config.json" 14 | }, 15 | "type": "module", 16 | "dependencies": { 17 | "@xk6-dashboard/view": "0.0.1" 18 | }, 19 | "devDependencies": { 20 | "eslint": "^8.0.1", 21 | "eslint-config-prettier": "^9.0.0", 22 | "eslint-config-standard": "^17.1.0", 23 | "eslint-plugin-import": "^2.25.2", 24 | "eslint-plugin-n": "^15.0.0 || ^16.0.0 ", 25 | "eslint-plugin-prettier": "^5.0.0", 26 | "eslint-plugin-promise": "^6.0.0", 27 | "prettier": "3.0.2" 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 8 | "plugins": ["@typescript-eslint", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "@typescript-eslint/no-unused-vars": "error", 16 | "@typescript-eslint/consistent-type-definitions": ["error", "type"] 17 | }, 18 | "ignorePatterns": ["dist/*"] 19 | } 20 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 128, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false, 9 | "bracketSameLine": true, 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xk6-dashboard/model", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup src/index.ts --dts-resolve --format esm", 8 | "dev": "tsup src/index.ts --watch --dts-resolve --format esm", 9 | "lint": "eslint --ext .js,.ts ." 10 | }, 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js" 15 | } 16 | }, 17 | "dependencies": { 18 | "@types/jmespath": "^0.15.0", 19 | "jmespath": "^0.16.0" 20 | }, 21 | "devDependencies": { 22 | "@typescript-eslint/eslint-plugin": "^6.7.2", 23 | "@typescript-eslint/parser": "^6.7.2", 24 | "eslint": "^8.49.0", 25 | "eslint-config-prettier": "^9.0.0", 26 | "eslint-plugin-prettier": "^5.0.0", 27 | "prettier": "^3.0.3", 28 | "tsup": "^7.2.0", 29 | "typescript": "^5.2.2" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/src/Event.ts: -------------------------------------------------------------------------------- 1 | export enum EventType { 2 | config = "config", 3 | param = "param", 4 | start = "start", 5 | stop = "stop", 6 | metric = "metric", 7 | snapshot = "snapshot", 8 | cumulative = "cumulative", 9 | threshold = "threshold" 10 | } 11 | 12 | export type ConfigEvent = { 13 | type: EventType.config 14 | data: Record 15 | } 16 | 17 | export type ParamEvent = { 18 | type: EventType.param 19 | data: Record 20 | } 21 | 22 | export type StartEvent = { 23 | type: EventType.start 24 | data: Array> 25 | } 26 | 27 | export type StopEvent = { 28 | type: EventType.stop 29 | data: Array> 30 | } 31 | 32 | export type MetricEvent = { 33 | type: EventType.metric 34 | data: Record> 35 | } 36 | 37 | export type SnapshotEvent = { 38 | type: EventType.snapshot 39 | data: Array> 40 | } 41 | 42 | export type CumulativeEvent = { 43 | type: EventType.cumulative 44 | data: Array> 45 | } 46 | 47 | export type ThresholdEvent = { 48 | type: EventType.threshold 49 | data: Record> 50 | } 51 | 52 | export type DashboardEvent = 53 | | ConfigEvent 54 | | ParamEvent 55 | | StartEvent 56 | | StopEvent 57 | | MetricEvent 58 | | SnapshotEvent 59 | | CumulativeEvent 60 | | ThresholdEvent 61 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/src/UnitType.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const enum UnitType { 6 | bytes = "bytes", 7 | bps = "bps", 8 | counter = "counter", 9 | rps = "rps", 10 | duration = "duration", 11 | timestamp = "timestamp", 12 | unknown = "" 13 | } 14 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { UnitType } from "./UnitType.ts" 6 | 7 | export { Metric, Metrics, MetricType, ValueType, Query, Aggregate, AggregateType } from "./Metrics.ts" 8 | export { EventType, DashboardEvent } from "./Event.ts" 9 | export { Config, Param, Digest } from "./Digest.ts" 10 | export { Samples, SamplesView, SampleVector, SampleVectorInit } from "./Samples.ts" 11 | export { Summary, SummaryView, SummaryRow } from "./Summary.ts" 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/model/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "allowImportingTsExtensions": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "emitDeclarationOnly": true, 9 | "declaration": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": ["eslint:recommended", "plugin:react/recommended", "plugin:@typescript-eslint/recommended", "prettier"], 7 | "ignorePatterns": ["dist", ".eslintrc.json"], 8 | "plugins": ["react", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module", 12 | "ecmaFeatures": { 13 | "jsx": true 14 | }, 15 | "jsx": true 16 | }, 17 | "rules": { 18 | "prettier/prettier": "error", 19 | "react/prop-types": 0 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | # 4 | # SPDX-License-Identifier: AGPL-3.0-only 5 | # SPDX-License-Identifier: MIT 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | pnpm-debug.log* 14 | lerna-debug.log* 15 | 16 | node_modules 17 | dist-ssr 18 | *.local 19 | 20 | # Editor directories and files 21 | .vscode/* 22 | !.vscode/extensions.json 23 | .idea 24 | .DS_Store 25 | *.suo 26 | *.ntvs* 27 | *.njsproj 28 | *.sln 29 | *.sw? 30 | 31 | bundle-stats.html -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.prettierignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | dist/ 6 | *.md -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 128, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false, 9 | "bracketSameLine": true, 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.testcontext.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | import { readFileSync } from "fs" 8 | import { gunzipSync, gzipSync } from "zlib" 9 | 10 | import config from "../config/dist/config.json" 11 | 12 | let testdata = "" 13 | 14 | if (process.env.NODE_ENV != "production") { 15 | let data = readFileSync(".testdata.ndjson.gz") 16 | let text = gunzipSync(Buffer.from(data, "base64")).toString("utf8") 17 | 18 | let conf = { event: "config", data: config } 19 | 20 | testdata = gzipSync(JSON.stringify(conf) + "\n" + text).toString("base64") 21 | } 22 | 23 | export default { testdata } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/.testdata.ndjson.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/dashboard/assets/packages/report/.testdata.ndjson.gz -------------------------------------------------------------------------------- /dashboard/assets/packages/report/dist/assets/circle-5a1f9f5e.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/dist/assets/logo-fd36a8d6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 15 | 16 | k6 report 17 | 18 | 19 | 20 |
21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xk6-dashboard/report", 3 | "private": true, 4 | "version": "1.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build --emptyOutDir", 9 | "preview": "vite preview", 10 | "lint:eslint": "eslint \"./src/**/*.{js,jsx,ts,tsx}\"", 11 | "lint:format": "prettier --ignore-unknown --check \"./**/*\"", 12 | "lint:types": "tsc --noEmit", 13 | "lint": "yarn lint:eslint && yarn lint:types && yarn lint:format" 14 | }, 15 | "dependencies": { 16 | "@vanilla-extract/css": "^1.13.0", 17 | "@vanilla-extract/dynamic": "^2.0.3", 18 | "@vanilla-extract/sprinkles": "^1.6.1", 19 | "@xk6-dashboard/config": "0.0.1", 20 | "@xk6-dashboard/model": "0.0.1", 21 | "@xk6-dashboard/view": "0.0.1", 22 | "preact": "^10.16.0", 23 | "react": "npm:@preact/compat", 24 | "react-dom": "npm:@preact/compat", 25 | "round-to": "^6.0.0", 26 | "uplot": "^1.6.24", 27 | "uplot-react": "^1.1.4" 28 | }, 29 | "devDependencies": { 30 | "@preact/preset-vite": "^2.5.0", 31 | "@typescript-eslint/eslint-plugin": "^6.7.5", 32 | "@typescript-eslint/parser": "^6.7.5", 33 | "@vanilla-extract/vite-plugin": "^3.9.0", 34 | "eslint": "^8.51.0", 35 | "eslint-config-prettier": "^9.0.0", 36 | "eslint-plugin-prettier": "^5.0.1", 37 | "rollup-plugin-visualizer": "^5.9.2", 38 | "sass": "^1.65.1", 39 | "vite": "^4.4.11", 40 | "vite-plugin-handlebars": "^1.6.0", 41 | "vite-plugin-singlefile": "^0.13.5", 42 | "vite-tsconfig-paths": "^4.2.1" 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/App/App.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | import { sizes } from "theme/sizes.css" 9 | 10 | export const main = style({ 11 | padding: vars.sizes.size5, 12 | "@media": { 13 | [`(min-width: ${sizes.lg})`]: { 14 | padding: vars.sizes.size11 15 | } 16 | } 17 | }) 18 | 19 | export const usage = style({ 20 | color: vars.colors.text.secondary, 21 | fontStyle: "italic" 22 | }) 23 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/App/App.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { Digest } from "@xk6-dashboard/model" 7 | 8 | import "theme/global.css" 9 | import { theme } from "theme" 10 | import { toClassName } from "utils" 11 | import { Flex } from "components/Flex" 12 | import { Header } from "components/Header" 13 | import { Tab } from "components/Tab/Tab" 14 | 15 | import * as styles from "./App.css" 16 | 17 | interface AppProps { 18 | digest: Digest 19 | } 20 | 21 | export default function App({ digest }: AppProps) { 22 | return ( 23 | 24 |
25 | 26 | {digest.config.tabs.map((tab) => ( 27 | 28 | ))} 29 | 30 |
31 |
32 |

33 | Select a time interval by holding down the mouse on any graph to zoom. To cancel zoom, double click on any graph. 34 |

35 |
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/App/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./App" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/assets/icons/circle.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Chart/Chart.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const chartWrapper = style({ 10 | position: "relative" 11 | }) 12 | 13 | export const noData = style({ 14 | position: "absolute", 15 | top: "50%", 16 | left: "50%", 17 | transform: "translate(-50%, -50%)", 18 | fontSize: vars.fontSizes.size5, 19 | fontWeight: vars.fontWeights.weight500, 20 | padding: `${vars.sizes.size2} ${vars.sizes.size8}`, 21 | border: `1px dashed ${vars.colors.primary.dark}` 22 | }) 23 | 24 | export const uplot = style({ 25 | breakInside: "avoid" 26 | }) 27 | 28 | export const title = style({ 29 | color: vars.colors.text.secondary, 30 | fontWeight: vars.fontWeights.weight500 31 | }) 32 | 33 | export const chart = style({ 34 | marginTop: vars.sizes.size1, 35 | marginBottom: vars.sizes.size1 36 | }) 37 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Chart/Chart.hooks.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { useLayoutEffect, useState } from "preact/hooks" 6 | 7 | type Width = number 8 | 9 | export const useElementWidth = (): [(node: T | null) => void, Width] => { 10 | const [ref, setRef] = useState(null) 11 | const [width, setWidth] = useState(0) 12 | 13 | useLayoutEffect(() => { 14 | const updateWidth = () => { 15 | if (ref) { 16 | setWidth(ref.offsetWidth) 17 | } 18 | } 19 | 20 | updateWidth() 21 | window.addEventListener("resize", updateWidth) 22 | 23 | return () => window.removeEventListener("resize", updateWidth) 24 | }) 25 | 26 | return [setRef, width] 27 | } 28 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Chart/Chart.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | 7 | import "uplot/dist/uPlot.min.css" 8 | import UplotReact from "uplot-react" 9 | import { AlignedData } from "uplot" 10 | 11 | import { Digest } from "@xk6-dashboard/model" 12 | import { Panel, SeriesPlot } from "@xk6-dashboard/view" 13 | 14 | import { colors } from "utils" 15 | 16 | import { createOptions } from "./Chart.utils" 17 | import { useElementWidth } from "./Chart.hooks" 18 | import * as styles from "./Chart.css" 19 | 20 | interface ChartProps { 21 | panel: Panel 22 | digest: Digest 23 | } 24 | 25 | export default function Chart({ panel, digest }: ChartProps) { 26 | const [ref, width] = useElementWidth() 27 | 28 | const plot = new SeriesPlot(digest, panel, colors) 29 | const hasData = !plot.empty && plot.data[0].length > 1 30 | const plotData = (hasData ? plot.data : []) as AlignedData 31 | const options = createOptions({ plot, width }) 32 | 33 | return ( 34 |
35 |

{panel.title}

36 |
37 | {!hasData &&

no data

} 38 | 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./Chart" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Flex/Flex.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, createThemeContract } from "@vanilla-extract/css" 6 | import { createSprinkles, defineProperties } from "@vanilla-extract/sprinkles" 7 | 8 | import { vars } from "theme" 9 | 10 | const variantProps = defineProperties({ 11 | properties: { 12 | flexDirection: ["row", "column"], 13 | flexWrap: ["nowrap", "wrap", "wrap-reverse"], 14 | alignItems: ["flex-start", "flex-end", "stretch", "center", "baseline", "start", "end", "self-start", "self-end"], 15 | justifyContent: [ 16 | "flex-start", 17 | "flex-end", 18 | "start", 19 | "end", 20 | "left", 21 | "right", 22 | "center", 23 | "space-between", 24 | "space-around", 25 | "space-evenly" 26 | ], 27 | gap: { 28 | 0: 0, 29 | 1: vars.sizes.size1, 30 | 2: vars.sizes.size3, 31 | 3: vars.sizes.size4, 32 | 4: vars.sizes.size9, 33 | 5: vars.sizes.size11 34 | }, 35 | padding: { 36 | 1: vars.sizes.size3, 37 | 2: vars.sizes.size5, 38 | 3: vars.sizes.size7, 39 | 4: vars.sizes.size10 40 | } 41 | } 42 | }) 43 | 44 | export const theme = createThemeContract({ 45 | flexGrow: null, 46 | flexShrink: null, 47 | flexBasis: null, 48 | height: null, 49 | width: null 50 | }) 51 | 52 | export const root = style({ 53 | display: "flex", 54 | ...theme 55 | }) 56 | 57 | export const variants = createSprinkles(variantProps) 58 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type ReactNode, type Ref, type ElementType, type HTMLAttributes } from "react" 6 | 7 | import { toClassName, toStyle } from "utils" 8 | 9 | import type { FlexElementProps } from "./Flex.types" 10 | import { root, theme, variants } from "./Flex.css" 11 | 12 | export interface FlexProps extends HTMLAttributes, FlexElementProps { 13 | children: ReactNode 14 | className?: string 15 | as?: ElementType 16 | } 17 | 18 | function FlexBase( 19 | { 20 | as: Tag = "div", 21 | align, 22 | basis, 23 | children, 24 | className, 25 | direction, 26 | gap = 3, 27 | grow, 28 | height, 29 | justify, 30 | padding, 31 | shrink, 32 | width, 33 | wrap, 34 | ...props 35 | }: FlexProps, 36 | ref: Ref 37 | ) { 38 | const variantClassName = variants({ 39 | alignItems: align, 40 | flexDirection: direction, 41 | flexWrap: wrap, 42 | gap, 43 | justifyContent: justify, 44 | padding 45 | }) 46 | 47 | const classNames = toClassName(root, variantClassName, className) 48 | const styles = toStyle(theme, { flexBasis: basis, flexGrow: grow, flexShrink: shrink, height, width }) 49 | 50 | return ( 51 | 52 | {children} 53 | 54 | ) 55 | } 56 | 57 | export const Flex = forwardRef(FlexBase) 58 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Flex/Flex.types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | type AlignItems = "stretch" | "flex-start" | "flex-end" | "center" | "baseline" | "start" | "end" | "self-start" | "self-end" 6 | 7 | type FlexBasis = "auto" | string 8 | 9 | type FlexDirection = "column" | "row" 10 | 11 | type FlexWrap = "nowrap" | "wrap" | "wrap-reverse" 12 | 13 | type JustifyContent = 14 | | "flex-start" 15 | | "flex-end" 16 | | "center" 17 | | "space-between" 18 | | "space-around" 19 | | "space-evenly" 20 | | "start" 21 | | "end" 22 | | "left" 23 | | "right" 24 | 25 | export interface FlexElementProps { 26 | align?: AlignItems 27 | direction?: FlexDirection 28 | gap?: 0 | 1 | 2 | 3 | 4 29 | justify?: JustifyContent 30 | wrap?: FlexWrap 31 | basis?: FlexBasis 32 | grow?: number 33 | shrink?: number 34 | padding?: 1 | 2 | 3 | 4 35 | height?: string | number 36 | width?: string | number 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Flex/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Flex" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Grid/Grid.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles" 7 | 8 | import { vars } from "theme" 9 | import { sizes } from "theme/sizes.css" 10 | 11 | const columnArray = Array.from({ length: 12 }, (_, i) => i + 1) 12 | 13 | export const containerBase = style({ 14 | display: "grid", 15 | gridTemplateRows: "auto", 16 | gridTemplateColumns: "repeat(12, 1fr)" 17 | }) 18 | 19 | const containerProperties = defineProperties({ 20 | properties: { 21 | gap: { 22 | 1: vars.sizes.size1, 23 | 2: `clamp(${vars.sizes.size1}, 4vw, ${vars.sizes.size2})`, 24 | 3: `clamp(${vars.sizes.size1}, 4vw, ${vars.sizes.size6})`, 25 | 4: `clamp(${vars.sizes.size1}, 4vw, ${vars.sizes.size11})` 26 | } 27 | } 28 | }) 29 | 30 | const columnProperties = defineProperties({ 31 | conditions: { 32 | xs: { "@media": `(min-width: ${sizes.xs})` }, 33 | sm: { "@media": `(min-width: ${sizes.sm})` }, 34 | md: { "@media": `(min-width: ${sizes.md})` }, 35 | lg: { "@media": `(min-width: ${sizes.lg})` }, 36 | xl: { "@media": `(min-width: ${sizes.xl})` }, 37 | xxl: { "@media": `(min-width: ${sizes.xxl})` } 38 | }, 39 | defaultCondition: "xs", 40 | properties: { 41 | gridColumn: columnArray.reduce( 42 | (obj, i) => ({ 43 | ...obj, 44 | [i]: `span ${i}` 45 | }), 46 | {} 47 | ) 48 | } 49 | }) 50 | 51 | export const column = createSprinkles(columnProperties) 52 | 53 | export const container = { 54 | root: containerBase, 55 | variants: createSprinkles(containerProperties) 56 | } 57 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type ElementType, type ReactNode, type Ref, type HTMLAttributes } from "react" 6 | 7 | import type { GridElementProps, GridColumnElementProps } from "./Grid.types" 8 | import { column, container } from "./Grid.css" 9 | import { toClassName } from "utils" 10 | 11 | export interface GridProps extends HTMLAttributes, GridElementProps { 12 | children: ReactNode 13 | className?: string 14 | as?: ElementType 15 | } 16 | 17 | export interface GridColumnProps extends HTMLAttributes, GridColumnElementProps { 18 | children: ReactNode 19 | className?: string 20 | as?: ElementType 21 | } 22 | 23 | function GridBase({ as: Tag = "div", gap = 3, children, className, ...props }: GridProps, ref: Ref) { 24 | return ( 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | 31 | function Column( 32 | { children, as: Tag = "div", className, xs = 12, sm, md, lg, xl, xxl, ...props }: GridColumnProps, 33 | ref: Ref 34 | ) { 35 | return ( 36 | 52 | {children} 53 | 54 | ) 55 | } 56 | 57 | export const Grid = Object.assign(forwardRef(GridBase), { 58 | Column: forwardRef(Column) 59 | }) 60 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Grid/Grid.types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export interface GridElementProps { 6 | columns?: number 7 | gap?: 1 | 2 | 3 | 4 8 | height?: string | number 9 | width?: string | number 10 | } 11 | 12 | export interface GridStyleProps extends Omit { 13 | $height?: string | number 14 | $width?: string | number 15 | } 16 | 17 | export interface GridColumnElementProps { 18 | xs?: number 19 | sm?: number 20 | md?: number 21 | lg?: number 22 | xl?: number 23 | xxl?: number 24 | } 25 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Grid/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Grid" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Header/Header.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const heading = style({ 9 | fontSize: vars.fontSizes.sizeFluid 10 | }) 11 | 12 | export const date = style({ 13 | color: vars.colors.text.secondary 14 | }) 15 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Header/Header.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { Digest } from "@xk6-dashboard/model" 7 | 8 | import "theme/global.css" 9 | import { Flex } from "components/Flex" 10 | import { ReactComponent as LogoIcon } from "assets/icons/logo.svg" 11 | 12 | import { toDate } from "./Header.utils" 13 | import * as styles from "./Header.css" 14 | 15 | interface HeaderProps { 16 | digest: Digest 17 | } 18 | 19 | export function Header({ digest }: HeaderProps) { 20 | return ( 21 | 22 | 23 | 24 | Report: {toDate(digest.start)} 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Header/Header.utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | const dateTimeFormatter = new Intl.DateTimeFormat("en-US", { 6 | year: "numeric", 7 | month: "2-digit", 8 | day: "2-digit", 9 | hour: "2-digit", 10 | minute: "2-digit", 11 | second: "2-digit", 12 | hour12: false 13 | }) 14 | 15 | export const toDate = (date?: Date) => { 16 | if (!date) { 17 | return 18 | } 19 | 20 | const parts = dateTimeFormatter.formatToParts(new Date(date)) 21 | 22 | const year = parts.find((p) => p.type === "year")?.value 23 | const month = parts.find((p) => p.type === "month")?.value 24 | const day = parts.find((p) => p.type === "day")?.value 25 | const hour = parts.find((p) => p.type === "hour")?.value 26 | const minute = parts.find((p) => p.type === "minute")?.value 27 | const second = parts.find((p) => p.type === "second")?.value 28 | 29 | return `${year}-${month}-${day} ${hour}:${minute}:${second}` 30 | } 31 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Header" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Panel.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { Digest } from "@xk6-dashboard/model" 7 | import { Panel as PanelClass, PanelKind } from "@xk6-dashboard/view" 8 | 9 | import Chart from "components/Chart" 10 | import Summary from "components/Summary" 11 | 12 | interface PanelProps { 13 | panel: PanelClass 14 | digest: Digest 15 | } 16 | 17 | export function Panel({ panel, digest }: PanelProps) { 18 | if (panel.kind == PanelKind.chart) { 19 | return 20 | } 21 | 22 | if (panel.kind == PanelKind.summary) { 23 | return 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Section/Section.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | import { sizes } from "theme/sizes.css" 9 | 10 | export const header = style({ 11 | marginLeft: "-4px", 12 | marginBottom: vars.sizes.size8 13 | }) 14 | 15 | export const icon = style({ 16 | position: "relative", 17 | top: "-1px" 18 | }) 19 | 20 | export const panel = style({ 21 | "@media": { 22 | [`(min-width: ${sizes.lg})`]: { 23 | padding: vars.sizes.size8 24 | } 25 | } 26 | }) 27 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Section/Section.utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Digest } from "@xk6-dashboard/model" 6 | import { Panel as PanelClass, SummaryTable } from "@xk6-dashboard/view" 7 | 8 | export const getColumnSizes = (panel: PanelClass, digest: Digest) => { 9 | if (panel.kind == "chart") { 10 | return { xs: 12, lg: panel.fullWidth ? 12 : 6 } 11 | } 12 | 13 | if (panel.kind == "summary") { 14 | const table = new SummaryTable(panel, digest) 15 | const num = table.view.aggregates.length 16 | const lg = num > 6 ? 12 : num > 1 ? 6 : 3 17 | const md = num > 6 ? 12 : num > 1 ? 12 : 6 18 | 19 | return { xs: 12, md, lg } 20 | } 21 | 22 | return {} 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Section/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Section" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Summary/Summary.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const container = style({ 10 | overflowX: "auto" 11 | }) 12 | 13 | export const caption = style({ 14 | textAlign: "center", 15 | fontWeight: vars.fontWeights.weight500, 16 | fontSize: vars.fontSizes.size6, 17 | color: vars.colors.text.primary, 18 | padding: vars.sizes.size3 19 | }) 20 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Summary/Summary.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { Digest } from "@xk6-dashboard/model" 7 | import { Panel, SummaryTable } from "@xk6-dashboard/view" 8 | 9 | import { Table } from "components/Table" 10 | 11 | import * as styles from "./Summary.css" 12 | 13 | interface SummaryProps { 14 | panel: Panel 15 | digest: Digest 16 | } 17 | 18 | export default function Summary({ panel, digest }: SummaryProps) { 19 | const table = new SummaryTable(panel, digest) 20 | 21 | if (table.empty) { 22 | return null 23 | } 24 | 25 | return ( 26 |
27 | 28 | 29 | 30 | 31 | {table.header.map((name, idx) => ( 32 | 33 | {name} 34 | 35 | ))} 36 | 37 | 38 | 39 | {table.body.map((row, idx) => ( 40 | 41 | {row.map((cell, cidx) => ( 42 | 43 | {cell} 44 | 45 | ))} 46 | 47 | ))} 48 | 49 |
{panel.title}
50 |
51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Summary/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./Summary" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Tab/Tab.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const header = style({ 10 | borderLeft: `6px solid ${vars.colors.primary.dark}`, 11 | paddingLeft: vars.sizes.size5 12 | }) 13 | 14 | export const title = style({ 15 | lineHeight: vars.lineHeights.size1, 16 | marginBottom: vars.sizes.size5 17 | }) 18 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Tab/Tab.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { Digest } from "@xk6-dashboard/model" 7 | 8 | import { Tab as TabType } from "types/config" 9 | import { Flex } from "components/Flex" 10 | import { Section } from "components/Section" 11 | 12 | import * as styles from "./Tab.css" 13 | 14 | interface TabProps { 15 | tab: TabType 16 | digest: Digest 17 | } 18 | 19 | export function Tab({ tab, digest }: TabProps) { 20 | return ( 21 | 22 |
23 |

{tab.title}

24 |

{tab.summary}

25 |
26 | 27 | {tab.sections.map((section) => ( 28 |
29 | ))} 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Tab/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Tab" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Table/Table.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, styleVariants } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const table = style({ 10 | borderCollapse: "collapse", 11 | width: "100%" 12 | }) 13 | 14 | export const th = style({ 15 | padding: vars.sizes.size3, 16 | fontSize: vars.fontSizes.size5, 17 | fontWeight: vars.fontWeights.weight600 18 | }) 19 | 20 | export const tr = styleVariants({ 21 | thead: [], 22 | tbody: [ 23 | { 24 | ":hover": { 25 | backgroundColor: vars.colors.primary.main 26 | } 27 | }, 28 | { 29 | selectors: { 30 | "&:nth-child(odd):not(:hover)": { 31 | backgroundColor: vars.colors.primary.light 32 | }, 33 | "&:nth-child(even):not(:hover)": { 34 | backgroundColor: vars.colors.common.white 35 | } 36 | } 37 | } 38 | ] 39 | }) 40 | 41 | export const td = style({ 42 | padding: vars.sizes.size3, 43 | fontSize: vars.fontSizes.size4 44 | }) 45 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Table" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/main.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | import React from "react" 8 | import { render } from "preact" 9 | import "uplot/dist/uPlot.min.css" 10 | 11 | import digest from "utils/digest" 12 | import App from "App" 13 | 14 | const rootElement = document.getElementById("root") as HTMLDivElement 15 | digest().then((d) => render(, rootElement)) 16 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/theme/global.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { globalStyle } from "@vanilla-extract/css" 6 | 7 | import { fontSizes, fonts, letterSpacings, lineHeights } from "./typography.css" 8 | import { grey } from "theme/colors.css" 9 | 10 | globalStyle("*, *::before, *::after", { 11 | boxSizing: "border-box", 12 | margin: 0, 13 | padding: 0 14 | }) 15 | 16 | globalStyle("*", { 17 | fontFamily: fonts.sans 18 | }) 19 | 20 | globalStyle("html", { 21 | fontSize: "62.5%", 22 | MozOsxFontSmoothing: "grayscale", 23 | WebkitFontSmoothing: "antialiased", 24 | WebkitTextSizeAdjust: "100%", 25 | textSizeAdjust: "100%" 26 | }) 27 | 28 | globalStyle("body", { 29 | color: grey["700"], 30 | letterSpacing: letterSpacings.size3, 31 | lineHeight: lineHeights.size3, 32 | textRendering: "optimizeLegibility" 33 | }) 34 | 35 | globalStyle("img, picture, video, canvas, svg", { 36 | display: "block", 37 | maxWidth: "100%" 38 | }) 39 | 40 | globalStyle("input, button, textarea, select", { 41 | font: "inherit" 42 | }) 43 | 44 | globalStyle("p, h1, h2, h3, h4, h5, h6", { 45 | overflowWrap: "break-word" 46 | }) 47 | 48 | globalStyle("h1, h2, h3, h4, h5, h6", { 49 | color: grey["800"], 50 | overflowWrap: "break-word" 51 | }) 52 | 53 | globalStyle("h1", { 54 | fontSize: fontSizes.size10 55 | }) 56 | 57 | globalStyle("h2", { 58 | fontSize: fontSizes.size9 59 | }) 60 | 61 | globalStyle("h3", { 62 | fontSize: fontSizes.size7 63 | }) 64 | 65 | globalStyle("h4", { 66 | fontSize: fontSizes.size6 67 | }) 68 | 69 | globalStyle("p", { 70 | fontSize: fontSizes.size5 71 | }) 72 | 73 | globalStyle("#root", { 74 | isolation: "isolate" 75 | }) 76 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./theme.css" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/theme/sizes.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const sizes = { 6 | size1: ".25rem", 7 | size2: ".5rem", 8 | size3: ".75rem", 9 | size4: "1rem", 10 | size5: "1.25rem", 11 | size6: "1.5rem", 12 | size7: "1.75rem", 13 | size8: "2rem", 14 | size9: "3rem", 15 | size10: "4rem", 16 | size11: "5rem", 17 | xs: "0px", 18 | sm: "480px", 19 | md: "768px", 20 | lg: "1024px", 21 | xl: "1440px", 22 | xxl: "1920px" 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/theme/theme.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { createGlobalTheme, createTheme, createThemeContract } from "@vanilla-extract/css" 6 | 7 | import { common, grey } from "./colors.css" 8 | import { sizes } from "./sizes.css" 9 | import * as typography from "./typography.css" 10 | 11 | const root = createGlobalTheme(":root", { 12 | sizes, 13 | ...typography 14 | }) 15 | 16 | const colorsTheme = createThemeContract({ 17 | common, 18 | primary: { 19 | light: null, 20 | main: null, 21 | dark: null 22 | }, 23 | text: { 24 | primary: null, 25 | secondary: null, 26 | disabled: null, 27 | hover: null 28 | } 29 | }) 30 | 31 | export const theme = createTheme(colorsTheme, { 32 | common, 33 | primary: { 34 | light: grey[100], 35 | main: grey[200], 36 | dark: grey[300] 37 | }, 38 | text: { 39 | primary: grey[900], 40 | secondary: grey[700], 41 | disabled: grey[400], 42 | hover: grey[500] 43 | } 44 | }) 45 | 46 | export const vars = { ...root, colors: colorsTheme } 47 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/theme/typography.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const fonts = { 6 | sans: "system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif", 7 | serif: "ui-serif,serif", 8 | mono: "Dank Mono,Operator Mono,Inconsolata,Fira Mono,ui-monospace,SF Mono,Monaco,Droid Sans Mono,Source Code Pro,monospace" 9 | } 10 | 11 | export const fontSizes = { 12 | size0: ".5rem", 13 | size1: ".75rem", 14 | size2: "1rem", 15 | size3: "1.1rem", 16 | size4: "1.25rem", 17 | size5: "1.5rem", 18 | size6: "2rem", 19 | size7: "2.5rem", 20 | size8: "3rem", 21 | size9: "3.5rem", 22 | size10: "4rem", 23 | sizeFluid: "clamp(2rem, 4vw, 4rem)" 24 | } 25 | 26 | export const fontWeights = { 27 | weight100: "100", 28 | weight200: "200", 29 | weight300: "300", 30 | weight400: "400", 31 | weight500: "500", 32 | weight600: "600", 33 | weight700: "700", 34 | weight800: "800", 35 | weight900: "900" 36 | } 37 | 38 | export const lineHeights = { 39 | size0: "0.95", 40 | size1: "1.1", 41 | size2: "1.25", 42 | size3: "1.375", 43 | size4: "1.5", 44 | size5: "1.75", 45 | size6: "2" 46 | } 47 | 48 | export const letterSpacings = { 49 | size0: "-.05em", 50 | size1: ".025em", 51 | size2: ".050em", 52 | size3: ".075em", 53 | size4: ".150em", 54 | size5: ".500em", 55 | size6: ".750em", 56 | size7: "1em" 57 | } 58 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/types/config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Section } from "@xk6-dashboard/view" 6 | 7 | export interface Tab { 8 | title?: string 9 | id?: string 10 | summary?: string 11 | report?: boolean 12 | sections: Section[] 13 | } 14 | 15 | export interface UIConfig { 16 | title: string 17 | tabs: Tab[] 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/typings/assets.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | declare module "*.md" 6 | declare module "*.svg" 7 | declare module "*.jpg" 8 | declare module "*.jpeg" 9 | declare module "*.png" 10 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/typings/xk6-dashboard-model.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import "@xk6-dashboard/model" 6 | 7 | import { UIConfig } from "types/config" 8 | 9 | declare module "@xk6-dashboard/model" { 10 | interface Config extends UIConfig {} 11 | 12 | interface Digest { 13 | config?: UIConfig 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/utils/colors.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { 6 | red, 7 | pink, 8 | purple, 9 | deepPurple, 10 | indigo, 11 | blue, 12 | lightBlue, 13 | cyan, 14 | teal, 15 | green, 16 | lightGreen, 17 | lime, 18 | yellow, 19 | amber, 20 | orange, 21 | deepOrange, 22 | brown, 23 | grey, 24 | blueGrey 25 | } from "theme/colors.css" 26 | 27 | const palette = { 28 | red, 29 | pink, 30 | purple, 31 | deepPurple, 32 | indigo, 33 | blue, 34 | lightBlue, 35 | cyan, 36 | teal, 37 | green, 38 | lightGreen, 39 | lime, 40 | yellow, 41 | amber, 42 | orange, 43 | deepOrange, 44 | brown, 45 | grey, 46 | blueGrey 47 | } as const 48 | 49 | const order = [ 50 | "grey", 51 | "teal", 52 | "blue", 53 | "purple", 54 | "indigo", 55 | "orange", 56 | "pink", 57 | "green", 58 | "cyan", 59 | "amber", 60 | "lime", 61 | "brown", 62 | "lightGreen", 63 | "red", 64 | "deepPurple", 65 | "lightBlue", 66 | "yellow", 67 | "deepOrange", 68 | "blueGrey" 69 | ] as const 70 | 71 | export const colors = order.map((name) => { 72 | return { 73 | stroke: palette[name][800], 74 | fill: palette[name][600] + "20" 75 | } 76 | }) 77 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./colors" 6 | export * from "./digest" 7 | export * from "./object" 8 | export * from "./theme" 9 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const omitUndefined = (styles: T) => { 6 | return Object.entries(styles).reduce( 7 | (props, [prop, value]) => (value === undefined ? props : { ...props, [prop]: value }), 8 | {} 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { assignInlineVars } from "@vanilla-extract/dynamic" 6 | 7 | import { omitUndefined } from "./object" 8 | 9 | type CSSVarFunction = `var(--${string})` | `var(--${string}, ${string | number})` 10 | type Contract = { 11 | [key: string]: CSSVarFunction | null | Contract 12 | } 13 | 14 | export const toClassName = (...xs: unknown[]) => xs.filter(Boolean).join(" ") 15 | 16 | export const toStyle = (theme: Contract, styles: Record) => { 17 | return assignInlineVars(theme, omitUndefined(styles)) 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "baseUrl": "src", 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/assets/packages/report/vite.config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | import { defineConfig } from "vite" 8 | import { viteSingleFile } from "vite-plugin-singlefile" 9 | import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin" 10 | import svgr from "vite-plugin-svgr" 11 | import preact from "@preact/preset-vite" 12 | import handlebars from "vite-plugin-handlebars" 13 | import tsconfigPaths from "vite-tsconfig-paths" 14 | import { visualizer } from "rollup-plugin-visualizer" 15 | 16 | import testcontext from "./.testcontext" 17 | 18 | export default defineConfig({ 19 | plugins: [ 20 | preact(), 21 | svgr(), 22 | viteSingleFile(), 23 | tsconfigPaths(), 24 | vanillaExtractPlugin(), 25 | handlebars({ context: testcontext }), 26 | visualizer({ 27 | filename: "bundle-stats.html" 28 | }) 29 | ] 30 | }) 31 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "eslint:recommended", 8 | "plugin:react/recommended", 9 | "plugin:@typescript-eslint/recommended", 10 | "plugin:react-hooks/recommended", 11 | "prettier" 12 | ], 13 | "ignorePatterns": ["dist", ".eslintrc.json"], 14 | "plugins": ["react-refresh", "prettier"], 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module", 18 | "ecmaFeatures": { 19 | "jsx": true 20 | }, 21 | "jsx": true 22 | }, 23 | "rules": { 24 | "prettier/prettier": "error", 25 | "react/prop-types": 0, 26 | "react-refresh/only-export-components": ["warn", { "allowConstantExport": true }] 27 | }, 28 | "settings": { 29 | "react": { 30 | "version": "detect" // React version. "detect" automatically picks the version you have installed. 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | # 4 | # SPDX-License-Identifier: AGPL-3.0-only 5 | # SPDX-License-Identifier: MIT 6 | 7 | # Logs 8 | logs 9 | *.log 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | node_modules 15 | *.local 16 | 17 | # Editor directories and files 18 | .vscode/* 19 | !.vscode/extensions.json 20 | !.vscode/settings.json 21 | .idea 22 | .DS_Store 23 | *.suo 24 | *.ntvs* 25 | *.njsproj 26 | *.sln 27 | *.sw? 28 | 29 | bundle-stats.html -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/.prettierignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | # 3 | # SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | dist/ 6 | *.md -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 128, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false, 9 | "bracketSameLine": true, 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/README.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | # xk6-dashboard UI 8 | 9 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/dark_mode-530eef3b.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/expand_less-2d2317d9.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/expand_more-c6b4db32.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/hour_glass-20497ed9.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/info-54caaa0b.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/light_mode-5b543e8c.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/logo-fd36a8d6.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/options-ee7c6312.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/rewind_time-def68db1.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/spinner-8a2a69f5.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/assets/stop_watch-624e074a.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | k6 dashboard 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/dist/xk6-dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | k6 dashboard 14 | 15 | 16 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/public/xk6-dashboard.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/App/App.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const container = style({ 9 | backgroundColor: vars.colors.secondary.dark, 10 | color: vars.colors.text.primary, 11 | minHeight: "100vh" 12 | }) 13 | 14 | export const main = style({ 15 | padding: vars.sizes.size6 16 | }) 17 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/App/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./App" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/dark_mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/expand_less.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/expand_more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/hour_glass.svg: -------------------------------------------------------------------------------- 1 | 5 | 6 | 9 | 19 | 20 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/info.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/light_mode.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/options.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/rewind_time.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 5 | 7 | 8 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/assets/icons/stop_watch.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Button/Button.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, styleVariants } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | const base = style({ 10 | backgroundColor: "transparent", 11 | border: "none", 12 | color: vars.colors.text.primary, 13 | fontSize: vars.fontSizes.size4, 14 | fontWeight: vars.fontWeights.weight500, 15 | 16 | selectors: { 17 | "&:is(:disabled)": { 18 | opacity: 0.5, 19 | cursor: "not-allowed" 20 | }, 21 | "&:not(:disabled)": { 22 | cursor: "pointer" 23 | } 24 | } 25 | }) 26 | 27 | const baseButton = style([ 28 | base, 29 | { 30 | borderRadius: vars.borderRadius.sm, 31 | fontWeight: vars.fontWeights.weight600, 32 | letterSpacing: vars.letterSpacings.size4, 33 | padding: `${vars.sizes.size3} ${vars.sizes.size8}`, 34 | textTransform: "uppercase" 35 | } 36 | ]) 37 | 38 | export const variant = styleVariants({ 39 | fill: [ 40 | baseButton, 41 | { 42 | backgroundColor: vars.colors.primary.main, 43 | color: vars.colors.common.white, 44 | selectors: { 45 | "&:hover:not(:active)": { 46 | backgroundColor: vars.colors.primary.light 47 | }, 48 | "&:active": { 49 | backgroundColor: vars.colors.primary.dark 50 | } 51 | } 52 | } 53 | ], 54 | outline: [ 55 | baseButton, 56 | { 57 | outline: `2px solid ${vars.colors.components.button.outline.border}`, 58 | outlineOffset: "-2px", 59 | color: vars.colors.components.button.outline.text, 60 | selectors: { 61 | "&:hover": { 62 | backgroundColor: vars.colors.components.button.outline.background 63 | } 64 | } 65 | } 66 | ], 67 | text: [base] 68 | }) 69 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type MouseEvent, type ReactNode, type Ref } from "react" 6 | 7 | import { toClassName } from "utils" 8 | 9 | import * as styles from "./Button.css" 10 | 11 | interface CommonProps { 12 | children: ReactNode 13 | className?: string 14 | variant?: "fill" | "outline" | "text" 15 | onClick?: (event: MouseEvent) => void 16 | } 17 | 18 | export interface ButtonProps extends CommonProps { 19 | as?: "button" 20 | disabled?: boolean 21 | href?: never 22 | type?: "button" | "submit" | "reset" 23 | } 24 | 25 | interface AnchorProps extends CommonProps { 26 | as: "a" 27 | href: string 28 | target?: "_blank" | "_self" | "_parent" | "_top" 29 | type?: never 30 | } 31 | 32 | type Props = ButtonProps | AnchorProps 33 | type Element = HTMLButtonElement & HTMLAnchorElement 34 | 35 | const ButtonBase = ({ as: Tag = "button", children, className, variant = "fill", ...props }: Props, ref: Ref) => { 36 | return ( 37 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | export const Button = forwardRef(ButtonBase) 44 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Button/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Button" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Card/Card.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const container = style({ 9 | padding: 0 10 | }) 11 | 12 | export const title = style({ 13 | backgroundColor: vars.colors.secondary.light, 14 | borderRadius: `${vars.borderRadius.md} ${vars.borderRadius.md} 0 0`, 15 | color: vars.colors.text.secondary, 16 | padding: `${vars.sizes.size6} ${vars.sizes.size8}`, 17 | fontSize: vars.fontSizes.size5, 18 | fontWeight: vars.fontWeights.weight500 19 | }) 20 | 21 | export const content = style({ 22 | padding: vars.sizes.size5 23 | }) 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Card/Card.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type CSSProperties, type Ref, type ReactNode } from "react" 6 | 7 | import { toClassName } from "utils" 8 | import { Paper } from "components/Paper" 9 | 10 | import * as styles from "./Card.css" 11 | 12 | interface CardProps extends React.HTMLAttributes { 13 | children: ReactNode 14 | className?: string 15 | style?: CSSProperties 16 | title?: string 17 | } 18 | 19 | function CardBase({ children, className, title, ...props }: CardProps, ref: Ref) { 20 | return ( 21 | 22 | {title &&

{title}

} 23 |
{children}
24 |
25 | ) 26 | } 27 | 28 | export const Card = forwardRef(CardBase) 29 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Card/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Card" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Chart/Chart.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { globalStyle, style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const chartWrapper = style({ 9 | position: "relative" 10 | }) 11 | 12 | export const noData = style({ 13 | position: "absolute", 14 | top: "50%", 15 | left: "50%", 16 | transform: "translate(-50%, -50%)", 17 | fontSize: vars.fontSizes.size5, 18 | fontWeight: vars.fontWeights.weight500, 19 | padding: `${vars.sizes.size2} ${vars.sizes.size8}`, 20 | border: `1px dashed ${vars.colors.border}` 21 | }) 22 | 23 | export const uplot = style({ 24 | breakInside: "avoid" 25 | }) 26 | 27 | export const title = style({ 28 | color: vars.colors.text.secondary, 29 | fontSize: vars.fontSizes.size5, 30 | fontWeight: vars.fontWeights.weight500 31 | }) 32 | 33 | globalStyle(`${uplot} > .u-title`, { 34 | fontSize: vars.fontSizes.size6, 35 | fontWeight: `${vars.fontWeights.weight300} !important` 36 | }) 37 | 38 | globalStyle(`${uplot} .u-label`, { 39 | fontWeight: `${vars.fontWeights.weight300} !important` 40 | }) 41 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Chart/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./Chart" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/ClickAwayListener.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { cloneElement, useRef, type ReactElement } from "react" 6 | import { useEventCallback, useEventListener } from "usehooks-ts" 7 | 8 | import { useForkRef } from "hooks" 9 | 10 | export interface ClickAwayListenerProps { 11 | children: ReactElement 12 | onClickAway: (event: MouseEvent | KeyboardEvent) => void 13 | } 14 | 15 | export const ClickAwayListener = ({ children, onClickAway }: ClickAwayListenerProps) => { 16 | const documentRef = useRef(document) 17 | const nodeRef = useRef(null) 18 | // @ts-expect-error TODO upstream fix 19 | const handleRef = useForkRef(nodeRef, children.ref) 20 | 21 | const handleClickAway = useEventCallback((event: MouseEvent | KeyboardEvent) => { 22 | if (!nodeRef.current) { 23 | throw new Error("ClickAwayListener: missing ref") 24 | } 25 | 26 | // @ts-expect-error TODO upstream fix 27 | const isInDOM = !documentRef.current.contains(event.target) || nodeRef.current.contains(event.target) 28 | 29 | if (event.type === "keyup" && "key" in event) { 30 | if (!["Escape", "Tab"].includes(event.key)) { 31 | return 32 | } 33 | 34 | if (event.key === "Tab" && isInDOM) { 35 | return 36 | } 37 | } 38 | 39 | if (event.type === "mouseup" && isInDOM) { 40 | return 41 | } 42 | 43 | onClickAway(event) 44 | }) 45 | 46 | useEventListener("mouseup", handleClickAway, documentRef) 47 | useEventListener("keyup", handleClickAway, documentRef) 48 | 49 | return <>{cloneElement(children, { ref: handleRef })} 50 | } 51 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Collapse/Collapse.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { defineProperties, createSprinkles } from "@vanilla-extract/sprinkles" 7 | 8 | import { vars } from "theme" 9 | 10 | const borderProps = defineProperties({ 11 | properties: { 12 | borderRadius: { 13 | true: `${vars.borderRadius.md} ${vars.borderRadius.md} 0 0`, 14 | false: vars.borderRadius.md 15 | } 16 | } 17 | }) 18 | 19 | export const header = style({ 20 | color: vars.colors.text.secondary, 21 | padding: vars.sizes.size6, 22 | backgroundColor: vars.colors.secondary.light, 23 | cursor: "pointer", 24 | border: "none" 25 | }) 26 | 27 | export const title = style({ 28 | fontSize: vars.fontSizes.size6, 29 | fontWeight: vars.fontWeights.weight500 30 | }) 31 | 32 | export const content = style({ 33 | padding: vars.sizes.size8, 34 | backgroundColor: vars.colors.secondary.main 35 | }) 36 | 37 | export const border = createSprinkles(borderProps) 38 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Collapse/Collapse.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { type ReactNode } from "react" 6 | 7 | import { toClassName } from "utils" 8 | import { Flex } from "components/Flex" 9 | import { Icon } from "components/Icon" 10 | 11 | import * as styles from "./Collapse.css" 12 | 13 | interface CollapseProps { 14 | children: ReactNode 15 | title: string 16 | isOpen: boolean 17 | onClick: () => void 18 | } 19 | 20 | export const Collapse = ({ children, title, isOpen, onClick }: CollapseProps) => { 21 | return ( 22 |
23 | 30 | {isOpen ? : } 31 |

{title}

32 |
33 | 34 | {isOpen &&
{children}
} 35 |
36 | ) 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Collapse/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Collapse" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Divider/Divider.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const divider = style({ 9 | backgroundColor: vars.colors.border, 10 | height: 3 11 | }) 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Divider/Divider.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | 7 | import { toClassName } from "utils" 8 | 9 | import * as styles from "./Divider.css" 10 | 11 | interface DividerProps { 12 | className?: string 13 | } 14 | 15 | export const Divider = ({ className, ...props }: DividerProps) => { 16 | return
17 | } 18 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Divider/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Divider" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Flex/Flex.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, createThemeContract } from "@vanilla-extract/css" 6 | import { createSprinkles, defineProperties } from "@vanilla-extract/sprinkles" 7 | 8 | import { vars } from "theme" 9 | 10 | const variantProps = defineProperties({ 11 | properties: { 12 | flexDirection: ["row", "column"], 13 | flexWrap: ["nowrap", "wrap", "wrap-reverse"], 14 | alignItems: ["flex-start", "flex-end", "stretch", "center", "baseline", "start", "end", "self-start", "self-end"], 15 | justifyContent: [ 16 | "flex-start", 17 | "flex-end", 18 | "start", 19 | "end", 20 | "left", 21 | "right", 22 | "center", 23 | "space-between", 24 | "space-around", 25 | "space-evenly" 26 | ], 27 | gap: { 28 | 0: 0, 29 | 1: vars.sizes.size1, 30 | 2: vars.sizes.size2, 31 | 3: vars.sizes.size6, 32 | 4: vars.sizes.size9, 33 | 5: vars.sizes.size11 34 | }, 35 | padding: { 36 | 0: 0, 37 | 1: vars.sizes.size1, 38 | 2: vars.sizes.size2, 39 | 3: vars.sizes.size6, 40 | 4: vars.sizes.size9, 41 | 5: vars.sizes.size11 42 | } 43 | } 44 | }) 45 | 46 | export const theme = createThemeContract({ 47 | flexGrow: null, 48 | flexShrink: null, 49 | flexBasis: null, 50 | height: null, 51 | width: null 52 | }) 53 | 54 | export const root = style({ 55 | display: "flex", 56 | ...theme 57 | }) 58 | 59 | export const variants = createSprinkles(variantProps) 60 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type ReactNode, type Ref, type ElementType, type HTMLAttributes } from "react" 6 | 7 | import { toClassName, toStyle } from "utils" 8 | 9 | import type { FlexElementProps } from "./Flex.types" 10 | import { root, theme, variants } from "./Flex.css" 11 | 12 | export interface FlexProps extends HTMLAttributes, FlexElementProps { 13 | children: ReactNode 14 | className?: string 15 | as?: ElementType 16 | } 17 | 18 | function FlexBase( 19 | { 20 | as: Tag = "div", 21 | align, 22 | basis, 23 | children, 24 | className, 25 | direction, 26 | gap = 3, 27 | grow, 28 | height, 29 | justify, 30 | padding, 31 | shrink, 32 | width, 33 | wrap, 34 | ...props 35 | }: FlexProps, 36 | ref: Ref 37 | ) { 38 | const variantClassName = variants({ 39 | alignItems: align, 40 | flexDirection: direction, 41 | flexWrap: wrap, 42 | gap, 43 | justifyContent: justify, 44 | padding 45 | }) 46 | 47 | const classNames = toClassName(root, variantClassName, className) 48 | const styles = toStyle(theme, { flexBasis: basis, flexGrow: grow, flexShrink: shrink, height, width }) 49 | 50 | return ( 51 | 52 | {children} 53 | 54 | ) 55 | } 56 | 57 | export const Flex = forwardRef(FlexBase) 58 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Flex/Flex.types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | type AlignItems = "stretch" | "flex-start" | "flex-end" | "center" | "baseline" | "start" | "end" | "self-start" | "self-end" 6 | 7 | type FlexBasis = "auto" | string 8 | 9 | type FlexDirection = "column" | "row" 10 | 11 | type FlexWrap = "nowrap" | "wrap" | "wrap-reverse" 12 | 13 | type JustifyContent = 14 | | "flex-start" 15 | | "flex-end" 16 | | "center" 17 | | "space-between" 18 | | "space-around" 19 | | "space-evenly" 20 | | "start" 21 | | "end" 22 | | "left" 23 | | "right" 24 | 25 | export interface FlexElementProps { 26 | align?: AlignItems 27 | direction?: FlexDirection 28 | gap?: 0 | 1 | 2 | 3 | 4 | 5 29 | justify?: JustifyContent 30 | wrap?: FlexWrap 31 | basis?: FlexBasis 32 | grow?: number 33 | shrink?: number 34 | padding?: 0 | 1 | 2 | 3 | 4 | 5 35 | height?: string | number 36 | width?: string | number 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Flex/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Flex" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Footer/Footer.css.ts: -------------------------------------------------------------------------------- 1 | import { style } from "@vanilla-extract/css" 2 | 3 | import { vars } from "theme" 4 | 5 | export const footer = style({ 6 | position: "sticky", 7 | bottom: 0, 8 | left: 0, 9 | padding: `${vars.sizes.size5} ${vars.sizes.size6}`, 10 | backgroundColor: vars.colors.secondary.dark, 11 | textAlign: "right" 12 | }) 13 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Footer/Footer.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | import { useMediaQuery } from "usehooks-ts" 3 | 4 | import { vars } from "theme" 5 | import { useTimeRange } from "store/timeRange" 6 | import { TimeRangeResetButton } from "components/TimeRangeResetButton" 7 | 8 | import * as styles from "./Footer.css" 9 | 10 | export const Footer = () => { 11 | const isTablet = useMediaQuery(`(max-width: ${vars.breakpoints.header})`) 12 | const { timeRange } = useTimeRange() 13 | 14 | if (!timeRange || !isTablet) { 15 | return null 16 | } 17 | 18 | return ( 19 |
20 | 21 |
22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Grid/Grid.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type ElementType, type ReactNode, type Ref, type HTMLAttributes } from "react" 6 | 7 | import type { GridElementProps, GridColumnElementProps } from "./Grid.types" 8 | import { column, container } from "./Grid.css" 9 | import { toClassName } from "utils" 10 | 11 | export interface GridProps extends HTMLAttributes, GridElementProps { 12 | children: ReactNode 13 | className?: string 14 | as?: ElementType 15 | } 16 | 17 | export interface GridColumnProps extends HTMLAttributes, GridColumnElementProps { 18 | children: ReactNode 19 | className?: string 20 | as?: ElementType 21 | } 22 | 23 | function GridBase({ as: Tag = "div", gap = 3, children, className, ...props }: GridProps, ref: Ref) { 24 | return ( 25 | 26 | {children} 27 | 28 | ) 29 | } 30 | 31 | function Column( 32 | { children, as: Tag = "div", className, xs = 12, sm, md, lg, xl, xxl, ...props }: GridColumnProps, 33 | ref: Ref 34 | ) { 35 | return ( 36 | 52 | {children} 53 | 54 | ) 55 | } 56 | 57 | export const Grid = Object.assign(forwardRef(GridBase), { 58 | Column: forwardRef(Column) 59 | }) 60 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Grid/Grid.types.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export interface GridElementProps { 6 | columns?: number 7 | gap?: 1 | 2 | 3 | 4 8 | height?: string | number 9 | width?: string | number 10 | } 11 | 12 | export interface GridStyleProps extends Omit { 13 | $height?: string | number 14 | $width?: string | number 15 | } 16 | 17 | export interface GridColumnElementProps { 18 | xs?: number 19 | sm?: number 20 | md?: number 21 | lg?: number 22 | xl?: number 23 | xxl?: number 24 | } 25 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Grid/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Grid" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Header/Header.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const header = style({ 10 | backgroundColor: vars.colors.secondary.main, 11 | boxShadow: `0 0 10px ${vars.colors.shadow}`, 12 | position: "sticky", 13 | top: 0, 14 | zIndex: 1 15 | }) 16 | 17 | export const content = style({ 18 | padding: `${vars.sizes.size3} ${vars.sizes.size6}` 19 | }) 20 | 21 | export const options = style({ 22 | padding: 0 23 | }) 24 | 25 | export const divider = style({ 26 | "@media": { 27 | [`(min-width: ${vars.breakpoints.header})`]: { 28 | display: "none" 29 | } 30 | } 31 | }) 32 | 33 | export const stats = style({ 34 | border: `2px solid ${vars.colors.border}`, 35 | padding: `${vars.sizes.size3} ${vars.sizes.size5}`, 36 | borderRadius: vars.borderRadius.md 37 | }) 38 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Header/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Header" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Icon/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Icon" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/IconButton/IconButton.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, styleVariants } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const button = style({ 9 | padding: `${vars.sizes.size1} ${vars.sizes.size1}`, 10 | position: "relative", 11 | ":before": { 12 | content: '""', 13 | position: "absolute", 14 | top: 0, 15 | left: 0, 16 | width: "100%", 17 | height: "100%", 18 | borderRadius: "100%", 19 | transform: "scale(0)", 20 | transition: "transform 0.2s ease-in-out", 21 | zIndex: -1 22 | }, 23 | selectors: { 24 | "&:hover:before": { 25 | backgroundColor: vars.colors.action.hover, 26 | transform: "scale(1)" 27 | }, 28 | "&:active:before": { 29 | backgroundColor: vars.colors.action.active 30 | } 31 | } 32 | }) 33 | 34 | export const icon = styleVariants({ 35 | fill: [], 36 | outline: [], 37 | text: [{ color: vars.colors.text.primary }] 38 | }) 39 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/IconButton/IconButton.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, Ref } from "react" 6 | 7 | import { toClassName } from "utils" 8 | import { Button, ButtonProps } from "components/Button" 9 | import { Icon, type IconMap } from "components/Icon" 10 | 11 | import * as styles from "./IconButton.css" 12 | 13 | interface IconButtonProps extends Omit { 14 | name: keyof typeof IconMap 15 | title?: string 16 | } 17 | 18 | function IconButtonBase( 19 | { className, name, title, variant = "fill", ...props }: IconButtonProps, 20 | ref: Ref 21 | ) { 22 | return ( 23 | 26 | ) 27 | } 28 | 29 | export const IconButton = forwardRef(IconButtonBase) 30 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/IconButton/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./IconButton" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/LoadingContainer/LoadingContainer.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const icon = style({ 9 | animation: `${vars.animation.spin} 1s linear infinite`, 10 | color: vars.colors.text.secondary 11 | }) 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/LoadingContainer/LoadingContainer.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { type ReactNode } from "react" 6 | 7 | import { Flex } from "components/Flex" 8 | import { Icon } from "components/Icon" 9 | 10 | import * as styles from "./LoadingContainer.css" 11 | 12 | interface LoadingContainerProps { 13 | children: ReactNode 14 | message?: string 15 | isLoading: boolean 16 | } 17 | 18 | export function LoadingContainer({ children, message, isLoading }: LoadingContainerProps) { 19 | if (!isLoading) { 20 | return children 21 | } 22 | 23 | return ( 24 | 25 | 26 |

{message}

27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/LoadingContainer/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./LoadingContainer" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Menu/Menu.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, styleVariants } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const popperBase = style({ 9 | backgroundColor: vars.colors.secondary.main, 10 | border: `1px solid ${vars.colors.primary.main}`, 11 | padding: 0, 12 | minWidth: 150, 13 | overflow: "hidden", 14 | zIndex: 10 15 | }) 16 | 17 | export const popper = styleVariants({ 18 | light: [ 19 | popperBase, 20 | { 21 | boxShadow: `0 0 8px rgba(0, 0, 0, 0.15)` 22 | } 23 | ], 24 | dark: [ 25 | popperBase, 26 | { 27 | boxShadow: "0 0 8px rgba(0, 0, 0, 0.8)" 28 | } 29 | ] 30 | }) 31 | 32 | export const item = style({ 33 | cursor: "pointer", 34 | fontSize: vars.sizes.size5, 35 | padding: vars.sizes.size3, 36 | ":hover": { 37 | backgroundColor: vars.colors.action.hover 38 | } 39 | }) 40 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Menu" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Nav/Nav.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | 7 | import { Button } from "components/Button" 8 | import { Flex } from "components/Flex" 9 | 10 | import * as styles from "./Nav.css" 11 | import { toClassName } from "utils" 12 | 13 | interface Option { 14 | title?: string 15 | id?: string 16 | } 17 | 18 | interface NavProps { 19 | options: Option[] 20 | value: number 21 | onChange: (idx: number) => void 22 | } 23 | 24 | export function Nav({ options, value, onChange }: NavProps) { 25 | return ( 26 | 33 | ) 34 | } 35 | 36 | interface ItemProps { 37 | index: number 38 | label?: string 39 | value: number 40 | onChange: (idx: number) => void 41 | } 42 | 43 | function Item({ index, label, value, onChange, ...props }: ItemProps) { 44 | const isActive = index === value 45 | const state = isActive ? "active" : "inactive" 46 | 47 | return ( 48 | 51 | ) 52 | } 53 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Nav/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Nav" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Panel.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | 7 | import { PanelKind, Panel as PanelClass } from "@xk6-dashboard/view" 8 | 9 | import Chart from "components/Chart" 10 | import Stat from "components/Stat" 11 | import Summary from "components/Summary" 12 | 13 | interface PanelProps { 14 | container?: boolean 15 | panel: PanelClass 16 | } 17 | 18 | export default function Panel({ container, panel }: PanelProps) { 19 | switch (panel.kind) { 20 | case PanelKind.chart: { 21 | return 22 | } 23 | case PanelKind.stat: { 24 | return 25 | } 26 | case PanelKind.summary: { 27 | return 28 | } 29 | default: { 30 | return null 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Paper/Paper.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const styles = style({ 10 | backgroundColor: vars.colors.secondary.main, 11 | borderRadius: vars.borderRadius.md, 12 | padding: vars.sizes.size8 13 | }) 14 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Paper/Paper.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { forwardRef, type HTMLAttributes, type Ref } from "react" 6 | 7 | import { toClassName } from "utils" 8 | 9 | import { styles } from "./Paper.css" 10 | 11 | interface PaperProps extends HTMLAttributes {} 12 | 13 | function PaperBase({ children, className, ...props }: PaperProps, ref: Ref) { 14 | return ( 15 |
16 | {children} 17 |
18 | ) 19 | } 20 | 21 | export const Paper = forwardRef(PaperBase) 22 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Paper/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Paper" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Progress/Progress.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { type ReactNode } from "react" 6 | 7 | import * as styles from "./Progress.css" 8 | 9 | interface ProgressProps { 10 | children?: ReactNode | ReactNode[] 11 | className?: string 12 | color?: string 13 | isLoading?: boolean 14 | max?: string 15 | value?: string | number 16 | } 17 | 18 | export const Progress = ({ children, isLoading = false, max = "100", value, ...props }: ProgressProps) => { 19 | const variant = isLoading ? "loading" : "default" 20 | 21 | return ( 22 |
23 | 24 | {children ?
{children}
: null} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Progress/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Progress" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Section/Section.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const summary = style({ 9 | marginBottom: vars.sizes.size6 10 | }) 11 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Section/Section.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { useState } from "react" 6 | 7 | import { isEmptySection, Section as SectionClass } from "@xk6-dashboard/view" 8 | 9 | import { useDigest } from "store/digest" 10 | import { Collapse } from "components/Collapse" 11 | import { Flex } from "components/Flex" 12 | import { Grid } from "components/Grid" 13 | 14 | import Panel from "../Panel" 15 | import * as styles from "./Section.css" 16 | 17 | interface SectionBodyProps { 18 | container?: boolean 19 | section: SectionClass 20 | } 21 | 22 | function SectionBody({ container, section }: SectionBodyProps) { 23 | return ( 24 | 25 | {section.panels.map((panel) => ( 26 | 27 | ))} 28 | 29 | ) 30 | } 31 | 32 | interface SectionProps { 33 | section: SectionClass 34 | } 35 | 36 | export function Section({ section }: SectionProps) { 37 | const [open, setOpen] = useState(true) 38 | const digest = useDigest() 39 | const empty = isEmptySection(section, digest) 40 | 41 | if (empty) { 42 | return null 43 | } 44 | 45 | if (!section.title) { 46 | return ( 47 | 48 | {section.summary &&

{section.summary}

} 49 | 50 |
51 | ) 52 | } 53 | 54 | return ( 55 | 56 | setOpen(!open)}> 57 | 58 | 59 | 60 | ) 61 | } 62 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Section/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Section" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Stat/Stat.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { globalStyle, style } from "@vanilla-extract/css" 6 | import { vars } from "theme" 7 | 8 | export const uplot = style({ 9 | minHeight: "100%" 10 | }) 11 | 12 | globalStyle(`${uplot} > .u-title`, { 13 | color: vars.colors.text.primary, 14 | fontSize: vars.fontSizes.size7, 15 | fontWeight: `${vars.fontWeights.weight400} !important`, 16 | whiteSpace: "nowrap" 17 | }) 18 | 19 | export const container = style({ 20 | padding: vars.sizes.size5, 21 | height: "100%" 22 | }) 23 | 24 | export const title = style({ 25 | fontSize: vars.fontSizes.size5, 26 | fontWeight: vars.fontWeights.weight500, 27 | color: vars.colors.text.secondary, 28 | paddingTop: vars.sizes.size5, 29 | textAlign: "center" 30 | }) 31 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Stat/Stat.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import { useElementSize } from "usehooks-ts" 7 | import "uplot/dist/uPlot.min.css" 8 | import { type AlignedData } from "uplot" 9 | import UplotReact from "uplot-react" 10 | import { Panel, SeriesPlot } from "@xk6-dashboard/view" 11 | 12 | import { createColorScheme } from "utils" 13 | import { useDigest } from "store/digest" 14 | import { useTheme } from "store/theme" 15 | import { Flex } from "components/Flex" 16 | import { Grid } from "components/Grid" 17 | import { Paper } from "components/Paper" 18 | 19 | import { createOptions } from "./Stat.utils" 20 | import * as styles from "./Stat.css" 21 | 22 | interface StatProps { 23 | panel: Panel 24 | } 25 | 26 | export default function Stat({ panel }: StatProps) { 27 | const digest = useDigest() 28 | const { theme } = useTheme() 29 | const [ref, { width }] = useElementSize() 30 | 31 | const plot = new SeriesPlot(digest, panel, createColorScheme(theme)) 32 | 33 | if (plot.empty) { 34 | return null 35 | } 36 | 37 | const options = createOptions({ digest, panel, plot, width }) 38 | 39 | return ( 40 | 41 | 42 | 43 |

{panel.title}

44 |
45 | 46 |
47 |
48 |
49 |
50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Stat/Stat.utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { type Options, type Series } from "uplot" 6 | import { Digest } from "@xk6-dashboard/model" 7 | import { format, Panel, SeriesPlot } from "@xk6-dashboard/view" 8 | 9 | import * as styles from "./Stat.css" 10 | 11 | const CHART_HEIGHT = 32 12 | 13 | interface CreateOptionsProps { 14 | digest: Digest 15 | panel: Panel 16 | plot: SeriesPlot 17 | width: number 18 | } 19 | 20 | export const createOptions = ({ digest, panel, plot, width }: CreateOptionsProps): Options => { 21 | const query = panel.series[0].query 22 | const serie = digest.samples.query(query) 23 | let title: string | undefined 24 | 25 | if (serie && Array.isArray(serie.values) && serie.values.length !== 0) { 26 | title = format(serie.unit, Number(serie.values.slice(-1)), true) 27 | } 28 | 29 | const options: Options = { 30 | class: styles.uplot, 31 | width: width, 32 | height: CHART_HEIGHT, 33 | title: title, 34 | series: plot.series as Series[], 35 | axes: [{ show: false }, { show: false }], 36 | legend: { show: false }, 37 | cursor: { show: false } 38 | } 39 | 40 | return options 41 | } 42 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Stat/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./Stat" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Summary/Summary.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | 7 | export const container = style({ 8 | minHeight: "100%" 9 | }) 10 | 11 | export const body = style({ 12 | overflowX: "auto" 13 | }) 14 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Summary/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { default } from "./Summary" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Table/Table.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style, styleVariants } from "@vanilla-extract/css" 6 | 7 | import { vars } from "theme" 8 | 9 | export const table = style({ 10 | borderCollapse: "collapse", 11 | width: "100%" 12 | }) 13 | 14 | export const th = style({ 15 | padding: vars.sizes.size3, 16 | fontSize: vars.fontSizes.size5, 17 | fontWeight: vars.fontWeights.weight600 18 | }) 19 | 20 | export const tr = styleVariants({ 21 | thead: [], 22 | tbody: [ 23 | { 24 | ":hover": { 25 | backgroundColor: vars.colors.components.table.row.hover 26 | } 27 | }, 28 | { 29 | selectors: { 30 | "&:nth-child(odd):not(:hover)": { 31 | backgroundColor: vars.colors.secondary.light 32 | }, 33 | "&:nth-child(even):not(:hover)": { 34 | backgroundColor: vars.colors.secondary.main 35 | } 36 | } 37 | } 38 | ] 39 | }) 40 | 41 | export const td = style({ 42 | padding: vars.sizes.size3, 43 | fontSize: vars.fontSizes.size4 44 | }) 45 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Table" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Tabs/Tabs.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { style } from "@vanilla-extract/css" 6 | import { createSprinkles, defineProperties } from "@vanilla-extract/sprinkles" 7 | 8 | import { vars } from "theme" 9 | 10 | const borderSize = 2 11 | 12 | export const tabsBase = style({ 13 | background: vars.colors.primary.dark, 14 | borderBottom: `${borderSize}px solid ${vars.colors.primary.main}` 15 | }) 16 | 17 | export const tabBase = style({ 18 | padding: `${vars.sizes.size3} ${vars.sizes.size6}`, 19 | fontSize: vars.fontSizes.size4, 20 | cursor: "pointer", 21 | textTransform: "uppercase", 22 | marginBottom: `-${borderSize}px` 23 | }) 24 | 25 | const tabsProperties = defineProperties({ 26 | properties: { 27 | color: { 28 | active: vars.colors.primary.main, 29 | inactive: vars.colors.text.disabled 30 | }, 31 | borderBottom: { 32 | active: `2px solid ${vars.colors.primary.main}`, 33 | inactive: "transparent" 34 | } 35 | } 36 | }) 37 | 38 | export const tabVariants = createSprinkles(tabsProperties) 39 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { createContext, useContext, type ReactNode } from "react" 6 | 7 | import { Flex } from "../Flex" 8 | 9 | import { tabBase, tabVariants, tabsBase } from "./Tabs.css" 10 | import { toClassName } from "utils" 11 | 12 | interface ContextProps { 13 | value: number 14 | onChange: (index: number) => void 15 | } 16 | 17 | export interface TabsProps extends ContextProps { 18 | children: ReactNode 19 | className?: string 20 | } 21 | 22 | export interface TabProps { 23 | className?: string 24 | label?: string 25 | index: number 26 | } 27 | 28 | const Context = createContext>({}) 29 | 30 | function TabsBase({ children, className, value, onChange, ...props }: TabsProps) { 31 | const context = { 32 | value, 33 | onChange 34 | } 35 | 36 | return ( 37 | 38 | {children} 39 | 40 | ) 41 | } 42 | 43 | function TabBase({ label, index, ...props }: TabProps) { 44 | const { value, onChange } = useContext(Context) as ContextProps 45 | const state = index === value ? "active" : "inactive" 46 | 47 | return ( 48 |
onChange(index)} 51 | {...props}> 52 | {label} 53 |
54 | ) 55 | } 56 | 57 | export const Tabs = Object.assign(TabsBase, { 58 | Tab: TabBase 59 | }) 60 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Tabs/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Tabs" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/TimeRangeResetButton.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import { useTimeRange } from "store/timeRange" 4 | import { Button } from "components/Button" 5 | import { Flex } from "components/Flex" 6 | import { Icon } from "components/Icon" 7 | 8 | export const TimeRangeResetButton = () => { 9 | const { timeRange, setTimeRange } = useTimeRange() 10 | 11 | if (!timeRange) { 12 | return null 13 | } 14 | 15 | return ( 16 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Tooltip/Tooltip.utils.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { type Placement } from "@popperjs/core" 6 | 7 | interface Attributes { 8 | [key: string]: { [key: string]: string } | undefined 9 | } 10 | 11 | export const getPopperPlacement = (attributes?: Attributes) => { 12 | return attributes?.popper?.["data-popper-placement"] as Placement | undefined 13 | } 14 | 15 | export const getArrowPosition = (placement?: Placement) => { 16 | if (!placement) { 17 | return "left" 18 | } 19 | 20 | if (placement.startsWith("top")) { 21 | return "top" 22 | } 23 | 24 | if (placement.startsWith("bottom")) { 25 | return "bottom" 26 | } 27 | 28 | if (placement.startsWith("right")) { 29 | return "right" 30 | } 31 | 32 | return "left" 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/components/Tooltip/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./Tooltip" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./useForkRef" 6 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/hooks/useForkRef.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { useMemo, type MutableRefObject, type RefCallback, type Ref } from "react" 6 | 7 | type PossibleRef = Ref | undefined 8 | 9 | function setRef(ref: PossibleRef | null, value: T) { 10 | if (typeof ref === "function") { 11 | ref(value) 12 | } else if (ref !== null && ref !== undefined) { 13 | ;(ref as MutableRefObject).current = value 14 | } 15 | } 16 | 17 | export function useForkRef(refA: PossibleRef | null, refB: PossibleRef | null): RefCallback | null { 18 | /** 19 | * This will create a new function if the ref props change and are defined. 20 | * This means react will call the old forkRef with `null` and the new forkRef 21 | * with the ref. Cleanup naturally emerges from this behavior 22 | */ 23 | return useMemo(() => { 24 | if (refA === null && refB === null) { 25 | return null 26 | } 27 | 28 | return (refValue: T) => { 29 | setRef(refA, refValue) 30 | setRef(refB, refValue) 31 | } 32 | }, [refA, refB]) 33 | } 34 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/main.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React from "react" 6 | import ReactDOM from "react-dom/client" 7 | 8 | import { DigestProvider } from "store/digest" 9 | import { ThemeProvider } from "store/theme" 10 | import { TimeRangeProvider } from "store/timeRange" 11 | 12 | import App from "App" 13 | 14 | const base = new URLSearchParams(window.location.search).get("endpoint") || "http://localhost:5665/" 15 | const rootElement = document.getElementById("root") as HTMLDivElement 16 | 17 | ReactDOM.createRoot(rootElement).render( 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | ) 26 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/store/digest.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { createContext, useContext, useEffect, useState, type ReactNode } from "react" 6 | import defaultConfig from "@xk6-dashboard/config" 7 | import { Digest, Config, EventType } from "@xk6-dashboard/model" 8 | 9 | const DigestContext = createContext(() => new Digest({ config: defaultConfig } as Digest)) 10 | DigestContext.displayName = "Digest" 11 | 12 | interface DigestProviderProps { 13 | children: ReactNode 14 | endpoint: string 15 | } 16 | 17 | function DigestProvider({ endpoint = "/events", children }: DigestProviderProps) { 18 | const [digest, setDigest] = useState(new Digest({ config: new Config(defaultConfig) })) 19 | 20 | useEffect(() => { 21 | const source = new EventSource(endpoint) 22 | 23 | const listener = (event: MessageEvent) => { 24 | digest.handleEvent(event) 25 | setDigest(new Digest(digest)) 26 | } 27 | 28 | for (const type in EventType) { 29 | source.addEventListener(type, listener) 30 | } 31 | }, []) 32 | 33 | return digest}>{children} 34 | } 35 | 36 | function useDigest() { 37 | const context = useContext(DigestContext) 38 | 39 | if (context === undefined) { 40 | throw new Error("useDigest must be used within a DigestProvider") 41 | } 42 | 43 | return context() 44 | } 45 | 46 | export { DigestProvider, useDigest } 47 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/store/theme.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { createContext, useContext, type Dispatch, type ReactNode, type SetStateAction } from "react" 6 | 7 | import { darkTheme, lightTheme } from "theme" 8 | import { useMediaQuery, useSessionStorage } from "usehooks-ts" 9 | 10 | export type Theme = "light" | "dark" 11 | 12 | interface ThemeContextProps { 13 | theme: Theme 14 | themeClassName: string 15 | setTheme: Dispatch> 16 | } 17 | 18 | const ThemeContext = createContext>({}) 19 | 20 | interface ThemeProviderProps { 21 | children: ReactNode 22 | } 23 | 24 | function ThemeProvider({ children }: ThemeProviderProps) { 25 | const isDarkMode = useMediaQuery("(prefers-color-scheme: dark)") 26 | const [theme, setTheme] = useSessionStorage("theme", isDarkMode ? "dark" : "light") 27 | 28 | const context = { 29 | theme, 30 | themeClassName: theme === "light" ? lightTheme : darkTheme, 31 | setTheme 32 | } 33 | 34 | return {children} 35 | } 36 | 37 | function useTheme() { 38 | const context = useContext(ThemeContext) 39 | 40 | if (context === undefined) { 41 | throw new Error("useTheme must be used within a ThemeProvider") 42 | } 43 | 44 | return context as ThemeContextProps 45 | } 46 | 47 | export { ThemeProvider, useTheme } 48 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/store/timeRange.tsx: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import React, { createContext, useContext, useState, type Dispatch, type ReactNode, type SetStateAction } from "react" 6 | 7 | interface TimeRange { 8 | from?: number 9 | to?: number 10 | } 11 | 12 | interface TimeRangeContextProps { 13 | timeRange: TimeRange | undefined 14 | setTimeRange: Dispatch> 15 | } 16 | 17 | const TimeRangeContext = createContext(undefined) 18 | 19 | interface TimeRangeProviderProps { 20 | children: ReactNode 21 | } 22 | 23 | function TimeRangeProvider({ children }: TimeRangeProviderProps) { 24 | const [timeRange, setTimeRange] = useState() 25 | 26 | const context = { 27 | timeRange, 28 | setTimeRange 29 | } 30 | 31 | return {children} 32 | } 33 | 34 | function useTimeRange() { 35 | const context = useContext(TimeRangeContext) 36 | 37 | if (context === undefined) { 38 | throw new Error("useTimeRange must be used within a TimeRangeProvider") 39 | } 40 | 41 | return context as TimeRangeContextProps 42 | } 43 | 44 | export { TimeRangeProvider, useTimeRange } 45 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/theme/animation.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { keyframes } from "@vanilla-extract/css" 6 | 7 | export const spin = keyframes({ 8 | from: { 9 | transform: "rotate(0deg)" 10 | }, 11 | to: { 12 | transform: "rotate(360deg)" 13 | } 14 | }) 15 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/theme/global.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { globalStyle } from "@vanilla-extract/css" 6 | 7 | import { fontSizes, fonts, letterSpacings } from "./typography.css" 8 | 9 | globalStyle("*, *::before, *::after", { 10 | boxSizing: "border-box", 11 | margin: 0, 12 | padding: 0 13 | }) 14 | 15 | globalStyle("*", { 16 | fontFamily: fonts.sans 17 | }) 18 | 19 | globalStyle("html", { 20 | fontSize: "62.5%", 21 | MozOsxFontSmoothing: "grayscale", 22 | WebkitFontSmoothing: "antialiased", 23 | WebkitTextSizeAdjust: "100%", 24 | textSizeAdjust: "100%" 25 | }) 26 | 27 | globalStyle("body", { 28 | letterSpacing: letterSpacings.size3, 29 | lineHeight: 1.5, 30 | textRendering: "optimizeLegibility" 31 | }) 32 | 33 | globalStyle("img, picture, video, canvas, svg", { 34 | display: "block", 35 | maxWidth: "100%" 36 | }) 37 | 38 | globalStyle("input, button, textarea, select", { 39 | font: "inherit" 40 | }) 41 | 42 | globalStyle("p, h1, h2, h3, h4, h5, h6", { 43 | overflowWrap: "break-word" 44 | }) 45 | 46 | globalStyle("p", { 47 | fontSize: fontSizes.size4 48 | }) 49 | 50 | globalStyle("#root", { 51 | isolation: "isolate" 52 | }) 53 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/theme/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./global.css" 6 | export * from "./theme.css" 7 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/theme/sizes.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const sizes = { 6 | size1: ".25rem", 7 | size2: ".5rem", 8 | size3: ".75rem", 9 | size4: "1rem", 10 | size5: "1.25rem", 11 | size6: "1.5rem", 12 | size7: "1.75rem", 13 | size8: "2rem", 14 | size9: "3rem", 15 | size10: "4rem", 16 | size11: "5rem", 17 | xs: "0px", 18 | sm: "480px", 19 | md: "768px", 20 | lg: "1024px", 21 | xl: "1440px", 22 | xxl: "1920px" 23 | } 24 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/theme/typography.css.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const fonts = { 6 | sans: "system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif", 7 | serif: "ui-serif,serif", 8 | mono: "Dank Mono,Operator Mono,Inconsolata,Fira Mono,ui-monospace,SF Mono,Monaco,Droid Sans Mono,Source Code Pro,monospace" 9 | } 10 | 11 | export const fontSizes = { 12 | size0: ".5rem", 13 | size1: ".75rem", 14 | size2: "1rem", 15 | size3: "1.1rem", 16 | size4: "1.25rem", 17 | size5: "1.5rem", 18 | size6: "2rem", 19 | size7: "2.5rem", 20 | size8: "3rem", 21 | size9: "3.5rem" 22 | } 23 | 24 | export const fontWeights = { 25 | weight100: "100", 26 | weight200: "200", 27 | weight300: "300", 28 | weight400: "400", 29 | weight500: "500", 30 | weight600: "600", 31 | weight700: "700", 32 | weight800: "800", 33 | weight900: "900" 34 | } 35 | 36 | export const lineHeights = { 37 | size0: "0.95", 38 | size1: "1.1", 39 | size2: "1.25", 40 | size3: "1.375", 41 | size4: "1.5", 42 | size5: "1.75", 43 | size6: "2" 44 | } 45 | 46 | export const letterSpacings = { 47 | size0: "-.05em", 48 | size1: ".025em", 49 | size2: ".050em", 50 | size3: ".075em", 51 | size4: ".150em", 52 | size5: ".500em", 53 | size6: ".750em", 54 | size7: "1em" 55 | } 56 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/types/config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Section } from "@xk6-dashboard/view" 6 | 7 | export interface Tab { 8 | title?: string 9 | id?: string 10 | summary?: string 11 | report?: boolean 12 | sections: Section[] 13 | } 14 | 15 | export interface UIConfig { 16 | title: string 17 | tabs: Tab[] 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/types/theme.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export interface VectorAttrs { 6 | fill: string 7 | stroke: string 8 | } 9 | 10 | export interface Colors { 11 | 50: string 12 | 100: string 13 | 200: string 14 | 300: string 15 | 400: string 16 | 500: string 17 | 600: string 18 | 700: string 19 | 800: string 20 | 900: string 21 | A100: string 22 | A200: string 23 | A400: string 24 | A700: string 25 | } 26 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/typings/assets.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | declare module "*.md" 6 | declare module "*.svg" 7 | declare module "*.jpg" 8 | declare module "*.jpeg" 9 | declare module "*.png" 10 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/typings/xk6-dashboard-model.d.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Param } from "@xk6-dashboard/model" 6 | 7 | import type { UIConfig } from "types/config" 8 | 9 | declare module "@xk6-dashboard/model" { 10 | interface Config extends UIConfig {} 11 | 12 | interface Digest { 13 | config?: UIConfig 14 | param: Param & { period: number } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/utils/chart.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import type { Colors, VectorAttrs } from "types/theme" 6 | 7 | import { 8 | red, 9 | pink, 10 | purple, 11 | deepPurple, 12 | indigo, 13 | blue, 14 | lightBlue, 15 | cyan, 16 | teal, 17 | green, 18 | lightGreen, 19 | lime, 20 | yellow, 21 | amber, 22 | orange, 23 | deepOrange, 24 | brown, 25 | grey, 26 | blueGrey 27 | } from "theme/colors.css" 28 | 29 | const colors: Record> = { 30 | red, 31 | pink, 32 | purple, 33 | deepPurple, 34 | indigo, 35 | blue, 36 | lightBlue, 37 | cyan, 38 | teal, 39 | green, 40 | lightGreen, 41 | lime, 42 | yellow, 43 | amber, 44 | orange, 45 | deepOrange, 46 | brown, 47 | grey, 48 | blueGrey 49 | } as const 50 | 51 | const order = [ 52 | "grey", 53 | "teal", 54 | "blue", 55 | "purple", 56 | "indigo", 57 | "orange", 58 | "pink", 59 | "green", 60 | "cyan", 61 | "amber", 62 | "lime", 63 | "brown", 64 | "lightGreen", 65 | "red", 66 | "deepPurple", 67 | "lightBlue", 68 | "yellow", 69 | "deepOrange", 70 | "blueGrey" 71 | ] as const 72 | 73 | export const createColorScheme = (mode: string) => { 74 | return order.map((name) => ({ 75 | stroke: mode == "dark" ? colors[name][500] : colors[name][800], 76 | fill: (mode == "dark" ? colors[name][300] : colors[name][600]) + "20" 77 | })) 78 | } 79 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export * from "./chart" 6 | export * from "./object" 7 | export * from "./theme" 8 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/utils/object.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export const omitUndefined = (styles: T) => { 6 | return Object.entries(styles).reduce( 7 | (props, [prop, value]) => (value === undefined ? props : { ...props, [prop]: value }), 8 | {} 9 | ) 10 | } 11 | 12 | /** 13 | * This method is used to pick the specified properties from the object 14 | */ 15 | export const pick = (props: string[], obj: object) => 16 | Object.entries(obj).reduce( 17 | (acc, [key, value]) => { 18 | if (props.includes(key)) { 19 | acc[key] = value 20 | } 21 | 22 | return acc 23 | }, 24 | {} as { [key: string]: unknown } 25 | ) 26 | 27 | /** 28 | * This method is used to merge two objects together. It will overwrite the values of the first object with the values from the second object 29 | */ 30 | export const mergeRight = (a: object, b: object) => ({ ...a, ...b }) 31 | 32 | /** 33 | * This method is used to merge two objects together. It will overwrite the values of the first object with the specified properties from the second object 34 | */ 35 | export const mergeRightProps = (props: string[]) => (a: object, b: object) => mergeRight(a, pick(props, b)) 36 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { assignInlineVars } from "@vanilla-extract/dynamic" 6 | 7 | import { omitUndefined } from "./object" 8 | 9 | type CSSVarFunction = `var(--${string})` | `var(--${string}, ${string | number})` 10 | type Contract = { 11 | [key: string]: CSSVarFunction | null | Contract 12 | } 13 | 14 | export const toClassName = (...xs: unknown[]) => xs.filter(Boolean).join(" ") 15 | 16 | export const toStyle = (theme: Contract, styles: Record) => { 17 | return assignInlineVars(theme, omitUndefined(styles)) 18 | } 19 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2020", 4 | "useDefineForClassFields": true, 5 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 6 | "module": "ESNext", 7 | "skipLibCheck": true, 8 | "baseUrl": "src", 9 | 10 | /* Bundler mode */ 11 | "moduleResolution": "bundler", 12 | "allowImportingTsExtensions": true, 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "noEmit": true, 16 | "jsx": "react", 17 | 18 | /* Linting */ 19 | "strict": true, 20 | "noUnusedLocals": true, 21 | "noUnusedParameters": true, 22 | "noFallthroughCasesInSwitch": true 23 | }, 24 | "include": ["src"], 25 | "references": [{ "path": "./tsconfig.node.json" }] 26 | } 27 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "skipLibCheck": true, 5 | "module": "ESNext", 6 | "moduleResolution": "bundler", 7 | "allowSyntheticDefaultImports": true 8 | }, 9 | "include": ["vite.config.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /dashboard/assets/packages/ui/vite.config.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | import { defineConfig } from "vite" 8 | import react from "@vitejs/plugin-react-swc" 9 | import svgr from "vite-plugin-svgr" 10 | import { visualizer } from "rollup-plugin-visualizer" 11 | import tsconfigPaths from "vite-tsconfig-paths" 12 | import { vanillaExtractPlugin } from "@vanilla-extract/vite-plugin" 13 | 14 | export default defineConfig({ 15 | plugins: [ 16 | svgr(), 17 | react(), 18 | tsconfigPaths(), 19 | vanillaExtractPlugin(), 20 | visualizer({ 21 | filename: "bundle-stats.html" 22 | }) 23 | ], 24 | build: { chunkSizeWarningLimit: 512 /*, outDir: '../../ui'*/ }, 25 | base: "" 26 | }) 27 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "parser": "@typescript-eslint/parser", 7 | "extends": ["eslint:recommended", "plugin:@typescript-eslint/recommended", "prettier"], 8 | "plugins": ["@typescript-eslint", "prettier"], 9 | "parserOptions": { 10 | "ecmaVersion": "latest", 11 | "sourceType": "module" 12 | }, 13 | "rules": { 14 | "prettier/prettier": "error", 15 | "@typescript-eslint/no-unused-vars": "error", 16 | "@typescript-eslint/consistent-type-definitions": ["error", "type"] 17 | }, 18 | "ignorePatterns": ["dist/*"] 19 | } 20 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 128, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "singleQuote": false, 6 | "trailingComma": "none", 7 | "bracketSpacing": true, 8 | "semi": false, 9 | "bracketSameLine": true, 10 | "singleAttributePerLine": false 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@xk6-dashboard/view", 3 | "version": "0.0.1", 4 | "private": true, 5 | "type": "module", 6 | "scripts": { 7 | "build": "tsup --no-splitting --entry.index src/index.ts --entry.config src/Config.ts --dts-resolve --format esm", 8 | "dev": "tsup --no-splitting --entry.index src/index.ts --entry.config src/Config.ts --watch --dts-resolve --format esm", 9 | "lint": "eslint --ext .js,.ts ." 10 | }, 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": "./dist/index.js", 14 | "./config": "./dist/config.js" 15 | }, 16 | "dependencies": { 17 | "@types/numeral": "^2.0.2", 18 | "@xk6-dashboard/model": "0.0.1", 19 | "numeral": "^2.0.6", 20 | "pretty-bytes": "^6.1.1", 21 | "pretty-ms": "^8.0.0", 22 | "uplot": "^1.6.25" 23 | }, 24 | "devDependencies": { 25 | "@typescript-eslint/eslint-plugin": "^6.7.2", 26 | "@typescript-eslint/parser": "^6.7.2", 27 | "eslint": "^8.49.0", 28 | "eslint-config-prettier": "^9.0.0", 29 | "eslint-plugin-prettier": "^5.0.0", 30 | "prettier": "^3.0.3", 31 | "tsup": "^7.2.0", 32 | "typescript": "^5.2.2" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/src/SummaryTable.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Digest, SummaryView, SummaryRow, AggregateType, Metrics } from "@xk6-dashboard/model" 6 | import { Panel } from "./Config.ts" 7 | 8 | import { format } from "./format.ts" 9 | 10 | export class SummaryTable { 11 | view: SummaryView 12 | metrics: Metrics 13 | 14 | constructor(panel: Panel, digest: Digest) { 15 | this.metrics = digest.metrics 16 | const queries = panel.series.map((item) => item.query) 17 | this.view = digest.summary.select(queries) 18 | } 19 | 20 | get empty() { 21 | return this.view.empty 22 | } 23 | 24 | get cols() { 25 | return this.view.aggregates.length 26 | } 27 | 28 | get header(): Array { 29 | return new Array("metric", ...this.view.aggregates.map((a) => a as string)) 30 | } 31 | 32 | get body(): Array> { 33 | const rows = new Array>() 34 | 35 | for (let i = 0; i < this.view.length; i++) { 36 | const row = new Array() 37 | 38 | row.push(this.view[i].name) 39 | row.push(...this.view.aggregates.map((a) => this.format(this.view[i], a))) 40 | 41 | rows.push(row) 42 | } 43 | 44 | return rows 45 | } 46 | 47 | format(row: SummaryRow, aggregate: AggregateType): string { 48 | const unit = this.metrics.unit(row.metric?.name ?? "", aggregate) 49 | return format(unit, row.values[aggregate], true) 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/src/helper.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { Digest } from "@xk6-dashboard/model" 6 | import { Section, Panel, PanelKind } from "./Config.ts" 7 | 8 | export function isEmptySection(section: Section, digest: Digest): boolean { 9 | for (let i = 0; i < section.panels.length; i++) { 10 | const panel = section.panels[i] 11 | 12 | if (!isEmptyPanel(panel, digest)) { 13 | return false 14 | } 15 | } 16 | 17 | return true 18 | } 19 | 20 | function isEmptyPanel(panel: Panel, digest: Digest): boolean { 21 | if (panel.kind == PanelKind.summary) { 22 | return isEmptySummary(panel, digest) 23 | } 24 | 25 | return isEmptyPlot(panel, digest) 26 | } 27 | 28 | function isEmptyPlot(panel: Panel, digest: Digest): boolean { 29 | const plot = digest.samples.select(panel.series.map((s) => s.query)) 30 | 31 | return plot.empty 32 | } 33 | 34 | function isEmptySummary(panel: Panel, digest: Digest): boolean { 35 | const view = digest.summary.select(panel.series.map((s) => s.query)) 36 | 37 | return view.empty 38 | } 39 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/src/index.ts: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export { format, dateFormats } from "./format.ts" 6 | export { SeriesPlot } from "./SeriesPlot.ts" 7 | export { tooltipPlugin } from "./tooltip.ts" 8 | 9 | export { Serie, PanelKind, Panel, Section } from "./Config.ts" 10 | 11 | export { SummaryTable } from "./SummaryTable.ts" 12 | 13 | export { isEmptySection } from "./helper.ts" 14 | -------------------------------------------------------------------------------- /dashboard/assets/packages/view/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "module": "NodeNext", 5 | "moduleResolution": "NodeNext", 6 | "allowImportingTsExtensions": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "emitDeclarationOnly": true, 9 | "declaration": true 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /dashboard/assets_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | import ( 10 | "encoding/json" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_assetDir(t *testing.T) { 17 | t.Parallel() 18 | 19 | fs := assetDir("hu", testdata) 20 | 21 | require.NotNil(t, fs) 22 | require.Panics(t, func() { 23 | assetDir("..", testdata) 24 | }) 25 | } 26 | 27 | func Test_newAssets(t *testing.T) { 28 | t.Parallel() 29 | 30 | assertAssets(t, newTestAssets(t)) 31 | 32 | assertAssets(t, newAssets()) 33 | } 34 | 35 | func assertAssets(t *testing.T, assets *assets) { 36 | t.Helper() 37 | 38 | require.NotNil(t, assets.ui) 39 | 40 | file, err := assets.ui.Open("index.html") 41 | 42 | require.NoError(t, err) 43 | require.NotNil(t, file) 44 | 45 | require.NoError(t, file.Close()) 46 | 47 | require.NotNil(t, assets.report) 48 | 49 | file, err = assets.report.Open("index.html") 50 | 51 | require.NoError(t, err) 52 | require.NotNil(t, file) 53 | 54 | require.NoError(t, file.Close()) 55 | 56 | require.NotNil(t, assets.config) 57 | 58 | conf := map[string]interface{}{} 59 | 60 | require.NoError(t, json.Unmarshal(assets.config, &conf)) 61 | } 62 | -------------------------------------------------------------------------------- /dashboard/customize.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | package dashboard 6 | 7 | import ( 8 | "encoding/json" 9 | "io" 10 | 11 | "go.k6.io/k6/lib/fsext" 12 | ) 13 | 14 | const defaultAltConfig = ".dashboard.json" 15 | 16 | func findDefaultConfig(fs fsext.Fs) string { 17 | if exists(fs, defaultAltConfig) { 18 | return defaultAltConfig 19 | } 20 | 21 | return "" 22 | } 23 | 24 | // customize allows using custom dashboard configuration. 25 | func customize(uiConfig json.RawMessage, proc *process) (json.RawMessage, error) { 26 | filename, ok := proc.env["XK6_DASHBOARD_CONFIG"] 27 | if !ok || len(filename) == 0 { 28 | if filename = findDefaultConfig(proc.fs); len(filename) == 0 { 29 | return uiConfig, nil 30 | } 31 | } 32 | 33 | return loadConfigJSON(filename, proc) 34 | } 35 | 36 | func loadConfigJSON(filename string, proc *process) (json.RawMessage, error) { 37 | file, err := proc.fs.Open(filename) 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | bin, err := io.ReadAll(file) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | conf := map[string]interface{}{} 48 | 49 | if err := json.Unmarshal(bin, &conf); err != nil { 50 | return nil, err 51 | } 52 | 53 | return json.Marshal(conf) 54 | } 55 | 56 | func exists(fs fsext.Fs, filename string) bool { 57 | if _, err := fs.Stat(filename); err != nil { 58 | return false 59 | } 60 | 61 | return true 62 | } 63 | -------------------------------------------------------------------------------- /dashboard/customize_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | package dashboard 6 | 7 | import ( 8 | _ "embed" 9 | "testing" 10 | 11 | "github.com/stretchr/testify/require" 12 | "github.com/tidwall/gjson" 13 | ) 14 | 15 | func Test_loadConfigJSON(t *testing.T) { 16 | t.Parallel() 17 | 18 | th := helper(t).osFs() 19 | 20 | conf, err := loadConfigJSON("testdata/customize/config.json", th.proc) 21 | 22 | require.NoError(t, err) 23 | 24 | require.NotNil(t, gjson.GetBytes(conf, "tabs.custom")) 25 | 26 | _, err = loadConfigJSON("testdata/customize/config-bad.json", th.proc) 27 | 28 | require.Error(t, err) 29 | 30 | _, err = loadConfigJSON("testdata/customize/config-not-exists.json", th.proc) 31 | 32 | require.Error(t, err) 33 | } 34 | 35 | func Test_customize(t *testing.T) { 36 | t.Parallel() 37 | 38 | th := helper(t) 39 | 40 | conf, err := customize(testconfig, th.proc) 41 | 42 | require.NoError(t, err) 43 | 44 | require.False(t, gjson.GetBytes(conf, `tabs.#(id="custom")`).Exists()) 45 | } 46 | 47 | //go:embed testdata/customize/config/config.json 48 | var testconfig []byte 49 | -------------------------------------------------------------------------------- /dashboard/event.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | type eventListener interface { 10 | onEvent(event string, data interface{}) 11 | onStart() error 12 | onStop(reason error) error 13 | } 14 | 15 | type eventSource struct { 16 | listeners []eventListener 17 | } 18 | 19 | func (src *eventSource) addEventListener(listener eventListener) { 20 | src.listeners = append(src.listeners, listener) 21 | } 22 | 23 | func (src *eventSource) fireEvent(event string, data interface{}) { 24 | for _, e := range src.listeners { 25 | e.onEvent(event, data) 26 | } 27 | } 28 | 29 | func (src *eventSource) fireStart() error { 30 | for _, e := range src.listeners { 31 | if err := e.onStart(); err != nil { 32 | return err 33 | } 34 | } 35 | 36 | return nil 37 | } 38 | 39 | func (src *eventSource) fireStop(reason error) error { 40 | for _, e := range src.listeners { 41 | if err := e.onStop(reason); err != nil { 42 | return err 43 | } 44 | } 45 | 46 | return nil 47 | } 48 | -------------------------------------------------------------------------------- /dashboard/event_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/assert" 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | type errorEventListener struct{} 17 | 18 | func (*errorEventListener) onEvent(string, interface{}) {} 19 | 20 | func (*errorEventListener) onStart() error { 21 | return assert.AnError 22 | } 23 | 24 | func (*errorEventListener) onStop(_ error) error { 25 | return assert.AnError 26 | } 27 | 28 | func Test_eventSource_error(t *testing.T) { 29 | t.Parallel() 30 | 31 | src := new(eventSource) 32 | 33 | src.addEventListener(new(errorEventListener)) 34 | 35 | require.Error(t, src.fireStart()) 36 | require.Error(t, src.fireStop(nil)) 37 | } 38 | -------------------------------------------------------------------------------- /dashboard/process.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | package dashboard 6 | 7 | import ( 8 | "github.com/sirupsen/logrus" 9 | "go.k6.io/k6/cmd/state" 10 | "go.k6.io/k6/lib/fsext" 11 | "go.k6.io/k6/output" 12 | ) 13 | 14 | type process struct { 15 | logger logrus.FieldLogger 16 | fs fsext.Fs 17 | env map[string]string 18 | } 19 | 20 | func (proc *process) fromParams(params output.Params) *process { 21 | proc.fs = params.FS 22 | proc.logger = params.Logger 23 | proc.env = params.Environment 24 | 25 | return proc 26 | } 27 | 28 | func (proc *process) fromGlobalState(gs *state.GlobalState) *process { 29 | proc.fs = gs.FS 30 | proc.logger = gs.Logger 31 | proc.env = gs.Env 32 | 33 | return proc 34 | } 35 | -------------------------------------------------------------------------------- /dashboard/record.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | package dashboard 6 | 7 | import ( 8 | "compress/gzip" 9 | "encoding/json" 10 | "io" 11 | "strings" 12 | "sync" 13 | ) 14 | 15 | type recorder struct { 16 | output string 17 | proc *process 18 | mu sync.RWMutex 19 | encoder *json.Encoder 20 | writer io.WriteCloser 21 | } 22 | 23 | var _ eventListener = (*recorder)(nil) 24 | 25 | func newRecorder(output string, proc *process) *recorder { 26 | rec := &recorder{ 27 | output: output, 28 | proc: proc, 29 | } 30 | 31 | return rec 32 | } 33 | 34 | func (rec *recorder) onStart() error { 35 | file, err := rec.proc.fs.Create(rec.output) 36 | if err != nil { 37 | return err 38 | } 39 | 40 | rec.writer = file 41 | 42 | if strings.HasSuffix(rec.output, ".gz") { 43 | rec.writer = gzip.NewWriter(file) 44 | } 45 | 46 | rec.encoder = json.NewEncoder(rec.writer) 47 | 48 | return nil 49 | } 50 | 51 | func (rec *recorder) onStop(_ error) error { 52 | return rec.writer.Close() 53 | } 54 | 55 | func (rec *recorder) onEvent(name string, data interface{}) { 56 | rec.mu.Lock() 57 | defer rec.mu.Unlock() 58 | 59 | if name == configEvent { 60 | return 61 | } 62 | 63 | event := &recorderEnvelope{Name: name, Data: data} 64 | 65 | if err := rec.encoder.Encode(event); err != nil { 66 | rec.proc.logger.Warn(err) 67 | } 68 | } 69 | 70 | type recorderEnvelope struct { 71 | Name string `json:"event"` 72 | Data interface{} `json:"data"` 73 | } 74 | -------------------------------------------------------------------------------- /dashboard/registry_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/stretchr/testify/require" 13 | "go.k6.io/k6/metrics" 14 | ) 15 | 16 | func Test_newRegistry(t *testing.T) { 17 | t.Parallel() 18 | 19 | reg := newRegistry() 20 | 21 | require.NotNil(t, reg) 22 | require.NotNil(t, reg.Registry) 23 | require.NotNil(t, reg.names) 24 | } 25 | 26 | func Test_registry_getOrNew(t *testing.T) { 27 | t.Parallel() 28 | 29 | reg := newRegistry() 30 | 31 | met, err := reg.getOrNew("foo", metrics.Counter, metrics.Data, nil) 32 | 33 | require.NoError(t, err) 34 | require.NotNil(t, met) 35 | require.Equal(t, []string{"foo"}, reg.names) 36 | 37 | met2, err := reg.getOrNew("foo", metrics.Counter, metrics.Data, nil) 38 | 39 | require.NoError(t, err) 40 | require.NotNil(t, met) 41 | require.Same(t, met, met2) 42 | require.Equal(t, []string{"foo"}, reg.names) 43 | 44 | met3, err := reg.getOrNew("bar", metrics.Counter, metrics.Data, nil) 45 | 46 | require.NoError(t, err) 47 | require.NotNil(t, met3) 48 | require.Equal(t, []string{"foo", "bar"}, reg.names) 49 | 50 | _, err = reg.getOrNew("", metrics.Counter, metrics.Data, nil) 51 | 52 | require.Error(t, err) 53 | 54 | require.Panics(t, func() { reg.mustGetOrNew("", metrics.Counter, metrics.Data, nil) }) 55 | } 56 | -------------------------------------------------------------------------------- /dashboard/testdata/assets/packages/report/dist/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /dashboard/testdata/assets/packages/ui/dist/index.html: -------------------------------------------------------------------------------- 1 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /dashboard/testdata/customize/config-bad.json: -------------------------------------------------------------------------------- 1 | { 2 | "foo": bar 3 | } -------------------------------------------------------------------------------- /dashboard/testdata/customize/config-custom.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export default function (config) { 6 | function getById(id) { 7 | return this.filter( 8 | (/** @type {{ id: string; }} */ element) => element.id == id 9 | ).at(0); 10 | } 11 | 12 | Array.prototype["getById"] = getById; 13 | 14 | function durationPanel(suffix) { 15 | return { 16 | id: `http_req_duration_${suffix}`, 17 | title: `HTTP Request Duration ${suffix}`, 18 | metric: `http_req_duration_trend_${suffix}`, 19 | format: "duration", 20 | }; 21 | } 22 | 23 | const overview = config.tabs.getById("overview_snapshot"); 24 | 25 | const customPanels = [ 26 | overview.panels.getById("vus"), 27 | overview.panels.getById("http_reqs"), 28 | durationPanel("avg"), 29 | durationPanel("p(90)"), 30 | durationPanel("p(95)"), 31 | durationPanel("p(99)"), 32 | ]; 33 | 34 | const durationChart = Object.assign( 35 | {}, 36 | overview.charts.getById("http_req_duration") 37 | ); 38 | 39 | const customTab = { 40 | id: "custom", 41 | title: "Custom", 42 | event: overview.event, 43 | panels: customPanels, 44 | charts: [overview.charts.getById("http_reqs"), durationChart], 45 | description: "Example of customizing the display of metrics.", 46 | }; 47 | 48 | config.tabs.push(customTab); 49 | 50 | return config; 51 | } 52 | -------------------------------------------------------------------------------- /dashboard/testdata/result.json.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/dashboard/testdata/result.json.gz -------------------------------------------------------------------------------- /dashboard/testdata/result.ndjson.gz: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/dashboard/testdata/result.ndjson.gz -------------------------------------------------------------------------------- /dashboard/web_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | import ( 10 | "net/http" 11 | "testing" 12 | 13 | "github.com/stretchr/testify/require" 14 | ) 15 | 16 | func Test_newWebServer(t *testing.T) { 17 | t.Parallel() 18 | 19 | th := helper(t) 20 | 21 | srv := newWebServer(th.assets.ui, http.NotFoundHandler(), th.proc.logger) 22 | 23 | require.NotNil(t, srv) 24 | require.NotNil(t, srv.ServeMux) 25 | require.NotNil(t, srv.eventEmitter) 26 | 27 | addr, err := srv.listenAndServe("127.0.0.1:0") 28 | 29 | require.NoError(t, err) 30 | 31 | base := "http://" + addr.String() 32 | 33 | testLoc := func(loc string) { 34 | res, eerr := http.Get(base + loc) //nolint:bodyclose,noctx 35 | 36 | require.NoError(t, eerr) 37 | require.Equal(t, http.StatusOK, res.StatusCode) 38 | } 39 | 40 | testLoc("/ui/index.html") 41 | testLoc("/events") 42 | testLoc("/") 43 | 44 | res, err := http.Get(base + "/no_such_path") //nolint:bodyclose,noctx 45 | 46 | require.NoError(t, err) 47 | require.Equal(t, http.StatusNotFound, res.StatusCode) 48 | } 49 | 50 | func Test_webServer_used_addr(t *testing.T) { 51 | t.Parallel() 52 | 53 | th := helper(t) 54 | 55 | srv := newWebServer(th.assets.ui, http.NotFoundHandler(), th.proc.logger) 56 | 57 | addr, err := srv.listenAndServe("127.0.0.1:0") 58 | 59 | require.NoError(t, err) 60 | 61 | _, err = srv.listenAndServe(addr.String()) 62 | 63 | require.Error(t, err) 64 | } 65 | -------------------------------------------------------------------------------- /docs/.dashboard-custom.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export default function (config) { 6 | config.tabs.push({ 7 | title: "Custom", 8 | id: "custom", 9 | sections: [], 10 | }); 11 | return config; 12 | } 13 | -------------------------------------------------------------------------------- /magefiles/go.mod: -------------------------------------------------------------------------------- 1 | module magefiles 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/magefile/mage v1.15.0 7 | github.com/princjef/mageutil v1.0.0 8 | ) 9 | 10 | require ( 11 | github.com/VividCortex/ewma v1.1.1 // indirect 12 | github.com/cheggaaa/pb/v3 v3.0.4 // indirect 13 | github.com/fatih/color v1.9.0 // indirect 14 | github.com/mattn/go-colorable v0.1.4 // indirect 15 | github.com/mattn/go-isatty v0.0.12 // indirect 16 | github.com/mattn/go-runewidth v0.0.7 // indirect 17 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect 18 | ) 19 | -------------------------------------------------------------------------------- /magefiles/go.work: -------------------------------------------------------------------------------- 1 | go 1.21 2 | 3 | use ( 4 | . 5 | ./.. 6 | ) 7 | -------------------------------------------------------------------------------- /magefiles/go.work.sum: -------------------------------------------------------------------------------- 1 | github.com/cheggaaa/pb v2.0.7+incompatible h1:gLKifR1UkZ/kLkda5gC0K6c8g+jU2sINPtBeOiNlMhU= 2 | github.com/cpuguy83/go-md2man/v2 v2.0.1 h1:r/myEWzV9lfsM1tFLgDyu0atFtJ1fXn261LKYj/3DxU= 3 | github.com/mattn/go-isatty v0.0.17/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 4 | github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 5 | -------------------------------------------------------------------------------- /register.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2021 - 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | // Package dashboard contains the assembly and registration of the output extension. 8 | package dashboard 9 | 10 | import ( 11 | "github.com/grafana/xk6-dashboard/dashboard" 12 | "go.k6.io/k6/output" 13 | ) 14 | 15 | const outputName = "dashboard" 16 | 17 | func init() { 18 | output.RegisterExtension(outputName, dashboard.New) 19 | } 20 | -------------------------------------------------------------------------------- /register_test.go: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Iván Szkiba 2 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 3 | // 4 | // SPDX-License-Identifier: AGPL-3.0-only 5 | // SPDX-License-Identifier: MIT 6 | 7 | package dashboard 8 | 9 | import ( 10 | "testing" 11 | 12 | "github.com/grafana/xk6-dashboard/dashboard" 13 | "github.com/stretchr/testify/assert" 14 | "go.k6.io/k6/output" 15 | ) 16 | 17 | func TestRegister(t *testing.T) { 18 | t.Parallel() 19 | 20 | assert.Panics(t, func() { 21 | output.RegisterExtension(outputName, dashboard.New) 22 | }) // already registered 23 | } 24 | -------------------------------------------------------------------------------- /releases/template.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | xk6-dashboard `` is here 🎉! This release includes: 8 | 9 | - (_optional_) `` 10 | - `` (_one or multiple bullets_) 11 | 12 | 13 | ## Breaking changes 14 | 15 | - `#pr`, `` 16 | - `#pr`, `` 17 | 18 | ### (_optional h3_) `` `#pr` 19 | 20 | ## New features 21 | 22 | _optional intro here_ 23 | 24 | ### `` `#pr` 25 | 26 | _what, why, and what this means for the user_ 27 | 28 | ### `` `#pr` 29 | 30 | _what, why, and what this means for the user_ 31 | 32 | ### UX improvements and enhancements 33 | 34 | _Format as ` . `_: 35 | 36 | - _`#999` Gives terminal output prettier printing. Thanks to `@person` for the help!_ 37 | - `#pr` `` 38 | - `#pr` `` 39 | 40 | ## Bug fixes 41 | 42 | _Format as ` . `_: 43 | - _`#111` fixes race condition in runtime_ 44 | 45 | ## Maintenance and internal improvements 46 | 47 | _Format as ` . `_: 48 | - _`#2770` Refactors parts of the JS module._ 49 | 50 | ## _Optional_ Roadmap 51 | 52 | _Discussion of future plans_ 53 | -------------------------------------------------------------------------------- /releases/v0.6.1.md: -------------------------------------------------------------------------------- 1 | 6 | 7 | xk6-dashboard `v0.6.1` is here 🎉! This release includes: 8 | 9 | - [#77](https://github.com/grafana/xk6-dashboard/issues/77) Add missing charts to the *TIMINGS* tab sections 10 | - [#78](https://github.com/grafana/xk6-dashboard/issues/78) Rename WebSockets *Pong Duration* to *Ping Duration* 11 | - [#79](https://github.com/grafana/xk6-dashboard/issues/79) Enable short descriptions of graphs 12 | 13 | ### Add missing charts to the *TIMINGS* tab sections 14 | 15 | #### HTTP 16 | - Request Failed Rate 17 | - Request Rate 18 | - Request Blocked 19 | 20 | #### Browser 21 | - Request Failed Rate 22 | 23 | #### WebSockets 24 | - Transfer Rate 25 | - Sessions Rate 26 | 27 | #### gRPC 28 | - Transfer Rate 29 | - Streams Rate 30 | 31 | ### Rename WebSockets *Pong Duration* to *Ping Duration* 32 | 33 | The official name is "ping", so renameed it back to the official name (however the measured value is the response time) 34 | 35 | ### Enable short descriptions of graphs 36 | 37 | Added "summary" configuration property (and placeholder value) to panels configuration. 38 | -------------------------------------------------------------------------------- /releases/v0.7.0.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.0` is here 🎉! This release includes: 2 | 3 | - New design 4 | - New CLI tool 5 | - Preparation for k6 experimental module 6 | 7 | ## New design 8 | 9 | The new user interface design fits better with the k6 cloud user interface. The implementation of the UI has also changed, its size has decreased a bit, the code has become more structured and maintainable. 10 | 11 | ## New CLI tool 12 | 13 | Subcommands that do not require running k6 have been added to a dedicated CLI tool (`k6-web-dashboard`). The main reason for this is that soon the xk6-dashboard will be included in k6 as an experimental module and it will no longer be possible to use subcommands. The `aggregate`, `replay` and `report` commands will still be available with the help of the dedicated CLI. 14 | 15 | ## Preparation for k6 experimental module 16 | 17 | The xk6-dashboard will soon be integrated into k6 as an experimental module. Therefore, certain changes in the configuration management were necessary. For example, the names of the configuration environment variables were given the `K6_WEB_DASHBOARD_` prefix. The name of the output extension also changed from `dashboard` to `web-dashboard`. 18 | 19 | 20 | -------------------------------------------------------------------------------- /releases/v0.7.1.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.1` is here 🎉! This release includes: 2 | 3 | - fix: [#142](https://github.com/grafana/xk6-dashboard/issues/142) k6 does not stop in case of an abortion 4 | -------------------------------------------------------------------------------- /releases/v0.7.2.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.2` is here 🎉! This release includes: 2 | 3 | - sync'd the dependencies with k6, this fix: [grafana/k6#3556](https://github.com/grafana/k6/issues/3556) 4 | 5 | -------------------------------------------------------------------------------- /releases/v0.7.4.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.4` is here 🎉! This is an internal maintenance release. 2 | (no bug fixes, no new features) 3 | 4 | -------------------------------------------------------------------------------- /releases/v0.7.5.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.5` is here 🎉! This is an internal maintenance release. 2 | (no bug fixes, no new features) 3 | 4 | ## Breaking changes 5 | 6 | ### Configuration customization from JavaScript code has been removed 7 | 8 | Until now, the dashboard configuration could be customized with a JavaScript code. Supporting this feature after switching the JavaScript interpreter (goja to sobek) causes serious difficulties. Since this feature is rarely used, it is easier to drop it than to support it. 9 | -------------------------------------------------------------------------------- /releases/v0.7.6.md: -------------------------------------------------------------------------------- 1 | xk6-dashboard `v0.7.6` is here 🎉! 2 | 3 | This is a maintenance release, changes: 4 | 5 | - Updated k6 to v1.0.0 6 | - Use shared GitHub workflows from xk6 7 | - Updated tooling dependencies 8 | - `go`: 1.24 9 | - `golangci-lint`: 2.1.6 10 | - `xk6`: 0.19.2 11 | - `gosec`: 2.22.4 12 | - `govulncheck`: 1.1.4 13 | - `goreleaser`: 2.9.0 14 | - Added support for [Development Containers](https://containers.dev/) 15 | - Generate Makefile from `CONTRIBUTING.md` 16 | -------------------------------------------------------------------------------- /screenshot/k6-dashboard-cumulative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-cumulative.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-custom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-custom.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-html-report-screen-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-html-report-screen-view.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-html-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-html-report.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-overview-cumulative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-overview-cumulative.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-overview-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-overview-dark.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-overview-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-overview-light.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-overview-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-overview-snapshot.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-report.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-report.pdf -------------------------------------------------------------------------------- /screenshot/k6-dashboard-report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-report.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-snapshot.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-summary-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-summary-dark.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-summary-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-summary-light.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-summary.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-timings-cumulative.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-timings-cumulative.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-timings-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-timings-dark.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-timings-light.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-timings-light.png -------------------------------------------------------------------------------- /screenshot/k6-dashboard-timings-snapshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/grafana/xk6-dashboard/6e18075db998aacce1fd4550c67d8f0519e52986/screenshot/k6-dashboard-timings-snapshot.png -------------------------------------------------------------------------------- /scripts/demo/demo-browser.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { browser } from "k6/experimental/browser"; 6 | 7 | export const options = { 8 | scenarios: { 9 | browser: { 10 | executor: "ramping-vus", 11 | options: { 12 | browser: { 13 | type: "chromium", 14 | }, 15 | }, 16 | startVUs: 1, 17 | stages: [ 18 | { duration: "30s", target: 1 }, 19 | { duration: "3m", target: 2 }, 20 | { duration: "2m", target: 2 }, 21 | { duration: "3m", target: 1 }, 22 | { duration: "2m", target: 2 }, 23 | { duration: "1m", target: 1 }, 24 | ], 25 | gracefulRampDown: "30s", 26 | }, 27 | }, 28 | thresholds: { 29 | browser_http_req_duration: ["p(90) < 500"], 30 | }, 31 | }; 32 | 33 | export default async function () { 34 | const page = browser.newPage(); 35 | 36 | try { 37 | await page.goto("https://test.k6.io/"); 38 | 39 | let link = page.locator('a[href="/contacts.php"]'); 40 | 41 | await Promise.all([page.waitForNavigation(), link.click()]); 42 | 43 | let back = page.locator('a[href="/"]'); 44 | 45 | await Promise.all([page.waitForNavigation(), back.click()]); 46 | 47 | link = page.locator('a[href="/news.php"]'); 48 | 49 | await Promise.all([page.waitForNavigation(), link.click()]); 50 | 51 | back = page.locator('a[href="/"]'); 52 | 53 | await Promise.all([page.waitForNavigation(), back.click()]); 54 | } finally { 55 | page.close(); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /scripts/demo/demo-http.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import http from "k6/http"; 6 | import { check, sleep, group } from "k6"; 7 | import smurfs from "./smurfs.js"; 8 | 9 | export let options = { 10 | scenarios: { 11 | http: { 12 | executor: "ramping-vus", 13 | startVUs: 1, 14 | stages: [ 15 | { duration: "30s", target: 2 }, 16 | { duration: "3m", target: 5 }, 17 | { duration: "2m", target: 2 }, 18 | { duration: "3m", target: 5 }, 19 | { duration: "2m", target: 3 }, 20 | { duration: "1m", target: 1 }, 21 | ], 22 | gracefulRampDown: "30s", 23 | }, 24 | }, 25 | thresholds: { 26 | http_req_duration: ["p(95) < 400", "p(90) < 500", "avg < 200"], 27 | }, 28 | }; 29 | 30 | export default function () { 31 | const base = "https://httpbin.test.k6.io/"; 32 | 33 | let response = http.get(`${base}status/200,200,200,404`); 34 | 35 | check(response, { 36 | "status is OK": (r) => r && r.status === 200, 37 | }); 38 | 39 | smurfs.forEach((smurf) => { 40 | response = http.post(`${base}/post`, JSON.stringify({ smurf }), { 41 | headers: { "Content-Type": "application/json" }, 42 | }); 43 | 44 | check(response, { 45 | "status is OK": (r) => r && r.status === 200, 46 | }); 47 | 48 | sleep(0.2); 49 | }); 50 | } 51 | -------------------------------------------------------------------------------- /scripts/demo/demo-rest.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import http from "k6/http"; 6 | import { check, sleep, group } from "k6"; 7 | 8 | export let options = { 9 | scenarios: { 10 | rest: { 11 | executor: "ramping-vus", 12 | startVUs: 1, 13 | stages: [ 14 | { duration: "30s", target: 2 }, 15 | { duration: "3m", target: 5 }, 16 | { duration: "2m", target: 2 }, 17 | { duration: "3m", target: 5 }, 18 | { duration: "2m", target: 3 }, 19 | { duration: "1m", target: 1 }, 20 | ], 21 | gracefulRampDown: "30s", 22 | }, 23 | }, 24 | thresholds: { 25 | http_req_duration: ["p(90) < 400"], 26 | }, 27 | }; 28 | 29 | export function setup() {} // to have "setup" group, which only has metric value at the begining of the test run 30 | 31 | export default function () { 32 | let crocodiles, ok; 33 | 34 | group("list crocodiles", () => { 35 | const response = http.get("https://test-api.k6.io/public/crocodiles/"); 36 | 37 | ok = check(response, { 38 | "status is OK": (r) => r && r.status === 200, 39 | }); 40 | 41 | if (ok) { 42 | crocodiles = response.json(); 43 | } 44 | }); 45 | 46 | if (!ok) { 47 | return; 48 | } 49 | 50 | group("get crocodile", () => { 51 | for (var i = 0; i < crocodiles.length; i++) { 52 | let response = http.get(http.url`https://test-api.k6.io/public/crocodiles/${crocodiles[i].id}`); 53 | 54 | check(response, { 55 | "status is OK": (r) => r && r.status == 200, 56 | }); 57 | 58 | sleep(0.2); 59 | } 60 | }); 61 | } 62 | -------------------------------------------------------------------------------- /scripts/demo/demo-ws.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import { WebSocket } from "k6/experimental/websockets"; 6 | import { check, sleep } from "k6"; 7 | import smurfs from "./smurfs.js"; 8 | 9 | export let options = { 10 | scenarios: { 11 | ws: { 12 | executor: "ramping-vus", 13 | startVUs: 1, 14 | stages: [ 15 | { duration: "30s", target: 2 }, 16 | { duration: "3m", target: 5 }, 17 | { duration: "2m", target: 2 }, 18 | { duration: "3m", target: 5 }, 19 | { duration: "2m", target: 3 }, 20 | { duration: "1m", target: 1 }, 21 | ], 22 | gracefulRampDown: "30s", 23 | }, 24 | }, 25 | thresholds: { 26 | ws_ping: ["p(90) < 1000", "avg < 500"], 27 | }, 28 | }; 29 | 30 | export default async function () { 31 | const ws = new WebSocket("wss://test-api.k6.io/ws/crocochat/dashboard/"); 32 | 33 | let count = 0; 34 | let prom = new Promise((resolve, reject) => { 35 | ws.onmessage = (msg) => { 36 | ws.ping(); 37 | let data = JSON.parse(msg.data); 38 | if (data.event == "CHAT_MSG") { 39 | if (++count == smurfs.length) { 40 | ws.close(); 41 | resolve(); 42 | } 43 | } 44 | }; 45 | 46 | ws.onerror = (err) => { 47 | reject(err); 48 | }; 49 | }); 50 | 51 | ws.onopen = () => { 52 | ws.ping(); 53 | smurfs.forEach((smurf) => { 54 | sleep(0.2); 55 | ws.send(`{"event":"SAY","message":"Hello ${smurf}"}`); 56 | }); 57 | }; 58 | 59 | try { 60 | await prom; 61 | } catch (_) {} 62 | 63 | check(count, { 64 | "smurf greetings OK": (c) => c == smurfs.length, 65 | }); 66 | } 67 | -------------------------------------------------------------------------------- /scripts/demo/demo.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import grpcFn, { options as grpcO } from "./demo-grpc.js"; 6 | import wsFn, { options as wsO } from "./demo-ws.js"; 7 | import browserFn, { options as browserO } from "./demo-browser.js"; 8 | import restFn, { options as restO } from "./demo-rest.js"; 9 | import httpFn, { options as httpO } from "./demo-http.js"; 10 | 11 | export { grpcFn, wsFn, browserFn, restFn, httpFn }; 12 | 13 | export let options = { 14 | thresholds: { 15 | checks: ["rate > 0.8"], 16 | }, 17 | }; 18 | 19 | function init() { 20 | const ws = wsO.scenarios.ws; 21 | const grpc = grpcO.scenarios.grpc; 22 | const browser = browserO.scenarios.browser; 23 | const rest = restO.scenarios.rest; 24 | const http = httpO.scenarios.http; 25 | ws.exec = "wsFn"; 26 | grpc.exec = "grpcFn"; 27 | browser.exec = "browserFn"; 28 | rest.exec = "restFn"; 29 | http.exec = "httpFn"; 30 | 31 | options.scenarios = { ws, grpc, browser, rest, http }; 32 | 33 | Object.assign(options.thresholds, wsO.thresholds); 34 | Object.assign(options.thresholds, grpcO.thresholds); 35 | Object.assign(options.thresholds, browserO.thresholds); 36 | Object.assign(options.thresholds, restO.thresholds); 37 | Object.assign(options.thresholds, httpO.thresholds); 38 | } 39 | 40 | init(); 41 | -------------------------------------------------------------------------------- /scripts/demo/hello.proto: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | syntax = "proto2"; 6 | 7 | package hello; 8 | 9 | service HelloService { 10 | rpc SayHello(HelloRequest) returns (HelloResponse); 11 | rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); 12 | rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); 13 | rpc BidiHello(stream HelloRequest) returns (stream HelloResponse); 14 | } 15 | 16 | message HelloRequest { 17 | optional string greeting = 1; 18 | } 19 | 20 | message HelloResponse { 21 | required string reply = 1; 22 | } 23 | -------------------------------------------------------------------------------- /scripts/demo/smurfs.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | export default [ 6 | "Grumphy", 7 | "Doc", 8 | "Bashful", 9 | "Dopey", 10 | "Sneezy", 11 | "Sleepy", 12 | "Happy", 13 | ]; 14 | -------------------------------------------------------------------------------- /scripts/test.js: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Raintank, Inc. dba Grafana Labs 2 | // 3 | // SPDX-License-Identifier: AGPL-3.0-only 4 | 5 | import http from "k6/http"; 6 | 7 | export let options = { 8 | discardResponseBodies: true, 9 | scenarios: { 10 | contacts: { 11 | executor: "constant-vus", 12 | vus: 2, 13 | duration: "20s", 14 | }, 15 | }, 16 | }; 17 | 18 | export default function () { 19 | http.get("http://test.k6.io"); 20 | } 21 | --------------------------------------------------------------------------------