├── .editorconfig ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── chore.md │ └── feature_request.md ├── pull_request_template.md └── workflows │ ├── cd-prerelease.yml │ ├── ci.yml │ ├── codeql.yml │ ├── imgcmp.yml │ ├── publish-gh-pages.yml │ └── publish.yml ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc ├── .vscode └── launch.json ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── DEVELOPING.md ├── LICENSE.txt ├── README.md ├── data ├── README.md └── fflib.log ├── eslint.config.mjs ├── jest.config.js ├── lana-docs-site ├── .gitignore ├── README.md ├── docs │ ├── community │ │ ├── changelog.mdx │ │ ├── contributing.mdx │ │ └── support.md │ └── docs │ │ ├── features.md │ │ ├── gettingstarted.md │ │ ├── installation.md │ │ ├── intro.md │ │ └── settings.md ├── docusaurus.config.ts ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── favicon.svg │ │ ├── lana-timeline.png │ │ ├── logo-dark.svg │ │ └── logo.svg └── tsconfig.json ├── lana ├── .gitignore ├── .vscode │ ├── extensions.json │ ├── launch.json │ ├── settings.json │ └── tasks.json ├── .vscodeignore ├── certinia-icon-color.png ├── dist │ ├── images │ │ ├── install-lana.webp │ │ ├── lana-analysis-find.png │ │ ├── lana-analysis.png │ │ ├── lana-calltree-find.png │ │ ├── lana-calltree.png │ │ ├── lana-database.png │ │ ├── lana-preview.gif │ │ ├── lana-showanalysis-lens.webp │ │ ├── lana-timeline-find.png │ │ ├── lana-timeline.png │ │ ├── lana-tooltip.webp │ │ └── settings-color-lana.webp │ ├── v1.10 │ │ ├── install-lana.webp │ │ ├── lana-analysis.png │ │ ├── lana-calltree.png │ │ ├── lana-database.png │ │ ├── lana-preview.gif │ │ ├── lana-showanalysis-lens.webp │ │ ├── lana-timeline.png │ │ ├── lana-tooltip.webp │ │ └── settings-color-lana.webp │ ├── v1.12 │ │ ├── lana-analysis.png │ │ ├── lana-calltree.png │ │ └── lana-preview.gif │ └── v1.14 │ │ ├── lana-analysis.png │ │ ├── lana-calltree.png │ │ ├── lana-database.png │ │ └── lana-preview.gif ├── package.json ├── rollup.config.mjs ├── src │ ├── AppSettings.ts │ ├── Context.ts │ ├── Main.ts │ ├── codelenses │ │ └── ShowAnalysisCodeLens.ts │ ├── commands │ │ ├── Command.ts │ │ ├── LogView.ts │ │ ├── RetrieveLogFile.ts │ │ └── ShowLogAnalysis.ts │ ├── display │ │ ├── Display.ts │ │ ├── OpenFileInPackage.ts │ │ ├── QuickPick.ts │ │ ├── QuickPickWorkspace.ts │ │ ├── WebView.ts │ │ └── WhatsNewNotification.ts │ ├── salesforce │ │ ├── codesymbol │ │ │ └── SymbolFinder.ts │ │ └── logs │ │ │ ├── GetLogFile.ts │ │ │ └── GetLogFiles.ts │ └── workspace │ │ └── VSWorkspace.ts ├── tsconfig-dev.json └── tsconfig.json ├── log-viewer ├── .gitignore ├── .vscode │ └── launch.json ├── declarations.d.ts ├── index.html ├── modules │ ├── Database.ts │ ├── Main.ts │ ├── Util.ts │ ├── __tests__ │ │ ├── ApexLogParser.test.ts │ │ ├── Database.test.ts │ │ ├── SOQLParser.test.ts │ │ ├── Util.test.ts │ │ └── soql │ │ │ └── SOQLLinter.test.ts │ ├── components │ │ ├── AppHeader.ts │ │ ├── BadgeBase.ts │ │ ├── CallStack.ts │ │ ├── LogLevels.ts │ │ ├── LogTitle.ts │ │ ├── LogViewer.ts │ │ ├── NavBar.ts │ │ ├── SOQLLinterIssues.ts │ │ ├── analysis-view │ │ │ ├── AnalysisView.ts │ │ │ └── column-calcs │ │ │ │ └── CallStackSum.ts │ │ ├── calltree-view │ │ │ ├── CalltreeView.ts │ │ │ └── module │ │ │ │ ├── Find.ts │ │ │ │ └── MiddleRowFocus.ts │ │ ├── database-view │ │ │ ├── DMLView.ts │ │ │ ├── DatabaseSOQLDetailPanel.ts │ │ │ ├── DatabaseSection.ts │ │ │ ├── DatabaseView.scss │ │ │ ├── DatabaseView.ts │ │ │ └── SOQLView.ts │ │ ├── datagrid │ │ │ └── datagrid-filter-bar.ts │ │ ├── find-widget │ │ │ └── FindWidget.ts │ │ ├── notifications │ │ │ ├── NotificationButton.ts │ │ │ ├── NotificationPanel.ts │ │ │ └── NotificationTag.ts │ │ └── skeleton │ │ │ ├── GridSkeleton.ts │ │ │ └── skeleton.styles.ts │ ├── datagrid │ │ ├── dataaccessor │ │ │ └── Number.ts │ │ ├── editors │ │ │ ├── MinMax.css │ │ │ └── MinMax.ts │ │ ├── filters │ │ │ └── MinMax.ts │ │ ├── format │ │ │ ├── Number.ts │ │ │ ├── Progress.css │ │ │ ├── Progress.ts │ │ │ ├── ProgressComponent.ts │ │ │ └── ProgressMS.ts │ │ ├── groups │ │ │ ├── GroupCalcs.ts │ │ │ └── GroupSort.ts │ │ ├── module │ │ │ ├── CommonModules.ts │ │ │ ├── RowKeyboardNavigation.ts │ │ │ └── RowNavigation.ts │ │ └── style │ │ │ └── DataGrid.scss │ ├── parsers │ │ └── ApexLogParser.ts │ ├── services │ │ └── VSCodeExtensionMessenger.ts │ ├── soql │ │ ├── SOQLLinter.ts │ │ └── SOQLParser.ts │ ├── styles │ │ ├── codicon.css │ │ ├── global.styles.ts │ │ └── notification.styles.ts │ ├── timeline │ │ ├── Timeline.ts │ │ ├── TimelineKey.ts │ │ └── TimelineView.ts │ └── vscode-ui │ │ └── VsIconCheckbox.ts ├── package.json ├── rollup.config.mjs ├── tsconfig-dev.json └── tsconfig.json ├── package.json ├── patches └── @salesforce__bunyan@2.0.0.patch ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── rollup.config.mjs └── scripts └── pre-release.js /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.{htm,html}] 12 | indent_size = 2 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🐛 Bug Report 3 | about: Found a bug? Report it so we can get it fixed 4 | title: '🐛 bug: ' 5 | type: 'Bug' 6 | labels: ['bug', 'needs-triage'] 7 | assignees: '' 8 | --- 9 | 10 | ## 🐛 Bug Report 11 | 12 | Thanks for taking the time to help us improve! Please fill out the details below so we can reproduce and resolve the issue quickly. 13 | 14 | NOTE: 15 | 16 | - This is a community project. We will triage your bug as soon as we can. 17 | - Before raising a new bug, please check our [issue list](https://github.com/certinia/debug-log-analyzer/issues) to see if it has already been reported. 18 | - For general help, use [discussions](https://github.com/certinia/debug-log-analyzer/discussions) or [Salesforce Stack Exchange](https://salesforce.stackexchange.com/search?q=apex+log+analyzer). 19 | 20 | ### 💡 Description 21 | 22 | _Clearly describe what the bug is._ 23 | 24 | > Example: “When I open the Analysis for a local debug log file, the timeline fails to appear, but it used to work in v1.1.0.” 25 | 26 | ### 🧠 Expected Behavior 27 | 28 | _What did you expect to happen?_ 29 | 30 | ### 🤯 Actual Behavior 31 | 32 | _What actually happened instead?_ 33 | 34 | ### 🔢 Steps to Reproduce 35 | 36 | 🔁 **How can we reproduce the bug?** 37 | _Include the minimal steps needed to trigger the issue. The more concise and clear, the better!_ 38 | 39 | > 1. Open the log analysis for a file. 40 | > 2. Go to the analysis view. 41 | > 3. Sort any column. 42 | > 4. Sorting does not work. 43 | 44 | ### 📎 Screenshots, stack traces or debug log files 45 | 46 | 📄 _Attach any relevant files that can help us diagnose the issue. 47 | If possible, please attach or paste a redacted Apex debug log that reproduces the issue. You can drag and drop the file into this issue._ 48 | 49 | ### 🧪 Environment 50 | 51 | _Provide the following details to help us replicate the environment where the bug occurs:_ 52 | 53 | **VS Code Version**: 54 | 55 | **Log Analyzer Extension Version**: 56 | 57 | **OS and version**: 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/chore.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: 🔧 Chore 3 | about: Tasks that aren't features or bugs 4 | title: '🔧 chore: <title>' 5 | type: 'Task' 6 | labels: ['chore'] 7 | assignees: '' 8 | --- 9 | 10 | ### 🔧 Chore Description 11 | 12 | _What needs to be done?_ 13 | 14 | > Example: “Update Prettier to latest version.” 15 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: ✨ Feature Request 3 | about: Suggest a feature or improvement 4 | title: '✨ feat: <title>' 5 | type: 'Feature' 6 | labels: ['enhancement', 'needs-triage'] 7 | assignees: '' 8 | --- 9 | 10 | ## 🔍 Before You Start 11 | 12 | Search [existing issues](https://github.com/certinia/debug-log-analyzer/issues) to check if this feature has already been suggested. 13 | 14 | > If your idea is new — awesome! Continue below 👇 15 | 16 | ## ✨ Feature Request 17 | 18 | _What's the feature you'd like to see? Give us a high-level overview._ 19 | 20 | > _Example: "Add a timeline view to visualize method execution over time."_ 21 | 22 | ### 🧩 Problem This Solves 23 | 24 | _What problem, limitation or pain point does this solve?_ 25 | 26 | ### 🧠 Proposed Solution 27 | 28 | _How would you like this to work or look? Even a rough idea helps._ 29 | _Describe the user experience, potential UI, expected behavior, or technical ideas._ 30 | 31 | ### 🔄 Alternatives 32 | 33 | _List any alternative solutions you've considered._ 34 | 35 | ### 📎 Additional context 36 | 37 | _Add any other context or screenshots about the feature request._ 38 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # 📝 PR Overview 2 | 3 | _Briefly describe **what** the pull request does and **why** it is needed._ 4 | 5 | > Example: "Adds a flame chart view for CPU time analysis in Apex logs to improve log analysis performance." 6 | 7 | ## 🛠️ Changes made 8 | 9 | - Change 1 10 | - Change 2 11 | 12 | ## 🧩 Type of change (check all applicable) 13 | 14 | - [ ] 🐛 Bug fix - something not working as expected 15 | - [ ] ✨ New feature – adds new functionality 16 | - [ ] ♻️ Refactor - internal changes with no user impact 17 | - [ ] ⚡ Performance Improvement 18 | - [ ] 📝 Documentation - README or documentation site changes 19 | - [ ] 🔧 Chore - dev tooling, CI, config 20 | - [ ] 💥 Breaking change 21 | 22 | ## 📷 Screenshots / gifs / video [optional] 23 | 24 | _Show off your UI changes._ 25 | 26 | ## 🔗 Related Issues 27 | 28 | fixes # 29 | resolves # 30 | closes # 31 | related # 32 | 33 | ## ✅ Tests added? 34 | 35 | - [ ] 👍 yes 36 | - [ ] 🙅 no, not needed 37 | - [ ] 🙋 no, I need help 38 | 39 | ## 📚 Docs updated? 40 | 41 | - [ ] 🔖 README.md 42 | - [ ] 🔖 CHANGELOG.md 43 | - [ ] 📖 help site 44 | - [ ] 🙅 not needed 45 | 46 | ## Anything else we need to know? [optional] 47 | -------------------------------------------------------------------------------- /.github/workflows/cd-prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Publish pre-release 2 | 3 | on: 4 | schedule: 5 | - cron: '15 4 * * 2' # every tuesday at 4:15AM UTC 6 | workflow_dispatch: 7 | 8 | jobs: 9 | check: 10 | name: Check for pre release changes 11 | runs-on: ubuntu-latest 12 | if: github.repository_owner == 'certinia' 13 | permissions: 14 | contents: write 15 | outputs: 16 | exitstatus: ${{ steps.earlyexit.outputs.exitstatus }} 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - id: earlyexit 24 | name: Should publish pre release 25 | run: | 26 | echo "Checking if Log Analyzer Pre Release can be published" 27 | headSHA=$(git rev-parse HEAD) 28 | if [ $(git tag -l pre) ]; then 29 | preSHA=$(git rev-parse --verify -q "pre") 30 | fi 31 | stableTag=$(git tag '*.*.*' --list --sort=-version:refname | head -n1 ) 32 | stableSHA=$(git rev-parse $stableTag) 33 | 34 | echo "HEAD 35 | $(git show $headSHA -s --format="commit: %h Date: %ad") 36 | 37 | Pre release 38 | $(git show $preSHA -s --format="commit: %h Date: %ad") 39 | 40 | Stable release ($stableTag) 41 | $(git show $stableSHA -s --format="commit: %h Date: %ad")" 42 | 43 | if [ "$headSHA" = "$preSHA" ] || [ "$headSHA" = "$stableSHA" ]; then 44 | echo " 45 | No pre-release needed, No changes since last pre-release or stable version. Exiting." 46 | echo "exitstatus=exit" >> "$GITHUB_OUTPUT" 47 | exit 0 48 | fi 49 | echo "exitstatus=continue" >> "$GITHUB_OUTPUT" 50 | 51 | publish: 52 | name: Publish pre-release 53 | needs: check 54 | runs-on: ubuntu-latest 55 | if: github.repository_owner == 'certinia' && needs.check.outputs.exitstatus == 'continue' 56 | steps: 57 | - name: Checkout code 58 | uses: actions/checkout@v4 59 | - name: Setup pnpm 60 | uses: pnpm/action-setup@v3 61 | with: 62 | version: 8 63 | - name: Set up Node 64 | uses: actions/setup-node@v4 65 | with: 66 | node-version: '20' 67 | cache: 'pnpm' 68 | - name: Install vsce + ovsx 69 | run: | 70 | pnpm add --global @vscode/vsce 71 | pnpm add --global ovsx 72 | - name: Dependencies 73 | run: | 74 | HUSKY=0 pnpm install 75 | - name: update pre-release version 76 | run: | 77 | echo "Updating pre-release version" 78 | pnpm run bump-prerelease; 79 | - name: Package the extension 80 | run: | 81 | cd lana 82 | vsce package --pre-release --no-dependencies 83 | - name: Publish to VS Code Marketplace + Open VSX Registry 84 | run: | 85 | cd lana 86 | echo "Verify vsce token has not expired" 87 | vsce verify-pat -p ${{ secrets.VSCE_TOKEN }} 88 | 89 | echo " 90 | Verify ovsx token has not expired" 91 | ovsx verify-pat -p ${{ secrets.OVSX_TOKEN }} 92 | 93 | versionNum=$(cat package.json | jq -r '.version') 94 | pkgPath=lana-${versionNum}.vsix 95 | 96 | echo "Publish to vsce 97 | vsix name: $pkgPath" 98 | vsce publish --packagePath ${pkgPath} --no-dependencies --pre-release --skip-duplicate -p ${{ secrets.VSCE_TOKEN }} 99 | 100 | echo " 101 | Publish to ovsx" 102 | ovsx publish ${pkgPath} --no-dependencies --pre-release --skip-duplicate -p ${{ secrets.OVSX_TOKEN }} 103 | - name: Update pre-release tag 104 | run: | 105 | echo "Updating pre release tag" 106 | git tag -f pre 107 | git push -f origin pre 108 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main, release/**] 6 | pull_request: 7 | branches: [main, release/**] 8 | workflow_dispatch: 9 | 10 | jobs: 11 | verify_files: 12 | name: Verify Files 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: pnpm/action-setup@v3 17 | with: 18 | version: 8 19 | - name: Set up Node 20 | uses: actions/setup-node@v4 21 | with: 22 | node-version: '20' 23 | cache: 'pnpm' 24 | - name: Install Packages 25 | run: HUSKY=0 pnpm install 26 | - name: Lint Files 27 | run: pnpm run lint 28 | 29 | tests: 30 | name: Run Log-viewer Tests 31 | runs-on: ubuntu-latest 32 | steps: 33 | - uses: actions/checkout@v4 34 | - uses: pnpm/action-setup@v3 35 | with: 36 | version: 8 37 | - name: Set up Node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: '20' 41 | cache: 'pnpm' 42 | - name: Install Packages 43 | run: HUSKY=0 pnpm install 44 | - name: Tests 45 | run: | 46 | pnpm run test:ci 47 | 48 | build: 49 | name: Verify VSCode Package Build 50 | runs-on: ubuntu-latest 51 | steps: 52 | - uses: actions/checkout@v4 53 | - uses: pnpm/action-setup@v3 54 | with: 55 | version: 8 56 | - name: Set up Node 57 | uses: actions/setup-node@v4 58 | with: 59 | node-version: '20' 60 | cache: 'pnpm' 61 | - name: Install vsce 62 | run: pnpm add --global @vscode/vsce 63 | - name: Install Dependencies 64 | run: | 65 | HUSKY=0 pnpm install 66 | - name: Build VSCode Package 67 | run: | 68 | cd lana 69 | vsce package --no-dependencies 70 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | paths-ignore: 18 | - '**/*.md' 19 | - '**/*.txt' 20 | pull_request: 21 | branches: [ "main" ] 22 | paths-ignore: 23 | - '**/*.md' 24 | - '**/*.txt' 25 | schedule: 26 | - cron: '26 5 * * 3' 27 | 28 | jobs: 29 | analyze: 30 | name: Analyze 31 | # Runner size impacts CodeQL analysis time. To learn more, please see: 32 | # - https://gh.io/recommended-hardware-resources-for-running-codeql 33 | # - https://gh.io/supported-runners-and-hardware-resources 34 | # - https://gh.io/using-larger-runners 35 | # Consider using larger runners for possible analysis time improvements. 36 | runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} 37 | timeout-minutes: ${{ (matrix.language == 'swift' && 120) || 360 }} 38 | permissions: 39 | # required for all workflows 40 | security-events: write 41 | 42 | # only required for workflows in private repositories 43 | actions: read 44 | contents: read 45 | 46 | strategy: 47 | fail-fast: false 48 | matrix: 49 | language: [ 'javascript-typescript' ] 50 | # CodeQL supports [ 'c-cpp', 'csharp', 'go', 'java-kotlin', 'javascript-typescript', 'python', 'ruby', 'swift' ] 51 | # Use only 'java-kotlin' to analyze code written in Java, Kotlin or both 52 | # Use only 'javascript-typescript' to analyze code written in JavaScript, TypeScript or both 53 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 54 | 55 | steps: 56 | - name: Checkout repository 57 | uses: actions/checkout@v4 58 | 59 | # Initializes the CodeQL tools for scanning. 60 | - name: Initialize CodeQL 61 | uses: github/codeql-action/init@v3 62 | with: 63 | languages: ${{ matrix.language }} 64 | # If you wish to specify custom queries, you can do so here or in a config file. 65 | # By default, queries listed here will override any specified in a config file. 66 | # Prefix the list here with "+" to use these queries and those in the config file. 67 | 68 | # For more details on CodeQL's query packs, refer to: https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 69 | # queries: security-extended,security-and-quality 70 | 71 | 72 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, Java, or Swift). 73 | # If this step fails, then you should remove it and run the build manually (see below) 74 | - name: Autobuild 75 | uses: github/codeql-action/autobuild@v3 76 | 77 | # ℹ️ Command-line programs to run using the OS shell. 78 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 79 | 80 | # If the Autobuild fails above, remove it and uncomment the following three lines. 81 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 82 | 83 | # - run: | 84 | # echo "Run, Build Application using script" 85 | # ./location_of_script_within_repo/buildscript.sh 86 | 87 | - name: Perform CodeQL Analysis 88 | uses: github/codeql-action/analyze@v3 89 | with: 90 | category: "/language:${{matrix.language}}" 91 | -------------------------------------------------------------------------------- /.github/workflows/imgcmp.yml: -------------------------------------------------------------------------------- 1 | name: Optimize Images 2 | on: 3 | push: 4 | branches: 5 | - main 6 | jobs: 7 | imgcmp: 8 | runs-on: ubuntu-latest 9 | if: github.repository_owner == 'certinia' 10 | permissions: 11 | contents: write 12 | pull-requests: write 13 | steps: 14 | - uses: 9sako6/imgcmp@v2.0.4 15 | with: 16 | token: ${{ secrets.GITHUB_TOKEN }} 17 | paths-ignore-regexp: 'node_modules/.*' 18 | -------------------------------------------------------------------------------- /.github/workflows/publish-gh-pages.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a Jekyll site to GitHub Pages 2 | name: Publish GitHub Pages Site 3 | 4 | on: 5 | # Runs after the publish suceeds 6 | workflow_run: 7 | workflows: ['Publish Stable'] 8 | types: [completed] 9 | 10 | # Allows you to run this workflow manually from the Actions tab 11 | workflow_dispatch: 12 | 13 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 14 | permissions: 15 | contents: read 16 | pages: write 17 | id-token: write 18 | 19 | # Allow one concurrent deployment 20 | concurrency: 21 | group: 'pages' 22 | cancel-in-progress: true 23 | 24 | jobs: 25 | # Build job 26 | build: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - name: Checkout 30 | uses: actions/checkout@v4 31 | # 👇 Build steps 32 | - name: pnpm setup 33 | uses: pnpm/action-setup@v3 34 | with: 35 | version: 8 36 | - name: Set up Node 37 | uses: actions/setup-node@v4 38 | with: 39 | node-version: '20' 40 | cache: 'pnpm' 41 | - name: Dependencies 42 | run: | 43 | HUSKY=0 pnpm install 44 | - name: Build Site 45 | run: | 46 | cd lana-docs-site 47 | pnpm run build 48 | # 👆 Build steps 49 | - name: Setup Pages 50 | uses: actions/configure-pages@v4 51 | - name: Upload artifact 52 | uses: actions/upload-pages-artifact@v3 53 | with: 54 | path: ./lana-docs-site/build 55 | 56 | # Deployment job 57 | deploy: 58 | environment: 59 | name: github-pages 60 | url: ${{ steps.deployment.outputs.page_url }} 61 | runs-on: ubuntu-latest 62 | needs: build 63 | steps: 64 | - name: Deploy to GitHub Pages 65 | id: deployment 66 | uses: actions/deploy-pages@v4 67 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Stable 2 | 3 | on: 4 | release: 5 | types: [published] 6 | 7 | jobs: 8 | build: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - uses: actions/checkout@v4 13 | - uses: pnpm/action-setup@v3 14 | with: 15 | version: 8 16 | - name: Set up Node 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: '20' 20 | cache: 'pnpm' 21 | - name: Install vsce + ovsx 22 | run: | 23 | pnpm add --global @vscode/vsce 24 | pnpm add --global ovsx 25 | - name: Dependencies 26 | run: | 27 | HUSKY=0 pnpm install 28 | - name: Build extension 29 | run: | 30 | cd lana 31 | vsce package --no-dependencies 32 | - name: Publish to VS Code Marketplace + Open VSX Registry 33 | run: | 34 | cd lana 35 | echo "Verify vsce token has not expired" 36 | vsce verify-pat -p ${{ secrets.VSCE_TOKEN }} 37 | echo "Verify ovsx token has not expired" 38 | ovsx verify-pat -p ${{ secrets.OVSX_TOKEN }} 39 | 40 | echo "Publish to vsce" 41 | vsce publish --packagePath lana-${{ github.event.release.tag_name }}.vsix --no-dependencies -p ${{ secrets.VSCE_TOKEN }} 42 | echo "Publish to ovsx" 43 | ovsx publish lana-${{ github.event.release.tag_name }}.vsix --no-dependencies -p ${{ secrets.OVSX_TOKEN }} 44 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | pnpm lint-staged 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | auto-install-peers=true 3 | # rollup plugins (commonjs + node resolve)doe not play nice without this. 4 | shamefully-hoist=true 5 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/ 2 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["prettier-plugin-organize-imports"], 3 | "tabWidth": 2, 4 | "printWidth": 100, 5 | "semi": true, 6 | "singleQuote": true 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | { 9 | "name": "Run Extension", 10 | "type": "extensionHost", 11 | "request": "launch", 12 | "args": ["--extensionDevelopmentPath=${workspaceFolder}/lana", "--disable-extensions"], 13 | "outFiles": ["${workspaceFolder}/lana/out/**/*.js"], 14 | "localRoot": "${workspaceFolder}/lana" 15 | }, 16 | { 17 | "name": "Extension Tests", 18 | "type": "extensionHost", 19 | "request": "launch", 20 | "args": [ 21 | "--extensionDevelopmentPath=${workspaceFolder}", 22 | "--extensionTestsPath=${workspaceFolder}/lana/out/test/suite/index" 23 | ], 24 | "outFiles": ["${workspaceFolder}/lana/out/test/**/*.js"], 25 | "preLaunchTask": "${defaultBuildTask}", 26 | "localRoot": "${workspaceFolder}/lana" 27 | } 28 | ] 29 | } 30 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Certinia Open Source Community Code of Conduct 2 | 3 | ## Introduction 4 | 5 | We value diversity and inclusion at Certinia. We are committed to providing a safe and welcoming environment for all, regardless of gender identity and expression, sexual orientation, disability, ethnicity, nationality, race, age, religion, or other similar personal characteristics. 6 | 7 | Diversity also fosters innovation and this Code of Conduct sets standards to enable diverse groups of people to work together effectively, productively, and respectfully in our open source community. It also establishes a mechanism for reporting issues and resolving conflicts. 8 | 9 | All questions and reports of abusive, harassing, or otherwise unacceptable behavior in a Certinia open-source project may be reported by contacting Certinia Legal at legal@certinia.com. 10 | 11 | ## Standards for Contributing to a Certinia Open Source Project 12 | 13 | Examples of behavior that we encourage, which contributes to creating a positive environment include: 14 | 15 | - Using welcoming and inclusive language 16 | - Being respectful of differing viewpoints and experiences 17 | - Gracefully accepting constructive criticism 18 | - Focusing on what is best for the community 19 | - Showing empathy toward other community members 20 | 21 | Examples of unacceptable behavior by participants, which we will not allow, include: 22 | 23 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 24 | - Personal attacks, insulting/derogatory comments, or trolling 25 | - Public or private harassment 26 | - Publishing, or threatening to publish, others' private information—such as a physical or electronic address—without explicit permission 27 | - Other conduct which could reasonably be considered inappropriate in a professional setting 28 | - Advocating for or encouraging any of the above behaviors 29 | 30 | ## Our Responsibilities 31 | 32 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 33 | 34 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned with this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 35 | 36 | ## Scope 37 | 38 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project email address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 39 | 40 | ## Enforcement 41 | 42 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting Certinia Legal at legal@certinia.com. All complaints will be reviewed with the Certinia Open Source Committee, as appropriate, and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. Legal and the Open Source Committee are obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 43 | 44 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by Legal and our Open Source Committee. 45 | 46 | ## Attribution 47 | 48 | This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org/), version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html. It includes adaptions and additions from [Go Community Code of Conduct](https://golang.org/conduct), [CNCF Code of Conduct](https://github.com/cncf/foundation/blob/master/code-of-conduct.md), and [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). 49 | 50 | This Code of Conduct is licensed under the [Creative Commons Attribution 3.0 License](https://creativecommons.org/licenses/by/3.0/us/). 51 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # 🤝 Contributing to Apex Log Analyzer 2 | 3 | 🎉🥳 Thank you for contributing! 🥳🎉 4 | 5 | We truly appreciate your help in making Apex Log Analyzer better. Whether you're fixing a bug, adding a feature, or improving documentation, your contributions are invaluable to us! 6 | 7 | Before you dive in, please make sure to review our [code of conduct](https://github.com/certinia/debug-log-analyzer/blob/main/CODE_OF_CONDUCT.md) to ensure a welcoming environment for everyone. 8 | 9 | ## 📋 Before You Start 10 | 11 | ### Search for Existing Issues 12 | 13 | 1. **Check out the [open issues](https://github.com/certinia/debug-log-analyzer/issues)** first to see if your idea or bug has already been reported. 14 | 2. If you don’t see anything related, **open an issue**! We’d love to hear your thoughts and provide feedback or suggestions before you get started. This helps avoid duplicate work and ensures we’re aligned. 15 | 16 | ## 🛠️ Setting Up Your Development Environment 17 | 18 | For detailed instructions on setting up your development environment, refer to the [Development Guide](https://github.com/certinia/debug-log-analyzer/blob/main/DEVELOPING.md). 19 | 20 | ## 🚀 Ready to Contribute? 21 | 22 | We can't wait to see your contributions! Whether it’s fixing a bug, adding a new feature, or improving the docs, we truly appreciate your time and effort. 23 | 24 | ### Quick Recap of Steps 25 | 26 | 1. **Fork** the repo. 27 | 2. **Clone** your fork locally. 28 | 3. Create a **feature branch** e.g (`feat-some-description` or `bug-some-description`). 29 | 4. **Make your changes**. 30 | 5. **Push** your changes. 31 | 6. **Open a PR** for review! 32 | 33 | ## 📅 Commit Message Guidelines 34 | 35 | - We follow a simple format for commit messages: [Conventional Commits](https://www.conventionalcommits.org/en/v1.0.0/) 36 | - Use the imperative mood in the subject line (e.g., "fix: date parsing in log parser" rather than "fixed date parsing in log parser"). 37 | 38 | ## 💬 Need Help? 39 | 40 | If you get stuck at any point, feel free to open an issue or reach out to us in the discussions tab. We’re here to help and we want to make your contribution experience as smooth as possible. 🤗 41 | 42 | Thank you again for contributing to **Apex Log Analyzer**! Your input helps make this tool even better for Salesforce developers. 🙌 43 | -------------------------------------------------------------------------------- /DEVELOPING.md: -------------------------------------------------------------------------------- 1 | # 🛠️ Developing the Apex Log Analyzer Extension 2 | 3 | Welcome to the development guide for the **Apex Log Analyzer** VS Code extension! This document will walk you through the steps required to get started with the development environment, build the extension, and contribute to the project. 4 | 5 | - The source code is written in [TypeScript](https://www.typescriptlang.org/). 6 | - The lana directory contains the source code for the VS Code Extension. 7 | - The log-viewer directory contains the source code for the webview displayed by the extension but does not depend on VSCode. 8 | 9 | ## 📚 Table of Contents 10 | 11 | 1. [Prerequisites](#-prerequisites) 12 | 2. [Setting Up the Development Environment](#-setting-up-the-development-environment) 13 | 3. [Building and Bundling](#-building-and-bundling) 14 | 4. [Running the Extension Locally](#-running-the-extension-locally) 15 | 5. [Packaging the Extension](#-packaging-the-extension) 16 | 17 | ## 🔧 Prerequisites 18 | 19 | Before you start developing, make sure you have the following tools installed: 20 | 21 | - **Node.js** v16 or above: [Install Node.js](https://nodejs.org/en/) 22 | - **[pnpm](https://pnpm.io/)**: This package manager will be used for installing dependencies 23 | - \*\*[VS Code](https://code.visualstudio.com/) 24 | 25 | Once you’ve got these ready, you’re all set to get started! 🚀 26 | 27 | ## 👨‍💻 Setting Up the Development Environment 28 | 29 | To get started, clone this repository and install the necessary dependencies. 30 | 31 | 1. **Create a fork of the repository first** 32 | 2. **Clone the repository:** 33 | 34 | ```zsh 35 | git clone https://github.com/your-username/apex-log-analyzer.git 36 | cd apex-log-analyzer 37 | ``` 38 | 39 | 3. **Install dependencies:** 40 | 41 | Use [pnpm](https://pnpm.io/) to install project dependencies: 42 | 43 | ```zsh 44 | pnpm i 45 | ``` 46 | 47 | ## ⚙️ Building and Bundling 48 | 49 | You can build the extension and prepare it for local development, run the watcher to re build automatically or production use. Here's how: 50 | 51 | 1. **Watch Build:** 52 | 53 | To build the extension without minification and then watch for file changes in the `lana` and `log-viewer` source, rebuilding incrementally, for a fast dev experience, use: 54 | 55 | ```bash 56 | pnpm run watch 57 | ``` 58 | 59 | 2. **Development Build:** 60 | 61 | To build the extension without minification (fast for local development), use: 62 | 63 | ```bash 64 | pnpm run build:dev 65 | ``` 66 | 67 | 3. **Production Build:** 68 | 69 | To create a production-ready build with minification, use: 70 | 71 | ```bash 72 | pnpm run build 73 | ``` 74 | 75 | ## 🚀 Running the Extension Locally 76 | 77 | Once you’ve built the extension or run the watcher, you can run it inside a local VS Code instance for testing and development. 78 | 79 | 1. **Start the extension host:** 80 | 81 | - Open the **Run and Debug** panel in VS Code (CMD/CTRL + Shift + D). 82 | - Select **Run Extension** from the dropdown. 83 | - Click the green play button to launch a new VS Code window (the extension host). 84 | 85 | 2. **Refresh the extension host:** 86 | 87 | If you're using the **watch** mode (see below), refresh the extension host view by pressing CMD/CTRL + R or clicking the restart icon. 88 | 89 | ## 🧪 Testing Your Changes 90 | 91 | Make sure your changes don’t break anything. If you’re working on a feature or bug fix that requires tests, be sure to add or update the relevant tests. 92 | 93 | Run Tests Locally: 94 | If you have added or modified tests, you can run them with: 95 | 96 | ```zsh 97 | pnpm run test 98 | ``` 99 | 100 | or run the tests from the test explorer in VScode 101 | 102 | Ensure all tests pass before submitting your pull request. 103 | 104 | ## 📦 Packaging the Extension 105 | 106 | This is for information only packaging and releasing is handled in Github. 107 | Once you're ready to package the extension for distribution: 108 | 109 | 1. Ensure that you’ve installed the dependencies: 110 | 111 | ```zsh 112 | pnpm install 113 | ``` 114 | 115 | 2. Package the extension: 116 | 117 | ```zsh 118 | cd lana 119 | vsce package --no-dependencies 120 | ``` 121 | 122 | This command will create a `.vsix` file that you can distribute or install locally. 123 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) Certinia Inc. All rights reserved. 4 | 5 | Redistribution and use in source and binary forms, with or without 6 | modification, are permitted provided that the following conditions are met: 7 | 8 | 1. Redistributions of source code must retain the above copyright notice, this 9 | list of conditions and the following disclaimer. 10 | 11 | 2. Redistributions in binary form must reproduce the above copyright notice, 12 | this list of conditions and the following disclaimer in the documentation 13 | and/or other materials provided with the distribution. 14 | 15 | 3. Neither the name of the copyright holder nor the names of its 16 | contributors may be used to endorse or promote products derived from 17 | this software without specific prior written permission. 18 | 19 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 20 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 21 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 22 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 23 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 24 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 25 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 26 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 27 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 | 30 | --- 31 | 32 | Third-Party Licenses and Acknowledgments: 33 | 34 | Tabulator Tables - https://github.com/olifolkerd/tabulator 35 | Copyright (c) - Oli Folkerd 36 | License: MIT 37 | 38 | Tabulator is used under the terms of the MIT License. A copy of the license is available at: 39 | https://opensource.org/licenses/MIT 40 | -------------------------------------------------------------------------------- /data/README.md: -------------------------------------------------------------------------------- 1 | # Manual data 2 | 3 | ## fflib.log 4 | 5 | A basic log which was used as the basis to generate the gifs for the main readme. 6 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import typescriptEslint from "@typescript-eslint/eslint-plugin"; 2 | import tsParser from "@typescript-eslint/parser"; 3 | import path from "node:path"; 4 | import { fileURLToPath } from "node:url"; 5 | import js from "@eslint/js"; 6 | import { FlatCompat } from "@eslint/eslintrc"; 7 | 8 | const __filename = fileURLToPath(import.meta.url); 9 | const __dirname = path.dirname(__filename); 10 | const compat = new FlatCompat({ 11 | baseDirectory: __dirname, 12 | recommendedConfig: js.configs.recommended, 13 | allConfig: js.configs.all 14 | }); 15 | 16 | export default [{ 17 | ignores: ["**/node_modules"], 18 | }, ...compat.extends( 19 | "eslint:recommended", 20 | "plugin:@typescript-eslint/eslint-recommended", 21 | "plugin:@typescript-eslint/recommended", 22 | "prettier", 23 | ), { 24 | plugins: { 25 | "@typescript-eslint": typescriptEslint, 26 | }, 27 | 28 | languageOptions: { 29 | parser: tsParser, 30 | ecmaVersion: "latest", 31 | sourceType: "module", 32 | }, 33 | 34 | rules: { 35 | "no-console": "warn", 36 | "@typescript-eslint/naming-convention": "warn", 37 | semi: "warn", 38 | 39 | "@typescript-eslint/no-unused-vars": ["error", { 40 | argsIgnorePattern: "^_", 41 | varsIgnorePattern: "^_", 42 | }], 43 | 44 | "@typescript-eslint/no-explicit-any": "warn", 45 | curly: "warn", 46 | eqeqeq: "warn", 47 | }, 48 | }]; -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | { 4 | displayName: 'log-viewer', 5 | rootDir: '<rootDir>/log-viewer', 6 | testEnvironment: 'node', 7 | 8 | moduleNameMapper: { 9 | '^(\\.{1,2}/.*)\\.js$': '$1', 10 | }, 11 | transform: { 12 | '^.+\\.(ts|js)?$': [ 13 | '@swc/jest', 14 | { 15 | jsc: { 16 | target: 'esnext', 17 | parser: { decorators: true, syntax: 'typescript' }, 18 | }, 19 | }, 20 | ], 21 | }, 22 | transformIgnorePatterns: [ 23 | // allow lit/@lit transformation 24 | '<rootDir>/node_modules/(?!@?lit)', 25 | ], 26 | testPathIgnorePatterns: ['<rootDir>/node_modules/', '<rootDir>/out/'], 27 | extensionsToTreatAsEsm: ['.ts', '.tsx'], 28 | }, 29 | ], 30 | }; 31 | -------------------------------------------------------------------------------- /lana-docs-site/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | -------------------------------------------------------------------------------- /lana-docs-site/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ pnpm i 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ pnpm start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ pnpm build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true pnpm deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER=<Your GitHub username> pnpm deploy 39 | ``` 40 | 41 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 42 | -------------------------------------------------------------------------------- /lana-docs-site/docs/community/changelog.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Changelog 3 | hide_title: true 4 | sidebar_position: 3 5 | sidebar_label: Changelog 6 | --- 7 | 8 | ```mdx-code-block 9 | import Changelog from "@site/../CHANGELOG.md" 10 | 11 | <Changelog /> 12 | ``` 13 | -------------------------------------------------------------------------------- /lana-docs-site/docs/community/contributing.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | title: Contributing 3 | hide_title: true 4 | sidebar_position: 2 5 | sidebar_label: Contributing 6 | --- 7 | 8 | ```mdx-code-block 9 | import Contributing from "@site/../CONTRIBUTING.md" 10 | 11 | <Contributing /> 12 | ``` 13 | -------------------------------------------------------------------------------- /lana-docs-site/docs/community/support.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Support 6 | 7 | Before participating, please read our [Code of Conduct](https://github.com/certinia/debug-log-analyzer/blob/main/CODE_OF_CONDUCT.md). 8 | 9 | ## Discussion 10 | 11 | For discussion about best practices or if you have any questions or need help, please start a [discussion on github](https://github.com/certinia/debug-log-analyzer/discussions) 12 | 13 | ## Feature requests and Bugs {#feature-requests} 14 | 15 | For new feature requests, open a [feature request issue](https://github.com/certinia/debug-log-analyzer/issues/new/choose) or [bug report issue](https://github.com/certinia/debug-log-analyzer/issues/new/choose) on github. Our issues can be sorted by upvotes, which gives us a way to prioritise what to work on next. 16 | -------------------------------------------------------------------------------- /lana-docs-site/docs/docs/gettingstarted.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Getting Started 6 | 7 | Start the analysis either from a log you have already downloaded or by downloading a log from an org to view. 8 | On larger logs the analysis window make take a few seconds to appear. 9 | 10 | ## From an Open Log File 11 | 12 | With the `.log` file open in VSCode. 13 | 14 | 1. Open command pallette (CMD/CTRL + Shift + P) -> 'Log: Show Apex Log Analysis'\ 15 | or 16 | 1. Click the 'Log: Show Apex Log Analysis' code lens at the top of the file\ 17 | ![show analysis lens](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/dist/images/lana-showanalysis-lens.webp)\ 18 | or 19 | 1. Right click -> 'Log: Show Apex Log Analysis' 20 | or 21 | 1. Click editor 'Log: Show Apex Log Analysis' icon button at top of editor 22 | or 23 | 1. Right click editor tab -> 'Log: Show Apex Log Analysis' 24 | 25 | ## Download a log 26 | 27 | 1. Open command pallette (CMD/CTRL + Shift + P) -> 'Log: Retrieve Apex Log And Show Analysis 28 | -------------------------------------------------------------------------------- /lana-docs-site/docs/docs/installation.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Installation 6 | 7 | ![install](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/dist/images/install-lana.webp) 8 | 9 | Search for `Apex Log Analyzer` from the extensions side bar in VS Code and click `Install` or 10 | install from the VS Code market place by clicking install on [Visual Studio Code Market Place: Apex Log Analyzer](https://marketplace.visualstudio.com/items?itemName=financialforce.lana) 11 | 12 | ### Pre-Release 13 | 14 | Click `Switch to Pre-Release Version` on the banner to get bleeding edge changes and help us to resolve bugs before the stable release. 15 | 16 | ### Command Pallette 17 | 18 | Open command pallette (CMD/CTRL + Shift + P), paste `ext install financialforce.lana`, and press enter. 19 | 20 | ```sh 21 | ext install financialforce.lana 22 | ``` 23 | -------------------------------------------------------------------------------- /lana-docs-site/docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | id: intro 3 | slug: / 4 | sidebar_position: 1 5 | --- 6 | 7 | # Introduction 8 | 9 | Apex Log Analyzer makes performance analysis of Salesforce debug logs much easier and quicker. Visualize code execution via a Flame chart and Call Tree, identify and resolve performance and SOQL/DML problems via Method and Database Analysis. 10 | 11 | ![preview](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/dist/images/lana-preview.gif) 12 | 13 | ## WARNING 14 | 15 | > In general set the `APEX_CODE` debug flag to be `FINE` or higher, with a lower level the log will likely not contain enough detail for meaningful analysis. 16 | > 17 | > The quality of data shown depends entirely on the data contained in the log files.\ 18 | > Special care should be taken when looking at log files that have been truncated as you are only seeing a part of the execution and that may lead you to misunderstand what is really happening. 19 | > 20 | > A log level of `FINE` seems to give a good balance between log detail and execution time.\ 21 | > Higher log levels result in higher reported execution time than would be seen with logging off.\ 22 | > This is due to the over head associated with logging method entry and exit. 23 | -------------------------------------------------------------------------------- /lana-docs-site/docs/docs/settings.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Settings 6 | 7 | ## Timeline color settings 8 | 9 | The default colors shown on the timeline can be changed in the VSCode settings.\ 10 | Either in the UI `preferences -> extensions -> Apex Log Analyzer` 11 | 12 | ![color settings](https://raw.githubusercontent.com/certinia/debug-log-analyzer/main/lana/dist/images/settings-color-lana.webp) 13 | 14 | or 15 | 16 | settings.json 17 | 18 | ```json 19 | "lana.timeline.colors": { 20 | "Code Unit": "#88AE58", 21 | "Workflow": "#51A16E", 22 | "Method": "#2B8F81", 23 | "Flow": "#337986", 24 | "DML": "#285663", 25 | "SOQL": "#5D4963", 26 | "System Method": "#5C3444" 27 | } 28 | ``` 29 | -------------------------------------------------------------------------------- /lana-docs-site/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import type * as Preset from '@docusaurus/preset-classic'; 2 | import type { Config } from '@docusaurus/types'; 3 | import { themes as prismThemes } from 'prism-react-renderer'; 4 | 5 | const organizationName = 'certinia'; 6 | const projectName = 'debug-log-analyzer'; 7 | 8 | const config: Config = { 9 | title: 'Apex Log Analyzer for Salesforce', 10 | tagline: 11 | 'Visualize code execution via a Flame graph and identify performance and SOQL/DML problems via Method and Database analysis', 12 | favicon: '../../lana/certinia-icon-color.png', 13 | 14 | // Set the production url of your site here 15 | url: `https://${organizationName}.github.io`, 16 | // Set the /<baseUrl>/ pathname under which your site is served 17 | // For GitHub pages deployment, it is often '/<projectName>/' 18 | baseUrl: `/${projectName}/`, 19 | 20 | // GitHub pages deployment config. 21 | // If you aren't using GitHub pages, you don't need these. 22 | organizationName: organizationName, // Usually your GitHub org/user name. 23 | projectName: projectName, // Usually your repo name. 24 | 25 | onBrokenLinks: 'throw', 26 | onBrokenMarkdownLinks: 'warn', 27 | 28 | trailingSlash: false, 29 | 30 | // Even if you don't use internationalization, you can use this field to set 31 | // useful metadata like html lang. For example, if your site is Chinese, you 32 | // may want to replace "en" with "zh-Hans". 33 | i18n: { 34 | defaultLocale: 'en', 35 | locales: ['en'], 36 | }, 37 | 38 | future: { 39 | // eslint-disable-next-line @typescript-eslint/naming-convention 40 | experimental_faster: true, 41 | }, 42 | 43 | presets: [ 44 | [ 45 | 'classic', 46 | { 47 | docs: { 48 | routeBasePath: '/', 49 | sidebarPath: './sidebars.ts', 50 | // Please change this to your repo. 51 | // Remove this to remove the "edit this page" links. 52 | editUrl: `https://github.com/${organizationName}/${projectName}/tree/main/lana-docs-site`, 53 | }, 54 | 55 | theme: { 56 | customCss: './src/css/custom.css', 57 | }, 58 | } satisfies Preset.Options, 59 | ], 60 | ], 61 | themeConfig: { 62 | // Replace with your project's social card 63 | image: 'img/lana-timeline.png', 64 | navbar: { 65 | title: 'Apex Log Analyzer for Salesforce', 66 | logo: { 67 | alt: 'Certinia Logo', 68 | src: 'img/logo.svg', 69 | srcDark: 'img/logo-dark.svg', 70 | }, 71 | items: [ 72 | { 73 | type: 'docSidebar', 74 | sidebarId: 'docSidebar', 75 | position: 'left', 76 | label: 'Docs', 77 | }, 78 | 79 | { 80 | type: 'docSidebar', 81 | sidebarId: 'communitySidebar', 82 | position: 'left', 83 | label: 'Community', 84 | }, 85 | { 86 | href: `https://github.com/${organizationName}/${projectName}`, 87 | label: 'GitHub', 88 | position: 'right', 89 | }, 90 | { 91 | type: 'search', 92 | position: 'right', 93 | }, 94 | ], 95 | }, 96 | footer: { 97 | style: 'dark', 98 | links: [ 99 | { 100 | title: 'Docs', 101 | items: [ 102 | { 103 | label: 'Introduction', 104 | to: '/', 105 | }, 106 | { 107 | label: 'Installation', 108 | to: 'docs/installation', 109 | }, 110 | ], 111 | }, 112 | { 113 | title: 'Community', 114 | items: [ 115 | { 116 | label: 'Support', 117 | to: 'community/support', 118 | }, 119 | { 120 | label: 'Contributing', 121 | to: 'community/contributing', 122 | }, 123 | { 124 | label: 'Changelog', 125 | to: 'community/changelog', 126 | }, 127 | ], 128 | }, 129 | { 130 | title: 'More', 131 | items: [ 132 | { 133 | label: 'GitHub', 134 | href: `https://github.com/${organizationName}/${projectName}`, 135 | }, 136 | { 137 | label: 'Twitter', 138 | href: 'https://twitter.com/CertiniaInc', 139 | }, 140 | ], 141 | }, 142 | ], 143 | copyright: `Copyright © ${new Date().getFullYear()} Certinia inc. All rights reserved.`, 144 | }, 145 | prism: { 146 | theme: prismThemes.github, 147 | darkTheme: prismThemes.dracula, 148 | }, 149 | } satisfies Preset.ThemeConfig, 150 | themes: [ 151 | [ 152 | '@easyops-cn/docusaurus-search-local', 153 | { 154 | // Base route path(s) of docs. Slash at beginning is not required. 155 | docsRouteBasePath: '/', 156 | 157 | // Whether to add a hashed query when fetching index 158 | hashed: true, 159 | 160 | // Highlight search terms on target page. 161 | highlightSearchTermsOnTargetPage: true, 162 | 163 | // whether to index docs pages 164 | indexDocs: true, 165 | 166 | // whether to index blog pages 167 | indexBlog: true, 168 | 169 | // whether to index static pages 170 | // /404.html is never indexed 171 | indexPages: true, 172 | 173 | // language of your documentation, see next section 174 | language: 'en', 175 | 176 | // Enable this if you want to be able to search for any partial word at the cost of search performance. 177 | removeDefaultStemmer: true, 178 | }, 179 | ], 180 | ], 181 | }; 182 | 183 | export default config; 184 | -------------------------------------------------------------------------------- /lana-docs-site/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lana-docs-site", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.7.0", 19 | "@docusaurus/faster": "^3.7.0", 20 | "@docusaurus/preset-classic": "^3.7.0", 21 | "@easyops-cn/docusaurus-search-local": "^0.49.2", 22 | "@mdx-js/react": "^3.1.0", 23 | "clsx": "^2.1.1", 24 | "prism-react-renderer": "^2.4.1", 25 | "react": "^19.1.0", 26 | "react-dom": "^19.1.0" 27 | }, 28 | "devDependencies": { 29 | "@docusaurus/module-type-aliases": "^3.7.0", 30 | "@docusaurus/tsconfig": "^3.7.0", 31 | "@docusaurus/types": "^3.7.0", 32 | "typescript": "^5.8.3" 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.5%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 3 chrome version", 42 | "last 3 firefox version", 43 | "last 5 safari version" 44 | ] 45 | }, 46 | "engines": { 47 | "node": ">=18.0" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lana-docs-site/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type { SidebarsConfig } from '@docusaurus/plugin-content-docs'; 2 | 3 | /** 4 | * Creating a sidebar enables you to: 5 | - create an ordered group of docs 6 | - render a sidebar for each doc of that group 7 | - provide next/previous navigation 8 | 9 | The sidebars can be generated from the filesystem, or explicitly defined here. 10 | 11 | Create as many sidebars as you want. 12 | */ 13 | const sidebars: SidebarsConfig = { 14 | // By default, Docusaurus generates a sidebar from the docs folder structure 15 | docSidebar: [{ type: 'autogenerated', dirName: 'docs' }], 16 | communitySidebar: [{ type: 'autogenerated', dirName: 'community' }], 17 | 18 | // But you can create a sidebar manually 19 | /* 20 | tutorialSidebar: [ 21 | 'intro', 22 | 'hello', 23 | { 24 | type: 'category', 25 | label: 'Tutorial', 26 | items: ['tutorial-basics/create-a-document'], 27 | }, 28 | ], 29 | */ 30 | }; 31 | 32 | export default sidebars; 33 | -------------------------------------------------------------------------------- /lana-docs-site/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import clsx from 'clsx'; 2 | import Heading from '@theme/Heading'; 3 | import styles from './styles.module.css'; 4 | 5 | type FeatureItem = { 6 | title: string; 7 | Svg: React.ComponentType<React.ComponentProps<'svg'>>; 8 | description: JSX.Element; 9 | }; 10 | 11 | const FeatureList: FeatureItem[] = [ 12 | { 13 | title: 'Easy to Use', 14 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 15 | description: ( 16 | <> 17 | Docusaurus was designed from the ground up to be easily installed and 18 | used to get your website up and running quickly. 19 | </> 20 | ), 21 | }, 22 | { 23 | title: 'Focus on What Matters', 24 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 25 | description: ( 26 | <> 27 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 28 | ahead and move your docs into the <code>docs</code> directory. 29 | </> 30 | ), 31 | }, 32 | { 33 | title: 'Powered by React', 34 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 35 | description: ( 36 | <> 37 | Extend or customize your website layout by reusing React. Docusaurus can 38 | be extended while reusing the same header and footer. 39 | </> 40 | ), 41 | }, 42 | ]; 43 | 44 | function Feature({title, Svg, description}: FeatureItem) { 45 | return ( 46 | <div className={clsx('col col--4')}> 47 | <div className="text--center"> 48 | <Svg className={styles.featureSvg} role="img" /> 49 | </div> 50 | <div className="text--center padding-horiz--md"> 51 | <Heading as="h3">{title}</Heading> 52 | <p>{description}</p> 53 | </div> 54 | </div> 55 | ); 56 | } 57 | 58 | export default function HomepageFeatures(): JSX.Element { 59 | return ( 60 | <section className={styles.features}> 61 | <div className="container"> 62 | <div className="row"> 63 | {FeatureList.map((props, idx) => ( 64 | <Feature key={idx} {...props} /> 65 | ))} 66 | </div> 67 | </div> 68 | </section> 69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /lana-docs-site/src/components/HomepageFeatures/styles.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /lana-docs-site/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | :root { 9 | --ifm-color-primary: #2e8555; 10 | --ifm-color-primary-dark: #29784c; 11 | --ifm-color-primary-darker: #277148; 12 | --ifm-color-primary-darkest: #205d3b; 13 | --ifm-color-primary-light: #33925d; 14 | --ifm-color-primary-lighter: #359962; 15 | --ifm-color-primary-lightest: #3cad6e; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #25c2a0; 23 | --ifm-color-primary-dark: #21af90; 24 | --ifm-color-primary-darker: #1fa588; 25 | --ifm-color-primary-darkest: #1a8870; 26 | --ifm-color-primary-light: #29d5b0; 27 | --ifm-color-primary-lighter: #32d8b4; 28 | --ifm-color-primary-lightest: #4fddbf; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /lana-docs-site/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /lana-docs-site/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /lana-docs-site/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana-docs-site/static/.nojekyll -------------------------------------------------------------------------------- /lana-docs-site/static/img/favicon.svg: -------------------------------------------------------------------------------- 1 | <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><linearGradient id="a" x1="0" x2="16" y1="8" y2="8" gradientUnits="userSpaceOnUse"><stop offset="0" stop-color="#dca0ff"/><stop offset=".086" stop-color="#c6a0ff"/><stop offset=".267" stop-color="#9fa0ff"/><stop offset=".412" stop-color="#86a0ff"/><stop offset=".5" stop-color="#7ea0ff"/><stop offset=".595" stop-color="#7ba8ff"/><stop offset=".75" stop-color="#73c1ff"/><stop offset=".945" stop-color="#67e8ff"/><stop offset="1" stop-color="#64f5ff"/></linearGradient></defs><path d="M0 0h16v16H0z" style="fill:url(#a)"/><path d="m12.01 10.988-.315-.275c-.688.923-1.592 1.415-2.575 1.415-1.749 0-2.87-1.651-2.87-4.226S7.41 3.617 9.1 3.617c.964 0 1.907.55 2.28 1.376a1.1 1.1 0 0 0-.766-.276c-.648 0-1.14.472-1.14 1.12 0 .63.511 1.14 1.16 1.14.708 0 1.219-.589 1.219-1.414 0-1.396-1.494-2.477-3.46-2.477-2.693 0-4.678 2.103-4.678 4.875 0 2.79 2.025 4.796 4.796 4.796 1.494 0 2.673-.59 3.499-1.77Z" style="fill:#fff"/></svg> -------------------------------------------------------------------------------- /lana-docs-site/static/img/lana-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana-docs-site/static/img/lana-timeline.png -------------------------------------------------------------------------------- /lana-docs-site/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /lana/.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Intellij 3 | # Ignoring all project files 4 | 5 | .DS_Store 6 | *.iml 7 | *.ipr 8 | *.iws 9 | .idea/ 10 | 11 | *.vsix 12 | tsconfig.tsbuildinfo 13 | 14 | out/ 15 | node_modules/* 16 | 17 | README.md 18 | CHANGELOG.md 19 | LICENSE.txt 20 | -------------------------------------------------------------------------------- /lana/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // See http://go.microsoft.com/fwlink/?LinkId=827846 3 | // for the documentation about the extensions.json format 4 | "recommendations": [ 5 | "dbaeumer.vscode-eslint" 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /lana/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | // A launch configuration that compiles the extension and then opens it inside a new window 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | { 6 | "version": "0.2.0", 7 | "configurations": [ 8 | 9 | { 10 | "name": "Run Extension", 11 | "type": "extensionHost", 12 | "request": "launch", 13 | "args": [ 14 | "--extensionDevelopmentPath=${workspaceFolder}" 15 | ], 16 | "outFiles": [ 17 | "${workspaceFolder}/out/**/*.js" 18 | ] 19 | }, 20 | { 21 | "name": "Extension Tests", 22 | "type": "extensionHost", 23 | "request": "launch", 24 | "args": [ 25 | "--extensionDevelopmentPath=${workspaceFolder}", 26 | "--extensionTestsPath=${workspaceFolder}/out/test/suite/index" 27 | ], 28 | "outFiles": [ 29 | "${workspaceFolder}/out/test/**/*.js" 30 | ], 31 | "preLaunchTask": "${defaultBuildTask}" 32 | } 33 | ] 34 | } 35 | -------------------------------------------------------------------------------- /lana/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | // Place your settings in this file to overwrite default and user settings. 2 | { 3 | "files.exclude": { 4 | "out": false // set this to true to hide the "out" folder with the compiled JS files 5 | }, 6 | "search.exclude": { 7 | "out": true // set this to false to include "out" folder in search results 8 | }, 9 | // Turn off tsc task auto detection since we have the necessary tasks as npm scripts 10 | "typescript.tsc.autoDetect": "off" 11 | } -------------------------------------------------------------------------------- /lana/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | // See https://go.microsoft.com/fwlink/?LinkId=733558 2 | // for the documentation about the tasks.json format 3 | { 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "watch", 9 | "problemMatcher": "$tsc-watch", 10 | "isBackground": true, 11 | "presentation": { 12 | "reveal": "never" 13 | }, 14 | "group": { 15 | "kind": "build", 16 | "isDefault": true 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /lana/.vscodeignore: -------------------------------------------------------------------------------- 1 | .vscode/ 2 | .github/ 3 | 4 | .gitignore 5 | BUILDING.md 6 | tsconfig.json 7 | tsconfig-dev.json 8 | node_modules 9 | rollup.config.mjs 10 | 11 | dist/ 12 | log-viewer/ 13 | !log-viewer/dist/ 14 | !log-viewer/index.html 15 | src/ 16 | -------------------------------------------------------------------------------- /lana/certinia-icon-color.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/certinia-icon-color.png -------------------------------------------------------------------------------- /lana/dist/images/install-lana.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/install-lana.webp -------------------------------------------------------------------------------- /lana/dist/images/lana-analysis-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-analysis-find.png -------------------------------------------------------------------------------- /lana/dist/images/lana-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-analysis.png -------------------------------------------------------------------------------- /lana/dist/images/lana-calltree-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-calltree-find.png -------------------------------------------------------------------------------- /lana/dist/images/lana-calltree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-calltree.png -------------------------------------------------------------------------------- /lana/dist/images/lana-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-database.png -------------------------------------------------------------------------------- /lana/dist/images/lana-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-preview.gif -------------------------------------------------------------------------------- /lana/dist/images/lana-showanalysis-lens.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-showanalysis-lens.webp -------------------------------------------------------------------------------- /lana/dist/images/lana-timeline-find.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-timeline-find.png -------------------------------------------------------------------------------- /lana/dist/images/lana-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-timeline.png -------------------------------------------------------------------------------- /lana/dist/images/lana-tooltip.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/lana-tooltip.webp -------------------------------------------------------------------------------- /lana/dist/images/settings-color-lana.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/images/settings-color-lana.webp -------------------------------------------------------------------------------- /lana/dist/v1.10/install-lana.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/install-lana.webp -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-analysis.png -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-calltree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-calltree.png -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-database.png -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-preview.gif -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-showanalysis-lens.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-showanalysis-lens.webp -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-timeline.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-timeline.png -------------------------------------------------------------------------------- /lana/dist/v1.10/lana-tooltip.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/lana-tooltip.webp -------------------------------------------------------------------------------- /lana/dist/v1.10/settings-color-lana.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.10/settings-color-lana.webp -------------------------------------------------------------------------------- /lana/dist/v1.12/lana-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.12/lana-analysis.png -------------------------------------------------------------------------------- /lana/dist/v1.12/lana-calltree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.12/lana-calltree.png -------------------------------------------------------------------------------- /lana/dist/v1.12/lana-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.12/lana-preview.gif -------------------------------------------------------------------------------- /lana/dist/v1.14/lana-analysis.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.14/lana-analysis.png -------------------------------------------------------------------------------- /lana/dist/v1.14/lana-calltree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.14/lana-calltree.png -------------------------------------------------------------------------------- /lana/dist/v1.14/lana-database.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.14/lana-database.png -------------------------------------------------------------------------------- /lana/dist/v1.14/lana-preview.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/certinia/debug-log-analyzer/50ab2612a86fd86b662d14910c90eced4281bffa/lana/dist/v1.14/lana-preview.gif -------------------------------------------------------------------------------- /lana/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | import commonjs from '@rollup/plugin-commonjs'; 2 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 3 | 4 | import { 5 | defineRollupSwcOption, 6 | swc, 7 | minify, 8 | defineRollupSwcMinifyOption, 9 | } from 'rollup-plugin-swc3'; 10 | 11 | const production = !process.env.ROLLUP_WATCH; 12 | const plugins = [ 13 | nodeResolve(), 14 | commonjs(), 15 | swc( 16 | defineRollupSwcOption({ 17 | exclude: 'node_modules', 18 | tsconfig: production ? 'tsconfig.json' : 'tsconfig-dev.json', 19 | jsc: {}, 20 | }) 21 | ), 22 | production && 23 | minify( 24 | defineRollupSwcMinifyOption({ 25 | // swc's minify option here 26 | mangle: true, 27 | compress: true, 28 | }) 29 | ), 30 | ]; 31 | 32 | export default { 33 | input: 'src/Main.ts', 34 | output: { 35 | format: 'cjs', 36 | file: 'out/Main.js', 37 | sourcemap: false, 38 | }, 39 | external: ['vscode'], 40 | plugins, 41 | }; 42 | -------------------------------------------------------------------------------- /lana/src/AppSettings.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | 5 | export const appName = 'Lana'; 6 | -------------------------------------------------------------------------------- /lana/src/Context.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { workspace, type ExtensionContext } from 'vscode'; 5 | 6 | import { ShowAnalysisCodeLens } from './codelenses/ShowAnalysisCodeLens.js'; 7 | import { RetrieveLogFile } from './commands/RetrieveLogFile.js'; 8 | import { ShowLogAnalysis } from './commands/ShowLogAnalysis.js'; 9 | import { Display } from './display/Display.js'; 10 | import { WhatsNewNotification } from './display/WhatsNewNotification.js'; 11 | import { SymbolFinder } from './salesforce/codesymbol/SymbolFinder.js'; 12 | import { VSWorkspace } from './workspace/VSWorkspace.js'; 13 | 14 | export class Context { 15 | symbolFinder = new SymbolFinder(); 16 | context: ExtensionContext; 17 | display: Display; 18 | workspaces: VSWorkspace[] = []; 19 | 20 | constructor(context: ExtensionContext, display: Display) { 21 | this.context = context; 22 | this.display = display; 23 | 24 | if (workspace.workspaceFolders) { 25 | this.workspaces = workspace.workspaceFolders.map((folder) => { 26 | return new VSWorkspace(folder); 27 | }); 28 | } 29 | 30 | RetrieveLogFile.apply(this); 31 | ShowLogAnalysis.apply(this); 32 | ShowAnalysisCodeLens.apply(this); 33 | WhatsNewNotification.apply(this); 34 | } 35 | 36 | async findSymbol(symbol: string): Promise<string[]> { 37 | const path = await this.symbolFinder.findSymbol(this.workspaces, symbol); 38 | if (!path.length) { 39 | this.display.showErrorMessage(`Type '${symbol}' was not found in workspace`); 40 | } 41 | return path; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /lana/src/Main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { type ExtensionContext } from 'vscode'; 5 | 6 | import { Context } from './Context.js'; 7 | import { Display } from './display/Display.js'; 8 | 9 | export let context: Context | null = null; 10 | 11 | export function activate(extensionContext: ExtensionContext) { 12 | context = new Context(extensionContext, new Display()); 13 | } 14 | 15 | export function deactivate() { 16 | context = null; 17 | } 18 | -------------------------------------------------------------------------------- /lana/src/codelenses/ShowAnalysisCodeLens.ts: -------------------------------------------------------------------------------- 1 | import { CodeLens, Range, languages, type CodeLensProvider, type TextDocument } from 'vscode'; 2 | 3 | import { Context } from '../Context.js'; 4 | import { ShowLogAnalysis } from '../commands/ShowLogAnalysis.js'; 5 | 6 | class ShowAnalysisCodeLens implements CodeLensProvider { 7 | context: Context; 8 | constructor(context: Context) { 9 | this.context = context; 10 | } 11 | // Each provider requires a provideCodeLenses function which will give the various documents the code lenses 12 | async provideCodeLenses(_document: TextDocument): Promise<CodeLens[]> { 13 | // Define where the CodeLens will exist 14 | const topOfDocument = new Range(0, 0, 0, 0); 15 | 16 | // Define what command we want to trigger when activating the CodeLens 17 | const command = ShowLogAnalysis.getCommand(this.context); 18 | const codeLens = new CodeLens(topOfDocument, { 19 | command: command.fullName, 20 | title: command.title, 21 | }); 22 | 23 | return [codeLens]; 24 | } 25 | 26 | static apply(context: Context): void { 27 | // Get a document selector for the CodeLens provider 28 | // This one is any file that has the language of apexlog 29 | const docSelector = [{ scheme: 'file', language: 'apexlog' }]; 30 | 31 | // Register our CodeLens provider 32 | const codeLensProviderDisposable = languages.registerCodeLensProvider( 33 | docSelector, 34 | new ShowAnalysisCodeLens(context), 35 | ); 36 | 37 | // Push the command and CodeLens provider to the context so it can be disposed of later 38 | context.context.subscriptions.push(codeLensProviderDisposable); 39 | } 40 | } 41 | 42 | export { ShowAnalysisCodeLens }; 43 | -------------------------------------------------------------------------------- /lana/src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { commands } from 'vscode'; 5 | 6 | import { Context } from '../Context.js'; 7 | 8 | export class Command { 9 | private static commandPrefix = 'lana.'; 10 | 11 | name: string; 12 | fullName: string; 13 | title: string; 14 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 15 | run: (...args: any[]) => any; 16 | 17 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 18 | constructor(name: string, title: string, run: (...args: any[]) => any) { 19 | this.name = name; 20 | this.fullName = Command.commandPrefix + this.name; 21 | this.title = title; 22 | this.run = run; 23 | } 24 | 25 | register(c: Context): Command { 26 | const command = commands.registerCommand(this.fullName, this.run); 27 | c.context.subscriptions.push(command); 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /lana/src/commands/LogView.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { createReadStream, existsSync } from 'fs'; 5 | import { writeFile } from 'fs/promises'; 6 | import { homedir } from 'os'; 7 | import { basename, dirname, join, parse } from 'path'; 8 | import { Uri, commands, window as vscWindow, workspace, type WebviewPanel } from 'vscode'; 9 | 10 | import { Context } from '../Context.js'; 11 | import { OpenFileInPackage } from '../display/OpenFileInPackage.js'; 12 | import { WebView } from '../display/WebView.js'; 13 | 14 | interface WebViewLogFileRequest<T = unknown> { 15 | requestId: string; 16 | cmd: string; 17 | payload: T; 18 | } 19 | 20 | export class LogView { 21 | private static helpUrl = 'https://certinia.github.io/debug-log-analyzer/'; 22 | 23 | static async createView( 24 | context: Context, 25 | beforeSendLog?: Promise<void>, 26 | logPath?: string, 27 | logData?: string, 28 | ): Promise<WebviewPanel> { 29 | const panel = WebView.apply( 30 | 'logFile', 31 | 'Log: ' + logPath ? basename(logPath || '') : 'Untitled', 32 | [Uri.file(join(context.context.extensionPath, 'out')), Uri.file(dirname(logPath || ''))], 33 | ); 34 | 35 | const logViewerRoot = join(context.context.extensionPath, 'out'); 36 | const index = join(logViewerRoot, 'index.html'); 37 | const bundleUri = panel.webview.asWebviewUri(Uri.file(join(logViewerRoot, 'bundle.js'))); 38 | const indexSrc = await this.getFile(index); 39 | const toReplace: { [key: string]: string } = { 40 | '${extensionRoot}': panel.webview.asWebviewUri(Uri.file(join(logViewerRoot))).toString(), // eslint-disable-line @typescript-eslint/naming-convention 41 | 'bundle.js': bundleUri.toString(true), // eslint-disable-line @typescript-eslint/naming-convention 42 | }; 43 | 44 | panel.iconPath = Uri.file(join(logViewerRoot, 'certinia-icon-color.png')); 45 | panel.webview.html = indexSrc.replace(/bundle.js|\${extensionRoot}/gi, function (matched) { 46 | return toReplace[matched] || ''; 47 | }); 48 | 49 | panel.webview.onDidReceiveMessage( 50 | async (msg: WebViewLogFileRequest) => { 51 | const { cmd, requestId, payload } = msg; 52 | 53 | switch (cmd) { 54 | case 'fetchLog': { 55 | await beforeSendLog; 56 | LogView.sendLog(requestId, panel, context, logPath, logData); 57 | break; 58 | } 59 | 60 | case 'openPath': { 61 | const filePath = <string>payload; 62 | if (filePath) { 63 | context.display.showFile(filePath); 64 | } 65 | break; 66 | } 67 | 68 | case 'openType': { 69 | const { typeName } = <{ typeName: string; text: string }>payload; 70 | if (typeName) { 71 | const [className, lineNumber] = typeName.split('-'); 72 | let line; 73 | if (lineNumber) { 74 | line = parseInt(lineNumber); 75 | } 76 | OpenFileInPackage.openFileForSymbol(context, className || '', line); 77 | } 78 | break; 79 | } 80 | 81 | case 'openHelp': { 82 | commands.executeCommand('vscode.open', Uri.parse(this.helpUrl)); 83 | break; 84 | } 85 | 86 | case 'getConfig': { 87 | panel.webview.postMessage({ 88 | requestId, 89 | cmd: 'getConfig', 90 | payload: workspace.getConfiguration('lana'), 91 | }); 92 | break; 93 | } 94 | 95 | case 'saveFile': { 96 | const { fileContent, options } = < 97 | { fileContent: string; options: { defaultFileName?: string } } 98 | >payload; 99 | 100 | if (fileContent && options?.defaultFileName) { 101 | const defaultWorkspace = (workspace.workspaceFolders || [])[0]; 102 | const defaultDir = defaultWorkspace?.uri.path || homedir(); 103 | const destinationFile = await vscWindow.showSaveDialog({ 104 | defaultUri: Uri.file(join(defaultDir, options.defaultFileName)), 105 | }); 106 | 107 | if (destinationFile) { 108 | writeFile(destinationFile.fsPath, fileContent).catch((error) => { 109 | const msg = error instanceof Error ? error.message : String(error); 110 | vscWindow.showErrorMessage(`Unable to save file: ${msg}`); 111 | }); 112 | } 113 | } 114 | break; 115 | } 116 | 117 | case 'showError': { 118 | const { text } = <{ text: string }>payload; 119 | if (text) { 120 | vscWindow.showErrorMessage(text); 121 | } 122 | break; 123 | } 124 | } 125 | }, 126 | undefined, 127 | [], 128 | ); 129 | 130 | return panel; 131 | } 132 | 133 | private static async getFile(filePath: string): Promise<string> { 134 | let data = ''; 135 | return new Promise((resolve, reject) => { 136 | createReadStream(filePath) 137 | .on('error', (error) => { 138 | reject(error); 139 | }) 140 | .on('data', (row) => { 141 | data += row; 142 | }) 143 | .on('end', () => { 144 | resolve(data); 145 | }); 146 | }); 147 | } 148 | 149 | private static sendLog( 150 | requestId: string, 151 | panel: WebviewPanel, 152 | context: Context, 153 | logFilePath?: string, 154 | logData?: string, 155 | ) { 156 | if (!logData && !existsSync(logFilePath || '')) { 157 | context.display.showErrorMessage('Log file could not be found.', { 158 | modal: true, 159 | }); 160 | } 161 | 162 | const filePath = parse(logFilePath || ''); 163 | panel.webview.postMessage({ 164 | requestId, 165 | cmd: 'fetchLog', 166 | payload: { 167 | logName: filePath.name, 168 | logUri: logFilePath ? panel.webview.asWebviewUri(Uri.file(logFilePath)).toString(true) : '', 169 | logPath: logFilePath, 170 | logData: logData, 171 | }, 172 | }); 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /lana/src/commands/RetrieveLogFile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { type LogRecord } from '@salesforce/apex-node'; 5 | import { existsSync } from 'fs'; 6 | import { join, parse } from 'path'; 7 | import { window, type WebviewPanel } from 'vscode'; 8 | 9 | import { appName } from '../AppSettings.js'; 10 | import { Context } from '../Context.js'; 11 | import { Item, Options, QuickPick } from '../display/QuickPick.js'; 12 | import { QuickPickWorkspace } from '../display/QuickPickWorkspace.js'; 13 | import { GetLogFile } from '../salesforce/logs/GetLogFile.js'; 14 | import { GetLogFiles } from '../salesforce/logs/GetLogFiles.js'; 15 | import { Command } from './Command.js'; 16 | import { LogView } from './LogView.js'; 17 | 18 | class DebugLogItem extends Item { 19 | logId: string; 20 | 21 | constructor( 22 | name: string, 23 | desc: string, 24 | details: string, 25 | logId: string, 26 | sticky = true, 27 | selected = false, 28 | ) { 29 | super(name, desc, details, sticky, selected); 30 | this.logId = logId; 31 | } 32 | } 33 | 34 | export class RetrieveLogFile { 35 | static apply(context: Context): void { 36 | new Command('retrieveLogFile', 'Log: Retrieve Apex Log And Show Analysis', () => 37 | RetrieveLogFile.safeCommand(context), 38 | ).register(context); 39 | context.display.output(`Registered command '${appName}: Retrieve Log'`); 40 | } 41 | 42 | private static async safeCommand(context: Context): Promise<WebviewPanel | void> { 43 | try { 44 | return RetrieveLogFile.command(context); 45 | } catch (err: unknown) { 46 | const msg = err instanceof Error ? err.message : String(err); 47 | context.display.showErrorMessage(`Error loading logfile: ${msg}`); 48 | return Promise.resolve(); 49 | } 50 | } 51 | 52 | private static async command(context: Context): Promise<WebviewPanel | void> { 53 | const ws = await QuickPickWorkspace.pickOrReturn(context); 54 | const [logFiles] = await Promise.all([ 55 | GetLogFiles.apply(ws), 56 | RetrieveLogFile.showLoadingPicker(), 57 | ]); 58 | 59 | const logFileId = await RetrieveLogFile.getLogFile(logFiles); 60 | if (logFileId) { 61 | const logFilePath = this.getLogFilePath(ws, logFileId); 62 | const writeLogFile = this.writeLogFile(ws, logFilePath); 63 | return LogView.createView(context, writeLogFile, logFilePath); 64 | } 65 | } 66 | 67 | private static async showLoadingPicker(): Promise<QuickPick> { 68 | const qp = window.createQuickPick(); 69 | qp.placeholder = 'Select a logfile'; 70 | qp.busy = true; 71 | qp.enabled = false; 72 | qp.show(); 73 | return qp; 74 | } 75 | 76 | private static async getLogFile(files: LogRecord[]): Promise<string | null> { 77 | const items = files 78 | .sort((a, b) => { 79 | const aDate = Date.parse(a.StartTime); 80 | const bDate = Date.parse(b.StartTime); 81 | return bDate - aDate; 82 | }) 83 | .map((r) => { 84 | const name = `${r.LogUser.Name} - ${r.Operation}`; 85 | const description = `${(r.LogLength / 1024).toFixed(2)} KB ${r.DurationMilliseconds} ms`; 86 | const detail = `${new Date(r.StartTime).toLocaleString()} - ${r.Status} - ${r.Id}`; 87 | return new DebugLogItem(name, description, detail, r.Id); 88 | }); 89 | 90 | const [selectedLog] = await QuickPick.pick(items, new Options('Select a logfile')); 91 | return selectedLog?.logId || null; 92 | } 93 | 94 | private static getLogFilePath(ws: string, fileId: string): string { 95 | const logDirectory = join(ws, '.sfdx', 'tools', 'debug', 'logs'); 96 | const logFilePath = join(logDirectory, `${fileId}.log`); 97 | return logFilePath; 98 | } 99 | 100 | private static async writeLogFile(ws: string, logPath: string) { 101 | const logExists = existsSync(logPath); 102 | if (!logExists) { 103 | const logfilePath = parse(logPath); 104 | await GetLogFile.apply(ws, logfilePath.dir, logfilePath.name); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /lana/src/commands/ShowLogAnalysis.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { existsSync } from 'fs'; 5 | import { Uri, window } from 'vscode'; 6 | 7 | import { appName } from '../AppSettings.js'; 8 | import { Context } from '../Context.js'; 9 | import { Command } from './Command.js'; 10 | import { LogView } from './LogView.js'; 11 | 12 | export class ShowLogAnalysis { 13 | static getCommand(context: Context): Command { 14 | return new Command('showLogAnalysis', 'Log: Show Apex Log Analysis', (uri: Uri) => 15 | ShowLogAnalysis.safeCommand(context, uri), 16 | ); 17 | } 18 | 19 | static apply(context: Context): void { 20 | ShowLogAnalysis.getCommand(context).register(context); 21 | context.display.output(`Registered command '${appName}: Show Log'`); 22 | } 23 | 24 | private static async safeCommand(context: Context, uri: Uri): Promise<void> { 25 | try { 26 | return ShowLogAnalysis.command(context, uri); 27 | } catch (err: unknown) { 28 | const msg = err instanceof Error ? err.message : String(err); 29 | context.display.showErrorMessage(`Error showing logfile: ${msg}`); 30 | return Promise.resolve(); 31 | } 32 | } 33 | 34 | private static async command(context: Context, uri: Uri): Promise<void> { 35 | const filePath = uri?.fsPath || window?.activeTextEditor?.document.fileName || ''; 36 | const fileContent = !existsSync(filePath) ? window?.activeTextEditor?.document.getText() : ''; 37 | 38 | if (filePath || fileContent) { 39 | LogView.createView(context, Promise.resolve(), filePath, fileContent); 40 | } else { 41 | context.display.showErrorMessage( 42 | 'No file selected or the file is too large. Try again using the file explorer or text editor command.', 43 | ); 44 | throw new Error( 45 | 'No file selected or the file is too large. Try again using the file explorer or text editor command.', 46 | ); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /lana/src/display/Display.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { Uri, commands, window, type MessageOptions } from 'vscode'; 5 | 6 | import { appName } from '../AppSettings.js'; 7 | 8 | export class Display { 9 | private outputChannel = window.createOutputChannel(appName); 10 | 11 | output(message: string, showChannel = false) { 12 | if (showChannel) { 13 | this.outputChannel.show(true); 14 | } 15 | this.outputChannel.appendLine(message); 16 | } 17 | 18 | showInformationMessage(s: string): void { 19 | window.showInformationMessage(s); 20 | } 21 | 22 | showErrorMessage(s: string, options: MessageOptions = {}): void { 23 | window.showErrorMessage(s, options); 24 | } 25 | 26 | showFile(path: string): void { 27 | commands.executeCommand('vscode.open', Uri.file(path.trim())); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /lana/src/display/OpenFileInPackage.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { sep } from 'path'; 5 | import { 6 | Position, 7 | Selection, 8 | Uri, 9 | ViewColumn, 10 | commands, 11 | type TextDocumentShowOptions, 12 | } from 'vscode'; 13 | 14 | import { Context } from '../Context.js'; 15 | import { Item, Options, QuickPick } from './QuickPick.js'; 16 | 17 | export class OpenFileInPackage { 18 | static async openFileForSymbol( 19 | context: Context, 20 | name: string, 21 | lineNumber?: number, 22 | ): Promise<void> { 23 | const paths = await context.findSymbol(name); 24 | if (!paths.length) { 25 | return; 26 | } 27 | const matchingWs = context.workspaces.filter((ws) => { 28 | const found = paths.findIndex((p) => p.startsWith(ws.path())); 29 | if (found > -1) { 30 | return ws; 31 | } 32 | }); 33 | 34 | const [wsPath] = 35 | matchingWs.length > 1 36 | ? await QuickPick.pick( 37 | matchingWs.map((p) => new Item(p.name(), p.path(), '')), 38 | new Options('Select a workspace:'), 39 | ) 40 | : [new Item(matchingWs[0]?.name() || '', matchingWs[0]?.path() || '', '')]; 41 | 42 | if (wsPath && lineNumber) { 43 | const zeroBasedLine = lineNumber - 1; 44 | const linePosition = new Position(zeroBasedLine, 0); 45 | 46 | const options: TextDocumentShowOptions = { 47 | preserveFocus: false, 48 | preview: false, 49 | viewColumn: ViewColumn.Active, 50 | selection: new Selection(linePosition, linePosition), 51 | }; 52 | 53 | const wsPathTrimmed = wsPath.description.trim(); 54 | const path = 55 | paths.find((e) => { 56 | return e.startsWith(wsPathTrimmed + sep); 57 | }) || ''; 58 | commands.executeCommand('vscode.open', Uri.file(path), options); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /lana/src/display/QuickPick.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { window, type QuickPickItem, type QuickPickOptions } from 'vscode'; 5 | 6 | export class Item implements QuickPickItem { 7 | label: string; 8 | description: string; 9 | detail: string; 10 | picked: boolean; 11 | alwaysShow: boolean; 12 | 13 | constructor(name: string, desc: string, details: string, sticky = true, selected = false) { 14 | this.label = name; 15 | this.description = desc; 16 | this.detail = details; 17 | this.picked = selected; 18 | this.alwaysShow = sticky; 19 | } 20 | } 21 | 22 | export class Options implements QuickPickOptions { 23 | canPickMany: boolean; 24 | ignoreFocusOut: boolean; 25 | placeHolder: string; 26 | 27 | constructor(placeholder: string, ignoreDefocus = false, multiSelect = false) { 28 | this.placeHolder = placeholder; 29 | this.ignoreFocusOut = ignoreDefocus; 30 | this.canPickMany = multiSelect; 31 | } 32 | } 33 | 34 | export class QuickPick { 35 | static async pick<T extends Item, U extends Options>(items: T[], options: U): Promise<T[]> { 36 | return QuickPick.showQuickPick(items, options).then((oneOrMany) => { 37 | if (oneOrMany) { 38 | return options.canPickMany ? (oneOrMany as T[]) : [oneOrMany as T]; 39 | } 40 | return []; 41 | }); 42 | } 43 | 44 | static async showQuickPick<T extends QuickPickItem>( 45 | items: T[], 46 | options: QuickPickOptions, 47 | ): Promise<T | T[] | undefined> { 48 | return window.showQuickPick<T>(items, options, undefined); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /lana/src/display/QuickPickWorkspace.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { parse } from 'path'; 5 | import { window } from 'vscode'; 6 | 7 | import { Context } from '../Context.js'; 8 | import { Item, Options, QuickPick } from './QuickPick.js'; 9 | 10 | export class QuickPickWorkspace { 11 | static async pickOrReturn(context: Context): Promise<string> { 12 | if (context.workspaces.length > 1) { 13 | const [workspace] = await QuickPick.pick( 14 | context.workspaces.map((ws) => new Item(ws.name(), ws.path(), '')), 15 | new Options('Select a workspace:'), 16 | ); 17 | 18 | if (workspace) { 19 | return workspace.description; 20 | } else { 21 | throw new Error('No workspace selected'); 22 | } 23 | } else if (context.workspaces.length === 1) { 24 | return context.workspaces[0]?.path() || ''; 25 | } else { 26 | if (window.activeTextEditor) { 27 | return parse(window.activeTextEditor.document.fileName).dir; 28 | } else { 29 | throw new Error('No workspace selected'); 30 | } 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /lana/src/display/WebView.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { Uri, window, type WebviewPanel, type WebviewPanelOptions } from 'vscode'; 5 | 6 | export class WebView { 7 | static apply(name: string, title: string, resourceRoots: Uri[]): WebviewPanel { 8 | return window.createWebviewPanel(name, title, -1, new WebViewOptions(resourceRoots)); 9 | } 10 | } 11 | 12 | class WebViewOptions implements WebviewPanelOptions { 13 | enableCommandUris = true; 14 | enableScripts = true; 15 | retainContextWhenHidden = true; 16 | localResourceRoots: Uri[]; 17 | enableFindWidget = false; 18 | 19 | constructor(resourceRoots: Uri[]) { 20 | this.localResourceRoots = resourceRoots; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lana/src/display/WhatsNewNotification.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { commands, window } from 'vscode'; 5 | 6 | import { Context } from '../Context.js'; 7 | 8 | export class WhatsNewNotification { 9 | static async apply(context: Context): Promise<void> { 10 | const extensionInfo = context.context.extension; 11 | const versionNumber: string[] = extensionInfo.packageJSON.version.split(/[.-]/); 12 | const versionText = versionNumber.slice(0, 3).join('.'); 13 | 14 | const changeLogViewedkey = 'update.confirmed.versions'; 15 | const changelogViewedVersions = 16 | context.context.globalState.get<string[]>(changeLogViewedkey) || []; 17 | 18 | // Only show the whats new notification if this is a minor version or larger (not a bug fix) + if the notification for this minor has not been dismissed or viewed already. 19 | if (versionNumber[2] !== '0' || changelogViewedVersions.includes(versionText)) { 20 | return; 21 | } 22 | 23 | const extensionId = extensionInfo.id; 24 | const whatsNew = "See What's New"; 25 | window 26 | .showInformationMessage("Apex Log Analyzer has been updated. See What's New.", whatsNew) 27 | .then((selection) => { 28 | if (selection === whatsNew) { 29 | commands.executeCommand('extension.open', extensionId, 'changelog'); 30 | } 31 | }); 32 | 33 | // if whats new was clicked, dismissed or timed out we do not want to show the notification again so register this version in the change log viewed state. 34 | context.context.globalState.update(changeLogViewedkey, [versionText]); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /lana/src/salesforce/codesymbol/SymbolFinder.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { type Workspace } from '@apexdevtools/apex-ls'; 5 | 6 | import { VSWorkspace } from '../../workspace/VSWorkspace.js'; 7 | 8 | export class SymbolFinder { 9 | async findSymbol(workspaces: VSWorkspace[], symbol: string): Promise<string[]> { 10 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 11 | // eslint-disable-next-line @typescript-eslint/naming-convention 12 | const { Workspaces } = await import('@apexdevtools/apex-ls'); 13 | const paths = []; 14 | for (const ws of workspaces) { 15 | const apexWs = Workspaces.get(ws.path()); 16 | const filePath = this.findInWorkspace(apexWs, symbol); 17 | if (filePath) { 18 | paths.push(filePath); 19 | } 20 | } 21 | 22 | return paths; 23 | } 24 | 25 | private findInWorkspace(ws: Workspace, symbol: string): string | null { 26 | const paths = ws.findType(symbol); 27 | if (paths.length === 0) { 28 | const parts = symbol.split('.'); 29 | if (parts.length > 1) { 30 | parts.pop(); 31 | return this.findInWorkspace(ws, parts.join('.')); 32 | } 33 | return null; 34 | } 35 | return paths.find((path) => path.endsWith('.cls')) || null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /lana/src/salesforce/logs/GetLogFile.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | 5 | export class GetLogFile { 6 | static async apply(wsPath: string, logDir: string, logId: string): Promise<void> { 7 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 8 | // eslint-disable-next-line @typescript-eslint/naming-convention 9 | const { AuthHelper } = await import('@apexdevtools/sfdx-auth-helper'); 10 | 11 | const ah = await AuthHelper.instance(wsPath); 12 | const connection = await ah.connect(await ah.getDefaultUsername()); 13 | 14 | if (connection) { 15 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 16 | // eslint-disable-next-line @typescript-eslint/naming-convention 17 | const { LogService } = await import('@salesforce/apex-node'); 18 | await new LogService(connection).getLogs({ logId: logId, outputDir: logDir }); 19 | } 20 | return new Promise((resolve) => resolve()); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lana/src/salesforce/logs/GetLogFiles.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { type LogRecord } from '@salesforce/apex-node'; 5 | 6 | export class GetLogFiles { 7 | static async apply(wsPath: string): Promise<LogRecord[]> { 8 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 9 | // eslint-disable-next-line @typescript-eslint/naming-convention 10 | const { AuthHelper } = await import('@apexdevtools/sfdx-auth-helper'); 11 | const ah = await AuthHelper.instance(wsPath); 12 | const connection = await ah.connect(await ah.getDefaultUsername()); 13 | 14 | if (connection) { 15 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 16 | // eslint-disable-next-line @typescript-eslint/naming-convention 17 | const { LogService } = await import('@salesforce/apex-node'); 18 | return new LogService(connection).getLogRecords(); 19 | } 20 | return []; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lana/src/workspace/VSWorkspace.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { type WorkspaceFolder } from 'vscode'; 5 | 6 | export class VSWorkspace { 7 | workspaceFolder: WorkspaceFolder; 8 | 9 | constructor(workspaceFolder: WorkspaceFolder) { 10 | this.workspaceFolder = workspaceFolder; 11 | } 12 | 13 | path(): string { 14 | return this.workspaceFolder.uri.fsPath; 15 | } 16 | name(): string { 17 | return this.workspaceFolder.name; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lana/tsconfig-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { "skipLibCheck": true } 4 | } 5 | -------------------------------------------------------------------------------- /lana/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2022"], 4 | "outDir": "out", 5 | "rootDir": "src", 6 | "esModuleInterop": true, 7 | "skipLibCheck": true, 8 | "target": "es2022", 9 | "verbatimModuleSyntax": true, 10 | "allowJs": true, 11 | "resolveJsonModule": true, 12 | "moduleDetection": "force", 13 | 14 | "strict": true, 15 | "noUncheckedIndexedAccess": true, 16 | 17 | "moduleResolution": "Bundler", 18 | "module": "ESNext", 19 | "noEmit": true, 20 | 21 | "composite": true, 22 | "declarationMap": true, 23 | 24 | "strictFunctionTypes": true, 25 | 26 | "allowSyntheticDefaultImports": true, 27 | "experimentalDecorators": true, 28 | "useDefineForClassFields": false, 29 | "forceConsistentCasingInFileNames": true, 30 | "importHelpers": true, 31 | "isolatedModules": true, 32 | "noEmitOnError": true, 33 | "noUnusedLocals": false, 34 | "noUnusedParameters": false, 35 | "removeComments": true, 36 | "sourceMap": false 37 | }, 38 | "include": ["./src/**/*.ts"], 39 | "exclude": ["**/node_modules", "**/.*/"] 40 | } 41 | -------------------------------------------------------------------------------- /log-viewer/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | 3 | node_modules/* 4 | out/ 5 | 6 | tsconfig.tsbuildinfo 7 | -------------------------------------------------------------------------------- /log-viewer/.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "pwa-chrome", 9 | "request": "launch", 10 | "name": "Launch Chrome against localhost", 11 | "url": "http://localhost:8080/out/index.html", 12 | "webRoot": "${workspaceFolder}" 13 | }, 14 | { 15 | "type": "node", 16 | "request": "launch", 17 | "name": "Jest Tests", 18 | "program": "${workspaceRoot}/node_modules/jest/bin/jest.js", 19 | "args": ["-i", "TreeView.test.ts"], 20 | "internalConsoleOptions": "openOnSessionStart", 21 | "outFiles": ["${workspaceRoot}/dist/**/*"], 22 | "envFile": "${workspaceRoot}/.env" 23 | } 24 | ] 25 | } 26 | -------------------------------------------------------------------------------- /log-viewer/declarations.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.scss' { 2 | const content: { [className: string]: string }; 3 | export = content; 4 | } 5 | 6 | declare module '*.css' { 7 | const content: { [className: string]: string }; 8 | export = content; 9 | } 10 | -------------------------------------------------------------------------------- /log-viewer/index.html: -------------------------------------------------------------------------------- 1 | <!DOCTYPE html> 2 | <html lang="en"> 3 | 4 | <head> 5 | <meta charset="UTF-8" /> 6 | <meta name="viewport" content="width=device-width, initial-scale=1" /> 7 | <title>Log Viewer 8 | 10 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /log-viewer/modules/Database.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { ApexLog, DMLBeginLine, Method, SOQLExecuteBeginLine } from './parsers/ApexLogParser.js'; 5 | 6 | export type Stack = Method[]; 7 | 8 | export class DatabaseAccess { 9 | private static _instance: DatabaseAccess | null = null; 10 | private static _treeRoot: ApexLog; 11 | 12 | static async create(rootMethod: ApexLog): Promise { 13 | const databaseAccess = new DatabaseAccess(); 14 | this._treeRoot = rootMethod; 15 | this._instance = databaseAccess; 16 | return this._instance; 17 | } 18 | 19 | static instance(): DatabaseAccess | null { 20 | return DatabaseAccess._instance; 21 | } 22 | 23 | public getStack( 24 | timestamp: number, 25 | stack: Stack = [], 26 | line: Method = DatabaseAccess._treeRoot, 27 | ): Stack { 28 | const children = line.children; 29 | const len = children.length; 30 | for (let i = 0; i < len; ++i) { 31 | const child = children[i]; 32 | if (child instanceof Method) { 33 | stack.push(child); 34 | if (child.timestamp === timestamp) { 35 | return stack; 36 | } 37 | 38 | const childStack = this.getStack(timestamp, stack, child); 39 | if (childStack.length > 0) { 40 | return childStack; 41 | } 42 | stack.pop(); 43 | } 44 | } 45 | return []; 46 | } 47 | 48 | public getSOQLLines(line: Method = DatabaseAccess._treeRoot): SOQLExecuteBeginLine[] { 49 | const results: SOQLExecuteBeginLine[] = []; 50 | 51 | const children = line.children; 52 | const len = children.length; 53 | for (let i = 0; i < len; ++i) { 54 | const child = children[i]; 55 | if (child instanceof SOQLExecuteBeginLine) { 56 | results.push(child); 57 | } 58 | 59 | if (child instanceof Method) { 60 | Array.prototype.push.apply(results, this.getSOQLLines(child)); 61 | } 62 | } 63 | 64 | return results; 65 | } 66 | 67 | public getDMLLines(line: Method = DatabaseAccess._treeRoot): DMLBeginLine[] { 68 | const results: DMLBeginLine[] = []; 69 | 70 | const children = line.children; 71 | const len = children.length; 72 | for (let i = 0; i < len; ++i) { 73 | const child = children[i]; 74 | if (child instanceof DMLBeginLine) { 75 | results.push(child); 76 | } 77 | 78 | if (child instanceof Method) { 79 | // results = results.concat(this.getDMLLines(child)); 80 | Array.prototype.push.apply(results, this.getDMLLines(child)); 81 | } 82 | } 83 | 84 | return results; 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /log-viewer/modules/Main.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { html, render } from 'lit'; 5 | 6 | import './components/LogViewer'; 7 | 8 | function onInit(): void { 9 | render(html``, document.body); 10 | } 11 | 12 | window.addEventListener('DOMContentLoaded', onInit, { once: true }); 13 | -------------------------------------------------------------------------------- /log-viewer/modules/Util.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | let requestId: number = 0; 5 | 6 | export default function formatDuration(durationNs: number, totalNs = 0) { 7 | const text = `${~~(durationNs / 1000)}`; // convert from nano-seconds to micro-seconds 8 | const textPadded = text.length < 4 ? '0000'.substring(text.length) + text : text; // length min = 4 9 | const millis = textPadded.slice(0, -3); // everything before last 3 chars 10 | const micros = textPadded.slice(-3); // last 3 chars 11 | const suffix = totalNs > 0 ? `/${Math.round(totalNs / 1_000_000)}` : ''; 12 | return `${millis}.${micros}${suffix} ms`; 13 | } 14 | 15 | export function debounce(callBack: (...args: T) => unknown) { 16 | if (requestId) { 17 | window.cancelAnimationFrame(requestId); 18 | } 19 | 20 | return (...args: T) => { 21 | requestId = window.requestAnimationFrame(() => { 22 | callBack(...args); 23 | }); 24 | }; 25 | } 26 | 27 | export async function isVisible( 28 | element: HTMLElement, 29 | options?: IntersectionObserverInit, 30 | ): Promise { 31 | return new Promise((resolve) => { 32 | const observer = new IntersectionObserver((entries, observerInstance) => { 33 | for (const entry of entries) { 34 | if (entry.isIntersecting) { 35 | resolve(true); 36 | observerInstance.disconnect(); 37 | return; 38 | } 39 | } 40 | }, options); 41 | 42 | observer.observe(element); 43 | }); 44 | } 45 | -------------------------------------------------------------------------------- /log-viewer/modules/__tests__/Database.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { DatabaseAccess } from '../Database.js'; 5 | import { parse } from '../parsers/ApexLogParser.js'; 6 | 7 | describe('Analyse database tests', () => { 8 | it('Only DML and SOQL are collected', async () => { 9 | const log = 10 | '09:18:22.6 (6508409)|USER_INFO|[EXTERNAL]|0050W000006W3LM|user@example.com|Greenwich Mean Time|GMT+01:00\n' + 11 | '09:18:22.6 (6574780)|EXECUTION_STARTED\n' + 12 | '09:18:22.6 (6586704)|CODE_UNIT_STARTED|[EXTERNAL]|066d0000002m8ij|pse.VFRemote: pse.SenchaTCController invoke(saveTimecard)\n' + 13 | '17:33:36.2 (1672655920)|SOQL_EXECUTE_BEGIN|[198]|Aggregations:0|SELECT Id FROM Account\n' + 14 | '17:33:36.2 (1678684460)|SOQL_EXECUTE_END|[198]|Rows:3\n' + 15 | '07:54:17.2 (1684126610)|DML_BEGIN|[774]|Op:Insert|Type:codaCompany__c|Rows:2\n' + 16 | '09:19:13.82 (51592737891)|CODE_UNIT_FINISHED|pse.VFRemote: pse.SenchaTCController invoke(saveTimecard)\n' + 17 | '09:19:13.82 (51595120059)|EXECUTION_FINISHED\n'; 18 | 19 | const apexLog = parse(log); 20 | const result = await DatabaseAccess.create(apexLog); 21 | 22 | const firstSOQL = result.getSOQLLines()[0]; 23 | expect(firstSOQL?.text).toEqual('SELECT Id FROM Account'); 24 | 25 | const firstDML = result.getDMLLines()[0]; 26 | expect(firstDML?.text).toEqual('DML Op:Insert Type:codaCompany__c'); 27 | }); 28 | }); 29 | -------------------------------------------------------------------------------- /log-viewer/modules/__tests__/SOQLParser.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | */ 4 | import { SOQLParser, SyntaxException } from '../soql/SOQLParser.js'; 5 | 6 | describe('Analyse database tests', () => { 7 | it('throws on unparsable query', async () => { 8 | const parser = new SOQLParser(); 9 | try { 10 | await parser.parse(''); 11 | expect(true).toBe(false); 12 | } catch (ex) { 13 | expect(ex).toEqual(new SyntaxException(1, 0, "mismatched input '' expecting 'select'")); 14 | } 15 | }); 16 | 17 | it('extracts simple FROM object', async () => { 18 | const parser = new SOQLParser(); 19 | const tree = await parser.parse('SELECT Id FROM Account'); 20 | expect(tree.fromObject()).toEqual('Account'); 21 | }); 22 | 23 | it('determines if only fields are being selected', async () => { 24 | const parser = new SOQLParser(); 25 | const tree = await parser.parse('SELECT Id FROM Account'); 26 | expect(tree.isSimpleSelect()).toEqual(true); 27 | }); 28 | 29 | it('determines if none-fields are being selected', async () => { 30 | const parser = new SOQLParser(); 31 | const tree = await parser.parse('SELECT Count(Id) FROM Account'); 32 | expect(tree.isSimpleSelect()).toEqual(false); 33 | }); 34 | 35 | it('determines if none-trival clauses are being used', async () => { 36 | const parser = new SOQLParser(); 37 | const tree = await parser.parse('SELECT Id FROM Account GROUP BY Name LIMIT 2'); 38 | expect(tree.isTrivialQuery()).toEqual(false); 39 | }); 40 | 41 | it('determines no LIMIT', async () => { 42 | const parser = new SOQLParser(); 43 | const tree = await parser.parse('SELECT Id FROM Account'); 44 | expect(tree.limitValue()).toEqual(undefined); 45 | }); 46 | 47 | it('determines LIMIT number', async () => { 48 | const parser = new SOQLParser(); 49 | const tree = await parser.parse('SELECT Id FROM Account LIMIT 2'); 50 | expect(tree.limitValue()).toEqual(2); 51 | }); 52 | 53 | it('determines LIMIT expression', async () => { 54 | const parser = new SOQLParser(); 55 | const tree = await parser.parse('SELECT Id FROM Account LIMIT :tmp'); 56 | expect(tree.limitValue()).toEqual(':tmp'); 57 | }); 58 | 59 | it('determines if does not have ORDER BY', async () => { 60 | const parser = new SOQLParser(); 61 | const tree = await parser.parse('SELECT Id FROM Account'); 62 | expect(tree.isOrdered()).toEqual(false); 63 | }); 64 | 65 | it('determines if has ORDER BY', async () => { 66 | const parser = new SOQLParser(); 67 | const tree = await parser.parse('SELECT Id FROM Account ORDER BY Name'); 68 | expect(tree.isOrdered()).toEqual(true); 69 | }); 70 | }); 71 | -------------------------------------------------------------------------------- /log-viewer/modules/__tests__/Util.test.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2020 Certinia Inc. All rights reserved. 3 | * @jest-environment jsdom 4 | */ 5 | import formatDuration from '../Util.js'; 6 | 7 | describe('Format duration tests', () => { 8 | it('Value converted from nanoseconds to milliseconds', () => { 9 | expect(formatDuration(1000)).toBe('0.001 ms'); 10 | }); 11 | it('Value always has 3dp', () => { 12 | expect(formatDuration(1000000)).toBe('1.000 ms'); 13 | }); 14 | it('Value truncated at 3dp', () => { 15 | expect(formatDuration(1234567)).toBe('1.234 ms'); 16 | }); 17 | it('pads microseconds correctly for short durations', () => { 18 | expect(formatDuration(5)).toBe('0.000 ms'); 19 | expect(formatDuration(50)).toBe('0.000 ms'); 20 | expect(formatDuration(500)).toBe('0.000 ms'); 21 | }); 22 | }); 23 | 24 | describe('Adds out off suffix', () => { 25 | it('rounds up to 0dp', () => { 26 | expect(formatDuration(1000, 2_000_600_000)).toBe('0.001/2001 ms'); 27 | }); 28 | 29 | it('handles zero duration and total', () => { 30 | expect(formatDuration(0, 0)).toBe('0.000 ms'); 31 | }); 32 | 33 | it('handles zero duration with totalNs', () => { 34 | expect(formatDuration(0, 1_000_000)).toBe('0.000/1 ms'); 35 | }); 36 | 37 | it('handles large duration and totalNs', () => { 38 | expect(formatDuration(12_345_678_900, 123_456_789_000)).toBe('12345.678/123457 ms'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /log-viewer/modules/components/AppHeader.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { 5 | provideVSCodeDesignSystem, 6 | vsCodePanelTab, 7 | vsCodePanelView, 8 | vsCodePanels, 9 | } from '@vscode/webview-ui-toolkit'; 10 | import { LitElement, css, html, unsafeCSS } from 'lit'; 11 | import { customElement, property, state } from 'lit/decorators.js'; 12 | 13 | import codiconStyles from '../styles/codicon.css'; 14 | import { globalStyles } from '../styles/global.styles.js'; 15 | import { ApexLog, type TimelineGroup } from '../timeline/Timeline.js'; 16 | import '../timeline/TimelineView.js'; 17 | import './analysis-view/AnalysisView.js'; 18 | import './calltree-view/CalltreeView'; 19 | import './database-view/DatabaseView.js'; 20 | import './find-widget/FindWidget.js'; 21 | import './LogLevels.js'; 22 | import './NavBar.js'; 23 | import { Notification } from './notifications/NotificationPanel.js'; 24 | 25 | provideVSCodeDesignSystem().register(vsCodePanelTab(), vsCodePanelView(), vsCodePanels()); 26 | 27 | @customElement('app-header') 28 | export class AppHeader extends LitElement { 29 | @property({ type: String }) 30 | logName = ''; 31 | @property() 32 | logPath = ''; 33 | @property() 34 | logSize = null; 35 | @property() 36 | logDuration = null; 37 | @property() 38 | logStatus = 'Processing...'; 39 | @property() 40 | notifications: Notification[] = []; 41 | @property() 42 | parserIssues: Notification[] = []; 43 | @property() 44 | timelineRoot: ApexLog | null = null; 45 | @property() 46 | timelineKeys: TimelineGroup[] = []; 47 | 48 | @state() 49 | _selectedTab = 'timeline-tab'; 50 | 51 | constructor() { 52 | super(); 53 | document.addEventListener('show-tab', (e: Event) => { 54 | this._showTabEvent(e); 55 | }); 56 | } 57 | 58 | static styles = [ 59 | globalStyles, 60 | unsafeCSS(codiconStyles), 61 | css` 62 | :host { 63 | background-color: var(--vscode-tab-activeBackground); 64 | box-shadow: inset 0 calc(max(1px, 0.0625rem) * -1) 65 | var(--vscode-panelSectionHeader-background); 66 | display: flex; 67 | flex-direction: column; 68 | height: 100%; 69 | 70 | --panel-tab-active-foreground: var(--vscode-panelTitle-activeBorder); 71 | --panel-tab-selected-text: var(--vscode-panelTitle-activeForeground, #e7e7e7); 72 | } 73 | 74 | vscode-panels { 75 | height: 100%; 76 | } 77 | 78 | vscode-panels::part(tabpanel) { 79 | overflow: auto; 80 | box-shadow: inset 0 calc(max(1px, 0.0625rem) * 1) 81 | var(--vscode-panelSectionHeader-background); 82 | } 83 | 84 | vscode-panel-view { 85 | height: 100%; 86 | } 87 | 88 | vscode-panel-tab[aria-selected='true'] { 89 | color: var(--panel-tab-selected-text); 90 | } 91 | 92 | vscode-panel-tab:hover { 93 | color: var(--panel-tab-selected-text); 94 | } 95 | `, 96 | ]; 97 | 98 | // TODO: use @change on vscode-panels to detect tab change instead of @click on 110 | 111 | 112 | 113 | 118 |   119 | Timeline 120 | 121 | 126 |   127 | Call Tree 128 | 129 | 134 |   135 | Analysis 136 | 137 | 138 |   139 | Database 140 | 141 | 142 | 143 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | `; 159 | } 160 | 161 | _showTabHTMLElem(e: Event) { 162 | const input = e.currentTarget as HTMLElement; 163 | this._showTab(input.id); 164 | } 165 | 166 | _showTabEvent(e: Event) { 167 | const tabId = (e as CustomEvent).detail.tabid; 168 | this._showTab(tabId); 169 | } 170 | 171 | _showTab(tabId: string) { 172 | if (this._selectedTab !== tabId) { 173 | this._selectedTab = tabId; 174 | 175 | // Not really happy this is here, find needs a refactor 176 | const findEvt = { 177 | detail: { 178 | text: '', 179 | count: 0, 180 | options: { matchCase: false }, 181 | }, 182 | }; 183 | document.dispatchEvent(new CustomEvent('lv-find', findEvt)); 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /log-viewer/modules/components/BadgeBase.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeTag } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html } from 'lit'; 6 | import { customElement, property } from 'lit/decorators.js'; 7 | 8 | import { globalStyles } from '../styles/global.styles.js'; 9 | import { skeletonStyles } from './skeleton/skeleton.styles.js'; 10 | 11 | provideVSCodeDesignSystem().register(vsCodeTag()); 12 | 13 | @customElement('badge-base') 14 | export class BadgeBase extends LitElement { 15 | @property() 16 | status: 'success' | 'failure' | 'neutral' = 'neutral'; 17 | 18 | @property({ type: Boolean }) 19 | isloading = false; 20 | 21 | colorMap = new Map([ 22 | ['success', 'success-tag'], 23 | ['failure', 'failure-tag'], 24 | ]); 25 | 26 | static styles = [ 27 | globalStyles, 28 | skeletonStyles, 29 | css` 30 | :host { 31 | } 32 | 33 | .tag { 34 | font-family: monospace; 35 | font-size: inherit; 36 | } 37 | 38 | .tag::part(control) { 39 | color: var(--vscode-editor-foreground); 40 | background-color: var(--button-icon-hover-background, rgba(90, 93, 94, 0.31)); 41 | text-transform: inherit; 42 | border: none; 43 | } 44 | .success-tag::part(control) { 45 | background-color: rgba(128, 255, 128, 0.2); 46 | } 47 | 48 | .failure-tag::part(control) { 49 | background-color: var(--notification-error-background); 50 | } 51 | `, 52 | ]; 53 | 54 | render() { 55 | if (this.isloading) { 56 | return html` `; 57 | } 58 | const statusTag = this.colorMap.get(this.status); 59 | return html``; 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /log-viewer/modules/components/CallStack.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { DatabaseAccess } from '../Database.js'; 8 | import { LogLine } from '../parsers/ApexLogParser.js'; 9 | import { globalStyles } from '../styles/global.styles.js'; 10 | import { goToRow } from './calltree-view/CalltreeView.js'; 11 | 12 | @customElement('call-stack') 13 | export class CallStack extends LitElement { 14 | @property({ type: Number }) 15 | timestamp = -1; 16 | @property({ type: Number }) 17 | startDepth = 1; 18 | @property({ type: Number }) 19 | endDepth = -1; 20 | 21 | static styles = [ 22 | globalStyles, 23 | css` 24 | :host { 25 | overflow: hidden; 26 | min-width: 0%; 27 | min-height: 1ch; 28 | max-height: 30vh; 29 | padding: 0px 5px 0px 5px; 30 | white-space: normal; 31 | } 32 | 33 | :host(:hover) { 34 | overflow: scroll; 35 | } 36 | 37 | details summary { 38 | cursor: pointer; 39 | } 40 | 41 | details summary > * { 42 | display: inline; 43 | } 44 | 45 | .callstack { 46 | display: flex; 47 | flex-direction: column; 48 | } 49 | 50 | .callstack__item { 51 | cursor: pointer; 52 | } 53 | 54 | .code_text { 55 | font-family: monospace; 56 | font-weight: var(--vscode-font-weight, normal); 57 | font-size: var(--vscode-editor-font-size, 0.9em); 58 | } 59 | `, 60 | ]; 61 | 62 | render() { 63 | const stack = DatabaseAccess.instance()?.getStack(this.timestamp).reverse() || []; 64 | if (stack.length) { 65 | const details = stack.slice(this.startDepth, this.endDepth).map((entry) => { 66 | return this.lineLink(entry); 67 | }); 68 | 69 | if (details.length === 1) { 70 | return details; 71 | } 72 | 73 | return html`
74 | ${details[0]} 75 |
${details.slice(1, -1)}
76 |
`; 77 | } else { 78 | return html`
No call stack available
`; 79 | } 80 | } 81 | 82 | private lineLink(line: LogLine) { 83 | return html` 84 | ${line.text} 90 | `; 91 | } 92 | 93 | private onCallerClick(evt: Event) { 94 | const { type } = window.getSelection() ?? {}; 95 | if (type === 'Range') { 96 | return; 97 | } 98 | 99 | evt.stopPropagation(); 100 | evt.preventDefault(); 101 | const target = evt.target as HTMLElement; 102 | const dataTimestamp = target.getAttribute('data-timestamp'); 103 | if (dataTimestamp) { 104 | goToRow(parseInt(dataTimestamp)); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /log-viewer/modules/components/LogLevels.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { DebugLevel } from '../parsers/ApexLogParser.js'; 8 | import { globalStyles } from '../styles/global.styles.js'; 9 | import { skeletonStyles } from './skeleton/skeleton.styles.js'; 10 | 11 | @customElement('log-levels') 12 | export class LogLevels extends LitElement { 13 | @property() 14 | logSettings: DebugLevel[] | null = null; 15 | 16 | constructor() { 17 | super(); 18 | } 19 | 20 | static styles = [ 21 | globalStyles, 22 | skeletonStyles, 23 | css` 24 | :host { 25 | display: flex; 26 | flex-wrap: wrap; 27 | gap: 4px; 28 | align-items: center; 29 | padding: 4px 0; 30 | min-height: 27px; 31 | } 32 | .setting { 33 | display: inline-block; 34 | font-family: var(--vscode-editor-font-family); 35 | background-color: var(--vscode-textBlockQuote-background); 36 | font-size: 0.9em; 37 | padding: 5px; 38 | } 39 | .setting__title { 40 | font-weight: bold; 41 | } 42 | 43 | .setting__level { 44 | color: #808080; 45 | } 46 | 47 | .setting-skeleton { 48 | min-width: 5ch; 49 | width: 100px; 50 | height: 1.5rem; 51 | } 52 | `, 53 | ]; 54 | 55 | render() { 56 | if (!this.logSettings) { 57 | const logLevels = []; 58 | for (let i = 0; i < 8; i++) { 59 | const levelHtml = html`
`; 60 | logLevels.push(levelHtml); 61 | } 62 | return html`${logLevels}`; 63 | } 64 | 65 | const logLevels = []; 66 | for (const { logCategory, logLevel } of this.logSettings) { 67 | const levelHtml = html`
68 | ${logCategory}: 69 | ${logLevel} 70 |
`; 71 | logLevels.push(levelHtml); 72 | } 73 | return html`${logLevels}`; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /log-viewer/modules/components/LogTitle.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html } from 'lit'; 6 | import { customElement, property } from 'lit/decorators.js'; 7 | 8 | import { vscodeMessenger } from '../services/VSCodeExtensionMessenger.js'; 9 | import { globalStyles } from '../styles/global.styles.js'; 10 | import { skeletonStyles } from './skeleton/skeleton.styles.js'; 11 | 12 | provideVSCodeDesignSystem().register(vsCodeButton()); 13 | 14 | @customElement('log-title') 15 | export class LogTitle extends LitElement { 16 | @property() 17 | logName = ''; 18 | 19 | @property() 20 | logPath = ''; 21 | /** 22 | * --button-icon styles come from @vscode/webview-ui-toolkit as they are hardcoded in vscode at the moment. @vscode/webview-ui-toolkit needs to be in use for these to work. 23 | */ 24 | static styles = [ 25 | globalStyles, 26 | skeletonStyles, 27 | css` 28 | :host { 29 | --text-weight-semibold: 600; 30 | display: flex; 31 | align-items: center; 32 | min-width: 4ch; 33 | min-height: 1rem; 34 | } 35 | .title-item { 36 | padding-block: 6px; 37 | padding-inline: 8px; 38 | background: var(--button-icon-background, rgba(90, 93, 94, 0.31)); 39 | border-radius: var(--button-icon-corner-radius, 5px); 40 | font-weight: var(--text-weight-semibold, 600); 41 | font-size: 1.1rem; 42 | overflow: hidden; 43 | text-overflow: ellipsis; 44 | } 45 | 46 | a { 47 | &:hover { 48 | background-color: var(--button-icon-hover-background, rgba(90, 93, 94, 0.31)); 49 | } 50 | } 51 | `, 52 | ]; 53 | 54 | render() { 55 | if (!this.logName) { 56 | return html`
 
`; 57 | } 58 | 59 | return html`${this.logName}`; 62 | } 63 | 64 | _goToLog() { 65 | vscodeMessenger.send('openPath', this.logPath); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /log-viewer/modules/components/LogViewer.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { ApexLog, parse } from '../parsers/ApexLogParser.js'; 8 | import { vscodeMessenger } from '../services/VSCodeExtensionMessenger.js'; 9 | import { globalStyles } from '../styles/global.styles.js'; 10 | import type { TimelineGroup } from '../timeline/Timeline.js'; 11 | import './AppHeader.js'; 12 | import { Notification, type NotificationSeverity } from './notifications/NotificationPanel.js'; 13 | 14 | import { keyMap, setColors } from '../timeline/Timeline.js'; 15 | 16 | @customElement('log-viewer') 17 | export class LogViewer extends LitElement { 18 | @property({ type: String }) 19 | logName = ''; 20 | @property() 21 | logPath = ''; 22 | @property() 23 | logSize: number | null = null; 24 | @property() 25 | logDuration: number | null = null; 26 | @property() 27 | logStatus = 'Processing...'; 28 | @property() 29 | notifications: Notification[] = []; 30 | @property() 31 | parserIssues: Notification[] = []; 32 | @property() 33 | timelineRoot: ApexLog | null = null; 34 | @property() 35 | timelineKeys: TimelineGroup[] = []; 36 | 37 | static styles = [ 38 | globalStyles, 39 | css` 40 | :host { 41 | width: 100%; 42 | height: 100%; 43 | } 44 | `, 45 | ]; 46 | 47 | constructor() { 48 | super(); 49 | vscodeMessenger.request('fetchLog').then((msg) => { 50 | this._handleLogFetch(msg); 51 | }); 52 | 53 | vscodeMessenger.request('getConfig').then((msg) => { 54 | setColors(msg.timeline.colors); 55 | this.timelineKeys = Array.from(keyMap.values()); 56 | }); 57 | } 58 | 59 | render() { 60 | return html` `; 71 | } 72 | 73 | async _handleLogFetch(data: LogDataEvent) { 74 | this.logName = data.logName?.trim() || ''; 75 | this.logPath = data.logPath?.trim() || ''; 76 | 77 | const logUri = data.logUri; 78 | const logData = data.logData || (await this._readLog(logUri || '')); 79 | 80 | const apexLog = parse(logData); 81 | 82 | this.logSize = apexLog.size; 83 | this.timelineRoot = apexLog; 84 | this.logDuration = apexLog.duration.total; 85 | 86 | const localNotifications = Array.from(this.notifications); 87 | apexLog.logIssues.forEach((element) => { 88 | const severity = this.toSeverity(element.type); 89 | 90 | const logMessage = new Notification(); 91 | logMessage.summary = element.summary; 92 | logMessage.message = element.description; 93 | logMessage.severity = severity; 94 | logMessage.timestamp = element.startTime || null; 95 | localNotifications.push(logMessage); 96 | }); 97 | this.notifications = localNotifications; 98 | 99 | this.parserIssues = this.parserIssuesToMessages(apexLog); 100 | this.logStatus = 'Ready'; 101 | } 102 | 103 | async _readLog(logUri: string): Promise { 104 | let msg = ''; 105 | if (logUri) { 106 | try { 107 | const response = await fetch(logUri); 108 | if (!response.ok || !response.body) { 109 | throw new Error(response.statusText || `Error reading log file: ${response.status}`); 110 | } 111 | 112 | const reader = response.body.pipeThrough(new TextDecoderStream()).getReader(); 113 | const chunks: string[] = []; 114 | while (true) { 115 | const { done, value } = await reader.read(); 116 | if (done) { 117 | break; 118 | } 119 | chunks.push(value); 120 | } 121 | return chunks.join(''); 122 | } catch (err: unknown) { 123 | msg = (err instanceof Error ? err.message : String(err)) ?? ''; 124 | } 125 | } else { 126 | msg = 'Invalid Log Path'; 127 | } 128 | 129 | const logMessage = new Notification(); 130 | logMessage.summary = 'Could not read log'; 131 | logMessage.message = msg; 132 | logMessage.severity = 'Error'; 133 | this.notifications.push(logMessage); 134 | return ''; 135 | } 136 | 137 | severity = new Map([ 138 | ['error', 'Error'], 139 | ['unexpected', 'Warning'], 140 | ['skip', 'Info'], 141 | ]); 142 | private toSeverity(errorType: 'unexpected' | 'error' | 'skip') { 143 | return this.severity.get(errorType) || 'Info'; 144 | } 145 | 146 | private parserIssuesToMessages(apexLog: ApexLog) { 147 | const issues: Notification[] = []; 148 | apexLog.parsingErrors.forEach((message) => { 149 | const isUnknownType = this.isUnknownType(message); 150 | 151 | const logMessage = new Notification(); 152 | logMessage.summary = isUnknownType ? message : message.slice(0, message.indexOf(':')); 153 | logMessage.message = isUnknownType 154 | ? html`report unsupported type` 160 | : message.slice(message.indexOf(':') + 1); 161 | 162 | issues.push(logMessage); 163 | }); 164 | return issues; 165 | } 166 | 167 | private isUnknownType(message: string) { 168 | return message.startsWith('Unsupported log event name:'); 169 | } 170 | } 171 | 172 | interface LogDataEvent { 173 | logName?: string; 174 | logUri?: string; 175 | logPath?: string; 176 | logData?: string; 177 | } 178 | 179 | /* eslint-disable @typescript-eslint/naming-convention */ 180 | interface VSCodeLanaConfig { 181 | timeline: { 182 | colors: { 183 | 'Code Unit': '#88AE58'; 184 | Workflow: '#51A16E'; 185 | Method: '#2B8F81'; 186 | Flow: '#337986'; 187 | DML: '#285663'; 188 | SOQL: '#5D4963'; 189 | 'System Method': '#5C3444'; 190 | }; 191 | }; 192 | } 193 | -------------------------------------------------------------------------------- /log-viewer/modules/components/NavBar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeButton, vsCodeTag } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html, unsafeCSS } from 'lit'; 6 | import { customElement, property } from 'lit/decorators.js'; 7 | 8 | import { vscodeMessenger } from '../services/VSCodeExtensionMessenger.js'; 9 | import codiconStyles from '../styles/codicon.css'; 10 | import { globalStyles } from '../styles/global.styles.js'; 11 | import { notificationStyles } from '../styles/notification.styles.js'; 12 | import './BadgeBase.js'; 13 | import './LogTitle.js'; 14 | import './notifications/NotificationButton.js'; 15 | import './notifications/NotificationPanel.js'; 16 | import { Notification } from './notifications/NotificationPanel.js'; 17 | import './notifications/NotificationTag.js'; 18 | 19 | provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeTag()); 20 | 21 | @customElement('nav-bar') 22 | export class NavBar extends LitElement { 23 | @property() 24 | logName = ''; 25 | 26 | @property() 27 | logPath = ''; 28 | 29 | @property() 30 | logSize = null; 31 | 32 | @property() 33 | logDuration = null; 34 | 35 | @property() 36 | logStatus = 'Processing...'; 37 | 38 | @property() 39 | notifications: Notification[] = []; 40 | 41 | @property() 42 | parserIssues: Notification[] = []; 43 | 44 | static styles = [ 45 | globalStyles, 46 | unsafeCSS(codiconStyles), 47 | css` 48 | :host { 49 | color: var(--vscode-editor-foreground); 50 | ${notificationStyles} 51 | } 52 | 53 | .navbar { 54 | padding-top: 4px; 55 | display: flex; 56 | gap: 10px; 57 | } 58 | 59 | .navbar--left { 60 | display: flex; 61 | width: 100%; 62 | position: relative; 63 | align-items: center; 64 | } 65 | .navbar--right { 66 | display: flex; 67 | flex: 1 1 auto; 68 | justify-content: flex-end; 69 | align-items: center; 70 | display: flex; 71 | } 72 | 73 | .log__information { 74 | display: flex; 75 | width: 100%; 76 | position: relative; 77 | white-space: nowrap; 78 | align-items: center; 79 | font-size: 1rem; 80 | padding: 4px 0px 4px 0px; 81 | gap: 5px; 82 | } 83 | 84 | .icon-button { 85 | width: 32px; 86 | height: 32px; 87 | } 88 | 89 | .codicon.icon { 90 | font-size: 22px; 91 | width: 20px; 92 | height: 20px; 93 | } 94 | `, 95 | ]; 96 | 97 | render() { 98 | const sizeText = this._toSize(this.logSize), 99 | elapsedText = this._toDuration(this.logDuration); 100 | 101 | const status = 102 | this.notifications.length > 0 103 | ? 'failure' 104 | : this.logStatus !== 'Processing...' 105 | ? 'success' 106 | : ''; 107 | 108 | return html` 109 | 134 | `; 135 | } 136 | 137 | _goToLog() { 138 | vscodeMessenger.send('openPath', this.logPath); 139 | } 140 | 141 | _toDuration(duration: number | null) { 142 | if (!duration && duration !== 0) { 143 | return ''; 144 | } 145 | 146 | return (duration / 1_000_000_000).toFixed(3) + 's'; 147 | } 148 | 149 | _toSize(fileSize: number | null) { 150 | if (!fileSize && fileSize !== 0) { 151 | return ''; 152 | } 153 | 154 | return (fileSize / 1000000).toFixed(2) + ' MB'; 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /log-viewer/modules/components/SOQLLinterIssues.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html, type PropertyValues, type TemplateResult } from 'lit'; 5 | import { customElement, property, state } from 'lit/decorators.js'; 6 | 7 | import { DatabaseAccess } from '../Database.js'; 8 | import { SOQLExecuteBeginLine, SOQLExecuteExplainLine } from '../parsers/ApexLogParser.js'; 9 | import { 10 | SEVERITY_TYPES, 11 | SOQLLinter, 12 | type SOQLLinterRule, 13 | type Severity, 14 | } from '../soql/SOQLLinter.js'; 15 | import { globalStyles } from '../styles/global.styles.js'; 16 | 17 | @customElement('soql-issues') 18 | export class SOQLLinterIssues extends LitElement { 19 | @property({ type: String }) 20 | soql = ''; 21 | 22 | @property({ type: Number }) 23 | timestamp = 0; 24 | 25 | @state() 26 | issues: SOQLLinterRule[] = []; 27 | 28 | static styles = [ 29 | globalStyles, 30 | css` 31 | :host { 32 | flex: 1; 33 | max-height: 30vh; 34 | overflow-y: scroll; 35 | padding: 0px 5px 0px 5px; 36 | } 37 | .title { 38 | font-weight: bold; 39 | } 40 | details { 41 | margin-bottom: 0.25em; 42 | overflow-wrap: anywhere; 43 | white-space: normal; 44 | } 45 | `, 46 | ]; 47 | 48 | async updated(changedProperties: PropertyValues): Promise { 49 | if (changedProperties.has('soql')) { 50 | const stack = DatabaseAccess.instance()?.getStack(this.timestamp).reverse() || []; 51 | const soqlLine = stack[0] as SOQLExecuteBeginLine; 52 | this.issues = this.getIssuesFromSOQLLine(soqlLine); 53 | this.issues = this.issues.concat(await new SOQLLinter().lint(this.soql, stack)); 54 | this.issues.sort((a, b) => { 55 | return SEVERITY_TYPES.indexOf(a.severity) - SEVERITY_TYPES.indexOf(b.severity); 56 | }); 57 | } 58 | } 59 | 60 | render() { 61 | const htmlText: TemplateResult[] = [ 62 | html`SOQL issues`, 63 | ]; 64 | 65 | if (this.issues.length) { 66 | const severityToEmoji = new Map( 67 | Object.entries({ 68 | error: '❌', 69 | warning: '⚠️', 70 | info: 'ℹ️', 71 | }), 72 | ); 73 | this.issues.forEach((issue) => { 74 | htmlText.push(html` 75 |
76 | 77 | ${severityToEmoji.get(issue.severity.toLowerCase())} 79 | 80 | ${issue.summary} 81 | 82 |

${issue.message}

83 |
84 | `); 85 | }); 86 | } else { 87 | htmlText.push(html`
No SOQL issues 👍
`); 88 | } 89 | 90 | return htmlText; 91 | } 92 | 93 | getIssuesFromSOQLLine(soqlLine: SOQLExecuteBeginLine | null): SOQLLinterRule[] { 94 | const soqlIssues = []; 95 | if (soqlLine) { 96 | const explain = soqlLine.children[0] as SOQLExecuteExplainLine; 97 | if (explain?.relativeCost && explain.relativeCost > 1) { 98 | soqlIssues.push(new ExplainLineSelectivityRule(explain.relativeCost)); 99 | } 100 | } 101 | return soqlIssues; 102 | } 103 | } 104 | 105 | class ExplainLineSelectivityRule implements SOQLLinterRule { 106 | message = ''; 107 | severity: Severity = 'Error'; 108 | summary = 'Query is not selective.'; 109 | constructor(relativeCost: number) { 110 | this.message = `The relative cost of the query is ${relativeCost}.`; 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /log-viewer/modules/components/analysis-view/column-calcs/CallStackSum.ts: -------------------------------------------------------------------------------- 1 | import type { LogLine } from '../../../parsers/ApexLogParser'; 2 | import type { Metric } from '../AnalysisView.js'; 3 | 4 | export function callStackSum(_values: number[], data: Metric[], _calcParams: unknown) { 5 | const nodes: LogLine[] = []; 6 | for (const row of data) { 7 | Array.prototype.push.apply(nodes, row.nodes); 8 | } 9 | const allNodes = new Set(nodes); 10 | 11 | let total = 0; 12 | for (const node of nodes) { 13 | if (!_isChildOfOther(node, allNodes)) { 14 | total += node.duration.total; 15 | } 16 | } 17 | 18 | return total; 19 | } 20 | 21 | function _isChildOfOther(node: LogLine, filteredNodes: Set) { 22 | let parent = node.parent; 23 | while (parent) { 24 | if (filteredNodes.has(parent)) { 25 | return true; 26 | } 27 | parent = parent.parent; 28 | } 29 | 30 | return false; 31 | } 32 | -------------------------------------------------------------------------------- /log-viewer/modules/components/database-view/DatabaseSOQLDetailPanel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { globalStyles } from '../../styles/global.styles.js'; 8 | import '../CallStack.js'; 9 | import '../SOQLLinterIssues.js'; 10 | 11 | @customElement('db-soql-detail-panel') 12 | export class DatabaseSOQLDetailPanel extends LitElement { 13 | @property({ type: String }) 14 | soql = ''; 15 | @property({ type: Number }) 16 | timestamp = null; 17 | 18 | static styles = [ 19 | globalStyles, 20 | css` 21 | :host { 22 | display: flex; 23 | overflow: hidden; 24 | width: 100%; 25 | height: 100%; 26 | } 27 | 28 | call-stack { 29 | border-right: 2px solid var(--vscode-editorHoverWidget-border); 30 | } 31 | 32 | soql-issues { 33 | min-width: 25%; 34 | } 35 | `, 36 | ]; 37 | 38 | render() { 39 | return html` 40 | 41 | 42 | `; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /log-viewer/modules/components/database-view/DatabaseSection.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { Method } from '../../parsers/ApexLogParser.js'; 8 | import { globalStyles } from '../../styles/global.styles.js'; 9 | import '../BadgeBase.js'; 10 | 11 | @customElement('database-section') 12 | export class DatabaseSection extends LitElement { 13 | @property({ type: String }) 14 | title = ''; 15 | @property({ type: Object, attribute: false }) 16 | dbLines: Method[] = []; 17 | 18 | static styles = [ 19 | globalStyles, 20 | css` 21 | .dbSection { 22 | padding: 10px 5px 5px 0px; 23 | } 24 | .dbTitle { 25 | font-weight: bold; 26 | font-size: 1.2em; 27 | } 28 | .dbBlock { 29 | margin-left: 10px; 30 | font-weight: normal; 31 | } 32 | `, 33 | ]; 34 | 35 | render() { 36 | const totalCount = this.dbLines.length; 37 | let totalRows = 0; 38 | this.dbLines.forEach((value) => { 39 | totalRows += value.dmlRowCount.self || value.soqlRowCount.self || 0; 40 | }); 41 | 42 | return html` 43 |
44 | ${this.title} 45 | Count: ${totalCount} 46 | Rows: ${totalRows} 47 |
48 | `; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /log-viewer/modules/components/database-view/DatabaseView.scss: -------------------------------------------------------------------------------- 1 | @import '../../datagrid/style/DataGrid.scss'; 2 | 3 | .row__details-container { 4 | height: 100%; 5 | padding: 4px 4px 4px 4px; 6 | background-color: var(--vscode-editorHoverWidget-background); 7 | } 8 | .db-group-row { 9 | display: flex; 10 | min-width: 0; 11 | font-family: monospace; 12 | } 13 | .db-group-row__title { 14 | white-space: pre-wrap; 15 | overflow-wrap: anywhere; 16 | } 17 | -------------------------------------------------------------------------------- /log-viewer/modules/components/database-view/DatabaseView.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | 5 | import { LitElement, css, html } from 'lit'; 6 | import { customElement, property, state } from 'lit/decorators.js'; 7 | 8 | import { ApexLog } from '../../parsers/ApexLogParser.js'; 9 | import { globalStyles } from '../../styles/global.styles.js'; 10 | import '../CallStack.js'; 11 | import './DMLView.js'; 12 | import './DatabaseSOQLDetailPanel.js'; 13 | import './DatabaseSection.js'; 14 | import './SOQLView.js'; 15 | 16 | @customElement('database-view') 17 | export class DatabaseView extends LitElement { 18 | @property() 19 | timelineRoot: ApexLog | null = null; 20 | 21 | dmlMatches = 0; 22 | soqlMatches = 0; 23 | 24 | @state() 25 | dmlHighlightIndex = 0; 26 | @state() 27 | soqlHighlightIndex = 0; 28 | 29 | findArgs: { text: string; count: number; options: { matchCase: boolean } } = { 30 | text: '', 31 | count: 0, 32 | options: { matchCase: false }, 33 | }; 34 | findMap = {}; 35 | 36 | constructor() { 37 | super(); 38 | 39 | document.addEventListener('db-find-results', this._findResults as EventListener); 40 | document.addEventListener('lv-find-match', this._findHandler as EventListener); 41 | document.addEventListener('lv-find', this._findHandler as EventListener); 42 | } 43 | 44 | static styles = [ 45 | globalStyles, 46 | css` 47 | :host { 48 | display: flex; 49 | flex-direction: column; 50 | height: 100%; 51 | width: 100%; 52 | } 53 | `, 54 | ]; 55 | 56 | render() { 57 | return html` 58 | 62 | 66 | `; 67 | } 68 | 69 | _findHandler = ( 70 | e: CustomEvent<{ text: string; count: number; options: { matchCase: boolean } }>, 71 | ) => { 72 | this._find(e.detail); 73 | }; 74 | 75 | _find = (arg: { count: number }) => { 76 | const matchIndex = arg.count; 77 | if (matchIndex <= this.dmlMatches) { 78 | this.dmlHighlightIndex = matchIndex; 79 | this.soqlHighlightIndex = 0; 80 | } else { 81 | this.soqlHighlightIndex = matchIndex - this.dmlMatches; 82 | this.dmlHighlightIndex = 0; 83 | } 84 | }; 85 | 86 | _findResults = (e: CustomEvent<{ totalMatches: number; type: 'dml' | 'soql' }>) => { 87 | if (e.detail.type === 'dml') { 88 | this.dmlMatches = e.detail.totalMatches; 89 | } else if (e.detail.type === 'soql') { 90 | this.soqlMatches = e.detail.totalMatches; 91 | } 92 | 93 | this._find({ count: 1 }); 94 | 95 | document.dispatchEvent( 96 | new CustomEvent('lv-find-results', { 97 | detail: { totalMatches: this.dmlMatches + this.soqlMatches }, 98 | }), 99 | ); 100 | }; 101 | } 102 | -------------------------------------------------------------------------------- /log-viewer/modules/components/datagrid/datagrid-filter-bar.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2021 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html } from 'lit'; 6 | import { customElement } from 'lit/decorators.js'; 7 | 8 | import { globalStyles } from '../../styles/global.styles.js'; 9 | 10 | provideVSCodeDesignSystem().register(); 11 | 12 | @customElement('datagrid-filter-bar') 13 | export class DatagridFilterBar extends LitElement { 14 | static styles = [ 15 | globalStyles, 16 | css` 17 | :host { 18 | height: 100%; 19 | width: 100%; 20 | display: flex; 21 | flex-direction: column; 22 | flex: 1; 23 | } 24 | 25 | .filter-bar { 26 | display: flex; 27 | } 28 | 29 | .filter-bar .filter-bar__actions--right { 30 | align-items: center; 31 | display: flex; 32 | flex: 1 1 auto; 33 | justify-content: flex-end; 34 | } 35 | `, 36 | ]; 37 | 38 | render() { 39 | return html`
40 |
41 | 42 | 43 |
44 | 45 |
46 | 47 |
48 |
`; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /log-viewer/modules/components/notifications/NotificationButton.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeButton, vsCodeDivider } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html, unsafeCSS, type TemplateResult } from 'lit'; 6 | import { customElement, property, state } from 'lit/decorators.js'; 7 | 8 | import codiconStyles from '../../styles/codicon.css'; 9 | import { globalStyles } from '../../styles/global.styles.js'; 10 | import { notificationStyles } from '../../styles/notification.styles.js'; 11 | import './NotificationPanel.js'; 12 | 13 | provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeDivider()); 14 | 15 | @customElement('notification-button') 16 | export class NotificationButton extends LitElement { 17 | @state() 18 | open = false; 19 | 20 | @property() 21 | notifications: Notification[] = []; 22 | 23 | colorStyles = new Map([ 24 | ['Error', 'error'], 25 | ['Warning', 'warning'], 26 | ['Info', 'info'], 27 | ]); 28 | 29 | constructor() { 30 | super(); 31 | document.addEventListener('click', (event) => { 32 | if (!event.composedPath().includes(this)) { 33 | this.open = false; 34 | } 35 | }); 36 | } 37 | 38 | static styles = [ 39 | globalStyles, 40 | unsafeCSS(codiconStyles), 41 | css` 42 | :host { 43 | top: 32px; 44 | right: 0px; 45 | ${notificationStyles} 46 | } 47 | 48 | .icon-button { 49 | width: 32px; 50 | height: 32px; 51 | } 52 | 53 | .badge-indicator { 54 | color: rgb(255, 255, 255); 55 | background-color: rgb(0, 120, 212); 56 | position: absolute; 57 | bottom: 18px; 58 | left: 18px; 59 | font-size: 9px; 60 | font-weight: 600; 61 | min-width: 8px; 62 | height: 16px; 63 | line-height: 16px; 64 | padding: 0px 4px; 65 | border-radius: 20px; 66 | text-align: center; 67 | } 68 | 69 | .notification-panel { 70 | position: absolute; 71 | top: calc(100% + 10px); 72 | right: 0px; 73 | } 74 | 75 | .menu-container { 76 | position: relative; 77 | } 78 | 79 | .codicon.icon { 80 | font-size: 22px; 81 | width: 20px; 82 | height: 20px; 83 | } 84 | 85 | .notification { 86 | padding: 8px 16px; 87 | overflow-wrap: anywhere; 88 | text-wrap: wrap; 89 | display: flex; 90 | gap: 8px; 91 | border-radius: 4px; 92 | } 93 | 94 | .text-container { 95 | padding: 8px 0px 0px 0px; 96 | } 97 | 98 | .error { 99 | background-color: var(--notification-error-background); 100 | } 101 | 102 | .warning { 103 | background-color: var(--notification-warning-background); 104 | } 105 | 106 | .info { 107 | background-color: var(--notification-information-background); 108 | } 109 | `, 110 | ]; 111 | 112 | render() { 113 | const sortOrder = new Map([ 114 | ['Error', 0], 115 | ['Warning', 1], 116 | ['Info', 2], 117 | ]); 118 | 119 | this.notifications.sort((a, b) => { 120 | return (sortOrder.get(a.severity) || 0) - (sortOrder.get(b.severity) || 0); 121 | }); 122 | 123 | const messages: TemplateResult[] = []; 124 | 125 | const lastIndex = this.notifications.length - 1; 126 | this.notifications.forEach((item, index) => { 127 | const colorStyle = this.colorStyles.get(item.severity) || ''; 128 | 129 | const content = item.message 130 | ? html`
131 | ${item.summary} 132 |
${item.message}
133 |
` 134 | : html`
${item.summary}
`; 135 | 136 | messages.push(html`
${content}
`); 137 | if (index !== lastIndex) { 138 | messages.push(html``); 139 | } 140 | }); 141 | 142 | const indicator = 143 | this.notifications.length > 0 144 | ? html`
${this.notifications.length}
` 145 | : html``; 146 | 147 | return html``; 162 | } 163 | 164 | _toggleNotifications() { 165 | this.open = !this.open; 166 | } 167 | } 168 | 169 | export class Notification { 170 | summary = ''; 171 | message = ''; 172 | severity: 'Error' | 'Warning' | 'Info' = 'Info'; 173 | } 174 | -------------------------------------------------------------------------------- /log-viewer/modules/components/notifications/NotificationPanel.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeButton, vsCodeDivider } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html, type TemplateResult } from 'lit'; 6 | import { customElement, property } from 'lit/decorators.js'; 7 | 8 | import { globalStyles } from '../../styles/global.styles.js'; 9 | import { notificationStyles } from '../../styles/notification.styles.js'; 10 | 11 | provideVSCodeDesignSystem().register(vsCodeButton(), vsCodeDivider()); 12 | 13 | @customElement('notification-panel') 14 | export class NotificationPanel extends LitElement { 15 | @property({ type: Boolean }) 16 | open = false; 17 | 18 | static styles = [ 19 | globalStyles, 20 | css` 21 | :host { 22 | z-index: 999; 23 | ${notificationStyles} 24 | } 25 | .container { 26 | background-color: var(--vscode-editor-background); 27 | max-height: 540px; 28 | width: 320px; 29 | padding: 8px 4px 8px 4px; 30 | border: calc(var(--border-width) * 1px) solid var(--divider-background); 31 | box-shadow: rgba(0, 0, 0, 0.5) 0px 4px 20px; 32 | border-radius: 4px; 33 | overflow: scroll; 34 | } 35 | 36 | .closed { 37 | display: none; 38 | } 39 | .notification { 40 | padding: 8px 16px; 41 | overflow-wrap: anywhere; 42 | text-wrap: wrap; 43 | display: flex; 44 | gap: 8px; 45 | border-radius: 4px; 46 | } 47 | .error-list { 48 | display: flex; 49 | flex-direction: column; 50 | } 51 | 52 | .notification-icon { 53 | justify-content: center; 54 | display: flex; 55 | flex-direction: column; 56 | } 57 | 58 | .no-messages { 59 | display: flex; 60 | justify-content: center; 61 | } 62 | 63 | .error { 64 | background-color: var(--notification-error-background); 65 | } 66 | 67 | .warning { 68 | background-color: var(--notification-warning-background); 69 | } 70 | 71 | .info { 72 | background-color: var(--notification-information-background); 73 | } 74 | 75 | .text-container { 76 | padding: 8px 0px 0px 0px; 77 | } 78 | `, 79 | ]; 80 | 81 | render() { 82 | return html`
83 |
84 |

No Items!

85 |
86 |
`; 87 | } 88 | } 89 | 90 | export type NotificationSeverity = 'Error' | 'Warning' | 'Info' | 'None'; 91 | export class Notification { 92 | summary = ''; 93 | message: string | TemplateResult<1> = ''; 94 | severity: NotificationSeverity = 'None'; 95 | timestamp: number | null = null; 96 | } 97 | -------------------------------------------------------------------------------- /log-viewer/modules/components/notifications/NotificationTag.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit'; 5 | import { LitElement, css, html, unsafeCSS, type TemplateResult } from 'lit'; 6 | import { customElement, property, state } from 'lit/decorators.js'; 7 | 8 | import codiconStyles from '../../styles/codicon.css'; 9 | import { globalStyles } from '../../styles/global.styles.js'; 10 | import { notificationStyles } from '../../styles/notification.styles.js'; 11 | import '../BadgeBase.js'; 12 | import { goToRow } from '../calltree-view/CalltreeView.js'; 13 | import './NotificationPanel.js'; 14 | 15 | provideVSCodeDesignSystem().register(vsCodeButton()); 16 | 17 | @customElement('notification-tag') 18 | export class NotificationTag extends LitElement { 19 | @state() 20 | open = false; 21 | 22 | @property() 23 | notifications: Notification[] = []; 24 | 25 | colorStyles = new Map([ 26 | ['Error', 'error'], 27 | ['Warning', 'warning'], 28 | ['Info', 'info'], 29 | ]); 30 | 31 | constructor() { 32 | super(); 33 | document.addEventListener('click', (event) => { 34 | if (!event.composedPath().includes(this)) { 35 | this.open = false; 36 | } 37 | }); 38 | } 39 | 40 | static styles = [ 41 | globalStyles, 42 | unsafeCSS(codiconStyles), 43 | css` 44 | :host { 45 | ${notificationStyles} 46 | } 47 | 48 | .icon { 49 | position: relative; 50 | width: 32px; 51 | height: 32px; 52 | } 53 | .icon-svg { 54 | width: 20px; 55 | height: 20px; 56 | } 57 | 58 | .icon-button { 59 | width: 32px; 60 | height: 32px; 61 | } 62 | 63 | .codicon.icon { 64 | font-size: 22px; 65 | width: 20px; 66 | height: 20px; 67 | } 68 | 69 | .badge-indicator { 70 | color: rgb(255, 255, 255); 71 | background-color: rgb(0, 120, 212); 72 | position: absolute; 73 | bottom: 18px; 74 | left: 18px; 75 | font-size: 9px; 76 | font-weight: 600; 77 | min-width: 8px; 78 | height: 16px; 79 | line-height: 16px; 80 | padding: 0px 4px; 81 | border-radius: 20px; 82 | text-align: center; 83 | } 84 | 85 | .tag-panel { 86 | position: absolute; 87 | top: calc(100% + 10px); 88 | left: 50%; 89 | transform: translateX(-50%); 90 | } 91 | 92 | .menu-container { 93 | position: relative; 94 | } 95 | 96 | .notification { 97 | padding: 8px 16px; 98 | overflow-wrap: anywhere; 99 | text-wrap: wrap; 100 | display: flex; 101 | gap: 8px; 102 | border-radius: 4px; 103 | } 104 | 105 | .text-container { 106 | padding: 8px 0px 0px 0px; 107 | } 108 | 109 | .error { 110 | background-color: var(--notification-error-background); 111 | } 112 | 113 | .warning { 114 | background-color: var(--notification-warning-background); 115 | } 116 | 117 | .info { 118 | background-color: var(--notification-information-background); 119 | } 120 | 121 | .button-bar { 122 | display: flex; 123 | align-items: center; 124 | height: 35px; 125 | } 126 | `, 127 | ]; 128 | 129 | render() { 130 | const status = this.notifications.length > 0 ? 'failure' : 'success'; 131 | 132 | const sortOrder = new Map([ 133 | ['Error', 0], 134 | ['Warning', 1], 135 | ['Info', 2], 136 | ]); 137 | 138 | this.notifications.sort((a, b) => { 139 | return (sortOrder.get(a.severity) || 0) - (sortOrder.get(b.severity) || 0); 140 | }); 141 | 142 | const messages: TemplateResult[] = []; 143 | 144 | const lastIndex = this.notifications.length - 1; 145 | this.notifications.forEach((item, index) => { 146 | const colorStyle = this.colorStyles.get(item.severity) || ''; 147 | 148 | const buttonBar = item.timestamp 149 | ? html`
150 | { 154 | goToRow(item.timestamp || 0); 155 | }} 156 | >Go To Call Tree 158 |
` 159 | : ''; 160 | 161 | const content = html`
162 | ${item.message 163 | ? html`
164 | ${item.summary} 165 |
${item.message}
166 |
` 167 | : item.summary} 168 | ${buttonBar} 169 |
`; 170 | 171 | messages.push(html`
${content}
`); 172 | if (index !== lastIndex) { 173 | messages.push(html``); 174 | } 175 | }); 176 | 177 | return html``; 187 | } 188 | 189 | _toggleNotifications() { 190 | this.open = !this.open; 191 | } 192 | } 193 | 194 | export class Notification { 195 | summary = ''; 196 | message = ''; 197 | severity: 'Error' | 'Warning' | 'Info' | 'none' = 'none'; 198 | timestamp: number | null = null; 199 | } 200 | -------------------------------------------------------------------------------- /log-viewer/modules/components/skeleton/GridSkeleton.ts: -------------------------------------------------------------------------------- 1 | import { LitElement, css, html } from 'lit'; 2 | import { customElement } from 'lit/decorators.js'; 3 | 4 | import { globalStyles } from '../../styles/global.styles.js'; 5 | import { skeletonStyles } from './skeleton.styles.js'; 6 | 7 | @customElement('grid-skeleton') 8 | export class GridSkeleton extends LitElement { 9 | static styles = [ 10 | globalStyles, 11 | skeletonStyles, 12 | css` 13 | :host { 14 | } 15 | 16 | .skeleton-text { 17 | width: 100%; 18 | height: 1rem; 19 | margin-bottom: 0.5rem; 20 | border-radius: 0.25rem; 21 | } 22 | 23 | .skeleton-wrapper { 24 | display: flex; 25 | position: relative; 26 | width: 100%; 27 | flex-direction: column; 28 | justify-content: center; 29 | } 30 | 31 | .skeleton-inline { 32 | display: flex; 33 | gap: 10px; 34 | } 35 | `, 36 | ]; 37 | 38 | render() { 39 | return html`
40 |
41 |
42 |
43 |
44 |
45 |
46 | 47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 |
57 |
58 |
`; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /log-viewer/modules/components/skeleton/skeleton.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const skeletonStyles = css` 4 | .skeleton { 5 | animation: pulse 2s cubic-bezier(0.4, 0, 0.6, 1) infinite; 6 | background-color: rgb(229 231 235); 7 | border-radius: 0.25rem; 8 | min-width: 5ch; 9 | width: 100%; 10 | } 11 | 12 | @keyframes pulse { 13 | 0%, 14 | 100% { 15 | opacity: 1; 16 | } 17 | 50% { 18 | opacity: 0.5; 19 | } 20 | } 21 | `; 22 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/dataaccessor/Number.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | import { type ColumnComponent, type RowComponent } from 'tabulator-tables'; 5 | 6 | export default function ( 7 | value: number | null, 8 | _data: unknown, 9 | _type: 'data' | 'download' | 'clipboard', 10 | accessorParams: { precision: number }, 11 | _column?: ColumnComponent, 12 | _row?: RowComponent, 13 | ): string { 14 | const returnValue = (value || 0) / 1000000; 15 | return returnValue.toFixed(accessorParams.precision || 3); 16 | } 17 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/editors/MinMax.css: -------------------------------------------------------------------------------- 1 | .min-max__input { 2 | background-color: var(--vscode-settings-numberInputBackground); 3 | color: var(--vscode-settings-numberInputForeground); 4 | border: 1px solid var(--vscode-settings-numberInputBorder, transparent); 5 | width: 50%; 6 | box-sizing: border-box; 7 | position: relative; 8 | padding: 2px 4px; 9 | box-sizing: border-box; 10 | border-radius: 2px; 11 | font-size: inherit; 12 | appearance: textfield !important; 13 | } 14 | 15 | .min-max__input::-webkit-outer-spin-button, 16 | .min-max__input::-webkit-inner-spin-button { 17 | -webkit-appearance: none; 18 | margin: 0; 19 | } 20 | 21 | .min-max__input:focus { 22 | outline-width: 1px; 23 | outline-style: solid; 24 | outline-offset: -1px; 25 | outline-color: var(--vscode-focusBorder); 26 | opacity: 1; 27 | } 28 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/editors/MinMax.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | import { 5 | type CellComponent, 6 | type EmptyCallback, 7 | type ValueBooleanCallback, 8 | type ValueVoidCallback, 9 | } from 'tabulator-tables'; 10 | 11 | import './MinMax.css'; 12 | 13 | export default function ( 14 | cell: CellComponent, 15 | onRendered: EmptyCallback, 16 | success: ValueBooleanCallback, 17 | cancel: ValueVoidCallback, 18 | _editorParams: object, 19 | ): HTMLElement | false { 20 | const container = document.createElement('span'); 21 | 22 | //create and style inputs 23 | const start = document.createElement('input'); 24 | start.min = '0'; 25 | start.type = 'number'; 26 | start.className = 'min-max__input input'; 27 | start.placeholder = 'Min'; 28 | 29 | const end = start.cloneNode() as HTMLInputElement; 30 | end.setAttribute('placeholder', 'Max'); 31 | 32 | function buildValues() { 33 | start.step = getStep(start.value); 34 | end.step = getStep(end.value); 35 | success({ 36 | start: start.value !== '' ? +start.value : null, 37 | end: end.value !== '' ? +end.value : null, 38 | }); 39 | } 40 | 41 | function getStep(numValue: string) { 42 | let step = numValue.split('.')[1]; 43 | if (step) { 44 | step = `0.${'0'.repeat(step.length - 1)}1`; 45 | } else { 46 | step = '1'; 47 | } 48 | return step; 49 | } 50 | 51 | function keypress(e: KeyboardEvent) { 52 | if (e.key === 'Enter') { 53 | buildValues(); 54 | } else if (e.key === 'Escape') { 55 | cancel(true); 56 | } 57 | } 58 | 59 | start.addEventListener('change', buildValues); 60 | start.addEventListener('blur', buildValues); 61 | start.addEventListener('keydown', keypress); 62 | 63 | end.addEventListener('change', buildValues); 64 | end.addEventListener('blur', buildValues); 65 | end.addEventListener('keydown', keypress); 66 | 67 | container.appendChild(start); 68 | container.appendChild(end); 69 | 70 | return container; 71 | } 72 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/filters/MinMax.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | 5 | export default function ( 6 | filterVal: { start: number | null; end: number | null }, 7 | rowVal: number, 8 | rowData: { _children: []; id: number }, 9 | filterParams: { columnName: string; filterCache: Map }, 10 | ): boolean { 11 | if (!('start' in filterVal) || !('end' in filterVal)) { 12 | return false; 13 | } 14 | 15 | return deepFilter(filterVal, rowVal, rowData, filterParams); 16 | } 17 | 18 | function deepFilter( 19 | headerValue: { start: number | null; end: number | null }, 20 | rowValue: number, 21 | rowData: { _children: []; id: number }, 22 | filterParams: { columnName: string; filterCache: Map }, 23 | ): boolean { 24 | const cachedMatch = filterParams.filterCache.get(rowData.id); 25 | if (cachedMatch !== null && cachedMatch !== undefined) { 26 | return cachedMatch; 27 | } 28 | 29 | const columnName = filterParams.columnName; 30 | let childMatch = false; 31 | for (const childRow of rowData._children || []) { 32 | const match = deepFilter(headerValue, childRow[columnName], childRow, filterParams); 33 | 34 | if (match) { 35 | childMatch = true; 36 | break; 37 | } 38 | } 39 | 40 | filterParams.filterCache.set(rowData.id, childMatch); 41 | if (childMatch) { 42 | return true; 43 | } 44 | 45 | const rowVal = +(rowValue / 1000000).toFixed(3); 46 | const min = headerValue.start; 47 | const max = headerValue.end; 48 | if (min && max) { 49 | return rowVal >= min && rowVal <= max; 50 | } else if (min) { 51 | return rowVal >= min; 52 | } else if (max) { 53 | return rowVal <= max; 54 | } 55 | 56 | return true; 57 | } 58 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/format/Number.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | import { type CellComponent, type EmptyCallback } from 'tabulator-tables'; 5 | 6 | export default function ( 7 | cell: CellComponent, 8 | formatterParams: NumberParams, 9 | _onRendered: EmptyCallback, 10 | ) { 11 | const value = (cell.getValue() || 0) / 1000000; 12 | return value.toFixed(formatterParams.precision || 3); 13 | } 14 | 15 | export interface NumberParams { 16 | precision?: number; 17 | } 18 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/format/Progress.css: -------------------------------------------------------------------------------- 1 | .progress-wrapper { 2 | position: relative; 3 | font-variant-numeric: tabular-nums; 4 | } 5 | .progress-bar { 6 | position: absolute; 7 | background: #a97d0f; 8 | opacity: 0.3; 9 | height: 100%; 10 | right: 0; 11 | } 12 | .progress-bar__text { 13 | position: relative; 14 | white-space: pre; 15 | display: inline-flex; 16 | } 17 | .progress-bar__text__percent { 18 | width: 9ch; 19 | } 20 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/format/Progress.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { type CellComponent, type EmptyCallback } from 'tabulator-tables'; 5 | import './Progress.css'; 6 | import { progressComponent } from './ProgressComponent.js'; 7 | 8 | export function progressFormatter( 9 | cell: CellComponent, 10 | formatterParams: ProgressParams, 11 | _onRendered: EmptyCallback, 12 | ) { 13 | const value = cell.getValue() ?? 0; 14 | const totalVal = formatterParams.totalValue ?? 0; 15 | 16 | return progressComponent(value, totalVal, { 17 | showPercentageText: formatterParams.showPercentageText, 18 | precision: formatterParams.precision, 19 | }); 20 | } 21 | 22 | export interface ProgressParams { 23 | precision?: number; 24 | totalValue?: number; 25 | showPercentageText?: boolean; 26 | } 27 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/format/ProgressComponent.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2025 Certinia Inc. All rights reserved. 3 | */ 4 | 5 | export function progressComponent( 6 | value: number, 7 | totalValue: number, 8 | options: { showPercentageText?: boolean; precision?: number } = { 9 | showPercentageText: true, 10 | precision: 3, 11 | }, 12 | ) { 13 | const roundedValue = `${(value || 0).toFixed(options.precision ?? 3)}`; 14 | 15 | if (totalValue !== undefined && totalValue !== null) { 16 | const showPercent = options.showPercentageText ?? true; 17 | const percentComplete = 18 | totalValue !== 0 ? (Math.round((value / totalValue) * 100) / 100) * 100 : 0; 19 | 20 | const percentageText = showPercent ? `(${percentComplete.toFixed(2)}%)` : ''; 21 | 22 | const progressBarElem = `${percentComplete ? `
` : ''}`; 23 | const progressBarTextElem = `
24 | ${roundedValue} 25 | ${showPercent ? `${percentageText}` : ''} 26 |
`; 27 | 28 | return `
29 | ${progressBarElem} 30 | ${progressBarTextElem} 31 |
`; 32 | } 33 | 34 | return roundedValue; 35 | } 36 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/format/ProgressMS.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { type CellComponent, type EmptyCallback } from 'tabulator-tables'; 5 | import './Progress.css'; 6 | import { progressComponent } from './ProgressComponent.js'; 7 | 8 | export function progressFormatterMS( 9 | cell: CellComponent, 10 | formatterParams: ProgressParams, 11 | _onRendered: EmptyCallback, 12 | ) { 13 | const value = (cell.getValue() || 0) / 1000000; 14 | const totalVal = formatterParams.totalValue ?? 0; 15 | const totalValAsMs = totalVal > 0 ? totalVal / 1000000 : 0; 16 | 17 | return progressComponent(value, totalValAsMs, { 18 | showPercentageText: formatterParams.showPercentageText, 19 | precision: formatterParams.precision, 20 | }); 21 | } 22 | 23 | export interface ProgressParams { 24 | precision?: number; 25 | totalValue?: number; 26 | showPercentageText?: boolean; 27 | } 28 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/groups/GroupCalcs.ts: -------------------------------------------------------------------------------- 1 | import { Module, type GroupComponent, type Tabulator } from 'tabulator-tables'; 2 | 3 | export class GroupCalcs extends Module { 4 | static moduleName = 'groupCalcs'; 5 | 6 | constructor(table: Tabulator) { 7 | super(table); 8 | this.registerTableOption('groupCalcs', false); 9 | } 10 | 11 | initialize() { 12 | // @ts-expect-error groupCalcs 13 | if (this.table.options.groupCalcs && !this.table.options.groupHeader) { 14 | this.table.options.groupHeader = this.groupHeader; 15 | } 16 | } 17 | 18 | groupHeader(value: unknown, count: number, data: unknown, group: GroupComponent) { 19 | // @ts-expect-error private function to the raw group instead of wrapper component 20 | const rawGroup = group._getSelf(); 21 | const columnCalcs = group.getTable().modules.columnCalcs; 22 | 23 | const row = columnCalcs.generateBottomRow(rawGroup.rows); 24 | row.data[columnCalcs.botCalcs[0].field] = group.getKey() + ` (${count})`; 25 | row.generateCells(); 26 | 27 | const arrowClone = rawGroup.arrowElement.cloneNode(true); 28 | rawGroup.arrowElement = document.createElement('span'); 29 | 30 | const firstCell = row.cells[0].getElement(); 31 | firstCell.insertBefore(arrowClone, firstCell.firstChild); 32 | 33 | const rowFrag = document.createDocumentFragment(); 34 | row.cells.forEach((cell: { getElement(): HTMLElement }) => { 35 | rowFrag.appendChild(cell.getElement()); 36 | }); 37 | row.element.appendChild(rowFrag); 38 | 39 | // row.cells.forEach((cell) => { 40 | // cell.cellRendered(); 41 | // }); 42 | return row.element; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/groups/GroupSort.ts: -------------------------------------------------------------------------------- 1 | import { Module, type ColumnComponent, type GroupArg, type Tabulator } from 'tabulator-tables'; 2 | 3 | export class GroupSort extends Module { 4 | static moduleName = 'groupSort'; 5 | 6 | constructor(table: Tabulator) { 7 | super(table); 8 | this.registerTableOption('groupSort', false); 9 | this.registerTableFunction('setSortedGroupBy', this._setSortedGroupBy.bind(this)); 10 | } 11 | 12 | initialize() { 13 | // @ts-expect-error groupSort is a custom propoerty see registerTableOption above 14 | if (this.table.options.groupSort) { 15 | this.subscribe('sort-changed', this._sortGroups.bind(this)); 16 | } 17 | } 18 | 19 | _setSortedGroupBy(...args: unknown[]) { 20 | const grpArg = args[0] as GroupArg; 21 | const grpArray = Array.isArray(grpArg) ? grpArg : [grpArg]; 22 | const oldGrpArg = this.table.options.groupBy as GroupArg; 23 | const oldGrpArray = Array.isArray(oldGrpArg) ? oldGrpArg : [oldGrpArg]; 24 | if (!this._areGroupsEqual(oldGrpArray, grpArray)) { 25 | this.table.options.groupBy = grpArg; 26 | this.table.blockRedraw(); 27 | this._sortGroups(); 28 | this.table.setGroupBy(grpArg); 29 | this.table.restoreRedraw(); 30 | } 31 | } 32 | 33 | _sortGroups() { 34 | const grpArray = Array.isArray(this.table.options.groupBy) 35 | ? this.table.options.groupBy 36 | : [this.table.options.groupBy]; 37 | const { options } = this.table; 38 | 39 | const validGrps = grpArray.filter(Boolean).length > 0; 40 | if (this.table && options.sortMode !== 'remote' && validGrps) { 41 | let groupFunc = grpArray[0]; 42 | const grpField = groupFunc as string; 43 | if (typeof groupFunc === 'string') { 44 | groupFunc = function (data) { 45 | return data[grpField]; 46 | }; 47 | } 48 | 49 | const groupsByKey: { [key: string]: unknown[] } = {}; 50 | if (groupFunc) { 51 | const rows = this.table.rowManager.rows; 52 | rows.forEach((row: InternalColumnTotal) => { 53 | const grpVal = groupFunc(row.data); 54 | let groupRows = groupsByKey[grpVal]; 55 | if (!groupRows) { 56 | groupRows = []; 57 | groupsByKey[grpVal] = groupRows; 58 | } 59 | groupRows.push(row); 60 | }); 61 | } 62 | 63 | let groupTotalsRows: InternalColumnTotal[] = []; 64 | const columnCalcs = this.table.modules.columnCalcs; 65 | const field = columnCalcs.botCalcs[0].field; 66 | for (const [key, rows] of Object.entries(groupsByKey)) { 67 | const row = columnCalcs.generateBottomRow(rows); 68 | row.data[field] = key; 69 | row.key = key; 70 | row.rows = rows; 71 | row.generateCells(); 72 | groupTotalsRows.push(row); 73 | } 74 | 75 | groupTotalsRows = this._sortGroupTotals(groupTotalsRows); 76 | const groupValues: string[] = []; 77 | groupTotalsRows.forEach((colTotals) => { 78 | groupValues.push(colTotals.data[field] as string); 79 | }); 80 | 81 | const originalGroupVals = (options.groupValues ?? [[]])[0] ?? []; 82 | if (!this._areGroupsEqual(groupValues, originalGroupVals)) { 83 | this.table?.setGroupValues([groupValues]); 84 | } 85 | } else { 86 | this.table?.setGroupValues([]); 87 | } 88 | } 89 | 90 | _areGroupsEqual(oldGroups: unknown[], newGroups: unknown[]) { 91 | return ( 92 | oldGroups && 93 | newGroups.length === oldGroups.length && 94 | newGroups.every((value, index) => value === oldGroups[index]) 95 | ); 96 | } 97 | 98 | _sortGroupTotals(groupTotalsRows: InternalColumnTotal[]) { 99 | const sortListActual: unknown[] = []; 100 | const { modules, options } = this.table; 101 | 102 | const sorter = modules.sort; 103 | const sortList: InternalSortItem[] = options.sortOrderReverse 104 | ? sorter.sortList.slice().reverse() 105 | : sorter.sortList; 106 | sortList.forEach((item) => { 107 | if (item.column) { 108 | const sortObj = item.column.modules.sort; 109 | if (sortObj) { 110 | //if no sorter has been defined, take a guess 111 | if (!sortObj.sorter) { 112 | sortObj.sorter = sorter.findSorter(item.column); 113 | } 114 | 115 | item.params = 116 | typeof sortObj.params === 'function' 117 | ? sortObj.params(item.column.getComponent(), item.dir) 118 | : sortObj.params; 119 | 120 | sortListActual.push(item); 121 | } 122 | } 123 | }); 124 | 125 | //sort data 126 | if (sortListActual.length) { 127 | sorter._sortItems(groupTotalsRows, sortListActual); 128 | } else { 129 | groupTotalsRows.sort((a, b) => { 130 | const index = b.rows.length - a.rows.length; 131 | if (index === 0) { 132 | return a.key.localeCompare(b.key); 133 | } 134 | return index; 135 | }); 136 | } 137 | return groupTotalsRows; 138 | } 139 | } 140 | 141 | // Representations of the internal Tabulator structures, that are entirely private to Tabulator. Subject to change and likely to b a bit flaky. May not cover all cases yet. 142 | type InternalColumnTotal = { 143 | data: { [key: string]: unknown }; 144 | key: string; 145 | rows: { [key: string]: unknown }[]; 146 | }; 147 | 148 | type InternalColumn = { 149 | getField(): string; 150 | getComponent(): ColumnComponent; 151 | modules: { 152 | sort: { 153 | params: (column: ColumnComponent, dir: string) => object; 154 | sorter: (...args: unknown[]) => number | boolean; 155 | }; 156 | }; 157 | }; 158 | 159 | type InternalSortItem = { 160 | column: InternalColumn; 161 | dir: string; 162 | params: object; 163 | }; 164 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/module/CommonModules.ts: -------------------------------------------------------------------------------- 1 | export { 2 | AccessorModule, 3 | ClipboardModule, 4 | ColumnCalcsModule, 5 | DataTreeModule, 6 | DownloadModule, 7 | EditModule, 8 | ExportModule, 9 | FilterModule, 10 | FormatModule, 11 | GroupRowsModule, 12 | InteractionModule, 13 | KeybindingsModule, 14 | MenuModule, 15 | ResizeColumnsModule, 16 | ResizeRowsModule, 17 | ResizeTableModule, 18 | ResponsiveLayoutModule, 19 | SelectRowModule, 20 | SortModule, 21 | TooltipModule, 22 | } from 'tabulator-tables'; 23 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/module/RowNavigation.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | import { Module, Tabulator, type RowComponent } from 'tabulator-tables'; 5 | type GoToRowOptions = { scrollIfVisible: boolean; focusRow: boolean }; 6 | export class RowNavigation extends Module { 7 | static moduleName = 'rowNavigation'; 8 | tableHolder: HTMLElement | null = null; 9 | 10 | constructor(table: Tabulator) { 11 | super(table); 12 | // @ts-expect-error registerTableFunction() needs adding to tabulator types 13 | this.registerTableFunction('goToRow', this.goToRow.bind(this)); 14 | } 15 | 16 | goToRow(row: RowComponent, opts: GoToRowOptions = { scrollIfVisible: true, focusRow: true }) { 17 | if (row) { 18 | const { focusRow } = opts; 19 | 20 | const table = this.table; 21 | this.tableHolder ??= table.element.querySelector('.tabulator-tableholder') as HTMLElement; 22 | 23 | table.blockRedraw(); 24 | 25 | const grp = row.getGroup(); 26 | if (grp && !grp.isVisible()) { 27 | grp.show(); 28 | } 29 | 30 | const rowsToExpand = []; 31 | //@ts-expect-error This is private to tabulator, but we have no other choice atm. 32 | let parent = row._getSelf().modules.dataTree ? row.getTreeParent() : false; 33 | while (parent) { 34 | if (!parent.isTreeExpanded()) { 35 | rowsToExpand.push(parent); 36 | } 37 | parent = parent.getTreeParent(); 38 | } 39 | 40 | rowsToExpand.forEach((row) => { 41 | row.treeExpand(); 42 | }); 43 | 44 | table.getSelectedRows().forEach((rowToDeselect) => { 45 | rowToDeselect.deselect(); 46 | }); 47 | row.select(); 48 | table.restoreRedraw(); 49 | 50 | if (focusRow) { 51 | this.tableHolder.focus(); 52 | } 53 | if (row) { 54 | setTimeout(() => this._scrollToRow(row, opts)); 55 | } 56 | } 57 | } 58 | 59 | _scrollToRow(row: RowComponent, opts: GoToRowOptions) { 60 | const { scrollIfVisible, focusRow } = opts; 61 | 62 | this.table.scrollToRow(row, 'center', scrollIfVisible).then(() => { 63 | setTimeout(() => { 64 | const elem = row.getElement(); 65 | 66 | if (scrollIfVisible || !this._isVisible(elem)) { 67 | // NOTE: work around because this.table.scrollToRow does not work correctly when the row is near the very bottom of the grid. 68 | elem.scrollIntoView({ behavior: 'auto', block: 'center', inline: 'start' }); 69 | } 70 | 71 | if (focusRow) { 72 | elem.focus(); 73 | } 74 | }); 75 | }); 76 | } 77 | 78 | _isVisible(el: Element) { 79 | const rect = el.getBoundingClientRect(); 80 | return rect.top >= 0 && rect.bottom <= window.innerHeight; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /log-viewer/modules/datagrid/style/DataGrid.scss: -------------------------------------------------------------------------------- 1 | // I prefer the table with no borders but if we want to add one I think this works best 2 | // var(--vscode-sideBar-border) 3 | 4 | //Main Theme Variables 5 | $backgroundColor: var(--vscode-editor-background); //background color of tabulator 6 | $borderColor: transparent; //border to tabulator 7 | $textSize: var(--vscode-editor-font-size); //table text size 8 | 9 | //header theming 10 | $headerBackgroundColor: transparent; //border to tabulator 11 | $headerTextColor: var(--vscode-editor-foreground); //header text color 12 | $headerBorderColor: transparent; //header border color 13 | $headerSeparatorColor: transparent; //header bottom separator color 14 | $headerMargin: 4px !default; //padding round header 15 | 16 | //column header arrows 17 | $sortArrowHover: #555 !default; 18 | $sortArrowActive: #666 !default; 19 | $sortArrowInactive: #bbb !default; 20 | 21 | //row theming 22 | $rowBackgroundColor: $backgroundColor; //table row background color 23 | $rowAltBackgroundColor: transparent; //table row background color 24 | $rowBorderColor: transparent; //table border color 25 | $rowTextColor: var(--vscode-editor-foreground); //table text color 26 | $rowHoverBackground: var(--vscode-list-hoverBackground); //row background color on hover 27 | //row background color when selected 28 | $rowSelectedBackground: var(--vscode-list-activeSelectionBackground); 29 | //row background color when selected and hovered 30 | $rowSelectedBackgroundHover: var(--vscode-list-activeSelectionBackground); 31 | 32 | $editBoxColor: var(--vscode-focusBorder, #1d68cd); //border color for edit boxes 33 | $errorColor: #dd0000 !default; //error indication 34 | 35 | //footer theming 36 | $footerBackgroundColor: transparent; //border to tabulator 37 | $footerTextColor: var(--vscode-editor-foreground); //footer text color 38 | $footerBorderColor: transparent; //footer border color 39 | $footerSeparatorColor: transparent; //footer bottom separator color 40 | $footerActiveColor: #d00 !default; //footer bottom active text color 41 | @import '~tabulator-tables/src/scss/tabulator.scss'; 42 | @import '../editors/MinMax'; 43 | @import '../format/Progress'; 44 | 45 | .tabulator { 46 | .tabulator-tableholder { 47 | overflow-x: hidden; 48 | .tabulator-table { 49 | display: block; 50 | background-color: default; 51 | } 52 | } 53 | 54 | .tabulator-header-filter { 55 | input[type='search'] { 56 | color: var(--vscode-editor-foreground); 57 | background-color: var(--vscode-dropdown-background, default); 58 | border: 1px solid var(--vscode-dropdown-border, transparent); 59 | width: 50%; 60 | box-sizing: border-box; 61 | position: relative; 62 | padding: 2px 4px; 63 | box-sizing: border-box; 64 | border-radius: 2px; 65 | appearance: textfield !important; 66 | } 67 | input[type='search']:focus { 68 | outline: var(--vscode-focusBorder, default) solid 1px; 69 | } 70 | } 71 | 72 | .tabulator-row { 73 | background-color: default; 74 | .tabulator-row-even { 75 | background-color: default; 76 | } 77 | 78 | &.tabulator-group { 79 | font-family: monospace; 80 | background: unset; 81 | border-bottom: unset; 82 | border-right: unset; 83 | border-top: unset; 84 | max-width: 100%; 85 | overflow: hidden; 86 | text-overflow: ellipsis; 87 | padding: unset; 88 | &:hover { 89 | background-color: $rowHoverBackground; 90 | } 91 | 92 | span { 93 | margin-left: unset; 94 | color: unset; 95 | } 96 | } 97 | } 98 | 99 | .datagrid-code-text { 100 | font-family: monospace; 101 | font-weight: var(--vscode-font-weight, normal); 102 | font-size: var(--vscode-editor-font-size, 0.9em); 103 | } 104 | 105 | .tabulator-tooltip { 106 | background: var(--vscode-editor-background); 107 | overflow-wrap: anywhere; 108 | } 109 | .tabulator-row.tabulator-selected { 110 | color: var(--vscode-list-activeSelectionForeground); 111 | } 112 | 113 | .tabulator-cell.datagrid-textarea { 114 | white-space: pre-wrap; 115 | overflow-wrap: break-word; 116 | min-height: 0; 117 | height: 100%; 118 | } 119 | 120 | input[type='checkbox'] { 121 | vertical-align: middle; 122 | } 123 | 124 | .sort-by { 125 | display: flex; 126 | flex-direction: column; 127 | gap: 2px; 128 | } 129 | 130 | .sort-by--bottom { 131 | color: rgb(102, 102, 102); 132 | border-bottom: none; 133 | border-top: 6px solid rgb(102, 102, 102); 134 | border-left: 6px solid transparent; 135 | border-right: 6px solid transparent; 136 | } 137 | .sort-by--top { 138 | color: rgb(102, 102, 102); 139 | border-bottom: 6px solid rgb(102, 102, 102); 140 | border-top: none; 141 | border-left: 6px solid transparent; 142 | border-right: 6px solid transparent; 143 | } 144 | 145 | .number-cell { 146 | font-variant-numeric: tabular-nums; 147 | } 148 | } 149 | 150 | .tabulator-edit-list { 151 | border-color: var(--vscode-focusBorder, default); 152 | 153 | .tabulator-edit-list-item { 154 | color: var(--vscode-editor-foreground, 'white'); 155 | &.active { 156 | background-color: var(--vscode-list-activeSelectionBackground); 157 | color: var(--vscode-editor-foreground, 'white'); 158 | } 159 | 160 | &:hover { 161 | background-color: var(--vscode-list-activeSelectionBackground); 162 | color: var(--vscode-editor-foreground, 'white'); 163 | } 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /log-viewer/modules/services/VSCodeExtensionMessenger.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2024 Certinia Inc. All rights reserved. 3 | */ 4 | class VSCodeExtensionMessenger { 5 | private static vscode: VSCodeAPI; 6 | private static instance: VSCodeExtensionMessenger; 7 | private static listeners = new Map(); 8 | 9 | private constructor() { 10 | VSCodeExtensionMessenger.listen((message: MessageEvent>) => { 11 | const { requestId, payload, error } = message.data; 12 | 13 | if (requestId && VSCodeExtensionMessenger.listeners.has(requestId)) { 14 | VSCodeExtensionMessenger.listeners.get(requestId)?.(payload, error); 15 | } 16 | }); 17 | } 18 | 19 | public static getInstance() { 20 | if (!VSCodeExtensionMessenger.instance) { 21 | VSCodeExtensionMessenger.instance = new VSCodeExtensionMessenger(); 22 | } 23 | 24 | return VSCodeExtensionMessenger.instance; 25 | } 26 | 27 | public getVsCodeAPI(): VSCodeAPI | null { 28 | if (!VSCodeExtensionMessenger.vscode) { 29 | VSCodeExtensionMessenger.vscode = acquireVsCodeApi(); 30 | } 31 | return VSCodeExtensionMessenger.vscode; 32 | } 33 | 34 | public send(message: string, payload?: T): void { 35 | const vscode = this.getVsCodeAPI(); 36 | if (!vscode) { 37 | return; 38 | } 39 | 40 | if (payload) { 41 | vscode.postMessage({ cmd: message, payload }); 42 | } else { 43 | vscode.postMessage({ cmd: message }); 44 | } 45 | } 46 | 47 | public request(message: string, payload?: T): Promise { 48 | const reqId = crypto.randomUUID(); 49 | return new Promise((resolve, reject) => { 50 | const listener = (incomingPayload: any, error: unknown) => { 51 | if (error) { 52 | reject(error); 53 | } else { 54 | resolve(incomingPayload); 55 | } 56 | VSCodeExtensionMessenger.listeners.delete(reqId); 57 | }; 58 | 59 | VSCodeExtensionMessenger.listeners.set(reqId, listener); 60 | 61 | const vscode = this.getVsCodeAPI(); 62 | if (!vscode) { 63 | return; 64 | } 65 | 66 | if (payload) { 67 | vscode.postMessage({ cmd: message, requestId: reqId, payload }); 68 | } else { 69 | vscode.postMessage({ cmd: message, requestId: reqId }); 70 | } 71 | }); 72 | } 73 | 74 | private static listen(callback: (event: MessageEvent>) => void): void { 75 | window.addEventListener('message', callback); 76 | } 77 | } 78 | 79 | declare function acquireVsCodeApi(): VSCodeAPI; 80 | 81 | interface VSCodeAPI { 82 | postMessage: (msg: T) => void; 83 | } 84 | 85 | interface VSCodeMessage extends MessageEvent { 86 | cmd: string; 87 | payload: T; 88 | requestId?: string; 89 | error?: unknown; 90 | } 91 | 92 | type ListenerType = (payload: T, error: K) => void; 93 | 94 | export const vscodeMessenger = VSCodeExtensionMessenger.getInstance(); 95 | -------------------------------------------------------------------------------- /log-viewer/modules/soql/SOQLParser.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2022 Certinia Inc. All rights reserved. 3 | */ 4 | import { type QueryContext } from '@apexdevtools/apex-parser'; 5 | import { 6 | type ANTLRErrorListener, 7 | type RecognitionException, 8 | type Recognizer, 9 | type Token, 10 | } from 'antlr4ts'; 11 | 12 | // To understand the parser AST see https://github.com/nawforce/apex-parser/blob/master/antlr/ApexParser.g4 13 | // Start with the 'query' rule at ~532 14 | // Salesforce SOQL Reference: https://developer.salesforce.com/docs/atlas.en-us.soql_sosl.meta/soql_sosl/sforce_api_calls_soql.htm 15 | 16 | export class SOQLTree { 17 | _queryContext: QueryContext; 18 | 19 | constructor(queryContext: QueryContext) { 20 | this._queryContext = queryContext; 21 | } 22 | 23 | /* Return true if SELECT list only contains field names, no functions, sub-queries or typeof */ 24 | isSimpleSelect(): boolean { 25 | const selectList = this._queryContext.selectList(); 26 | const selectEntries = selectList.selectEntry(); 27 | return selectEntries.every((selectEntry) => selectEntry.fieldName() !== undefined); 28 | } 29 | 30 | /* Return true for queries only containing WHERE, ORDER BY & LIMIT clauses */ 31 | isTrivialQuery(): boolean { 32 | return ( 33 | this._queryContext.usingScope() === undefined && 34 | this._queryContext.withClause() === undefined && 35 | this._queryContext.groupByClause() === undefined && 36 | this._queryContext.offsetClause() === undefined && 37 | this._queryContext.allRowsClause() === undefined && 38 | this._queryContext.forClauses().childCount === 0 && 39 | this._queryContext.updateList() === undefined 40 | ); 41 | } 42 | 43 | /* Return true if query has ORDER BY */ 44 | isOrdered(): boolean { 45 | return this._queryContext.orderByClause() !== undefined; 46 | } 47 | 48 | /* Return limit value if defined, maybe a number or a bound expression */ 49 | limitValue(): number | string | undefined { 50 | const limitClause = this._queryContext.limitClause(); 51 | if (limitClause === undefined) { 52 | return undefined; 53 | } else if (limitClause?.IntegerLiteral() !== undefined) { 54 | return parseInt(limitClause?.IntegerLiteral()?.text as string); 55 | } else { 56 | return limitClause?.boundExpression()?.text as string; 57 | } 58 | } 59 | 60 | /* Return FROM clase SObject name, if there is a single SObject */ 61 | fromObject(): undefined | string { 62 | const fromContext = this._queryContext.fromNameList(); 63 | const fieldNames = fromContext.fieldName(); 64 | if (fieldNames.length === 1) { 65 | return fieldNames[0]?.text; 66 | } else { 67 | return undefined; 68 | } 69 | } 70 | } 71 | 72 | export class SOQLParser { 73 | async parse(query: string): Promise { 74 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 75 | // eslint-disable-next-line @typescript-eslint/naming-convention 76 | const { ApexLexer, ApexParser, CaseInsensitiveInputStream } = await import( 77 | '@apexdevtools/apex-parser' 78 | ); 79 | // Dynamic import for code splitting. Improves performance by reducing the amount of JS that is loaded and parsed at the start. 80 | // eslint-disable-next-line @typescript-eslint/naming-convention 81 | const { CharStreams, CommonTokenStream } = await import('antlr4ts'); 82 | const lexer = new ApexLexer(new CaseInsensitiveInputStream(CharStreams.fromString(query))); 83 | const tokens = new CommonTokenStream(lexer); 84 | const parser = new ApexParser(tokens); 85 | parser.removeErrorListeners(); 86 | parser.addErrorListener(new ThrowingErrorListener()); 87 | return new SOQLTree(parser.query()); 88 | } 89 | } 90 | 91 | export class SyntaxException { 92 | line: number; 93 | column: number; 94 | message: string; 95 | 96 | constructor(line: number, column: number, message: string) { 97 | this.line = line; 98 | this.column = column; 99 | this.message = message; 100 | } 101 | } 102 | 103 | class ThrowingErrorListener implements ANTLRErrorListener { 104 | syntaxError( 105 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 106 | recognizer: Recognizer, 107 | offendingSymbol: Token, 108 | line: number, 109 | charPositionInLine: number, 110 | msg: string, 111 | _e: RecognitionException | undefined, 112 | ): void { 113 | throw new SyntaxException(line, charPositionInLine, msg); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /log-viewer/modules/styles/global.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const globalStyles = css` 4 | a { 5 | color: var(--vscode-textLink-foreground); 6 | text-decoration: none; 7 | cursor: pointer; 8 | 9 | &:hover { 10 | color: var(--vscode-textLink-activeForeground); 11 | text-decoration: underline; 12 | } 13 | 14 | &:active { 15 | background: transparent; 16 | color: var(--vscode-textLink-activeForeground); 17 | text-decoration: underline; 18 | } 19 | } 20 | 21 | ::-webkit-scrollbar { 22 | width: 10px; 23 | height: 10px; 24 | } 25 | 26 | ::-webkit-scrollbar-corner { 27 | background-color: var(--vscode-editor-background); 28 | } 29 | 30 | ::-webkit-scrollbar-thumb { 31 | background-color: var(--vscode-scrollbarSlider-background); 32 | } 33 | 34 | .findMatch { 35 | animation-duration: 0; 36 | animation-name: inherit !important; 37 | color: var(--vscode-editor-findMatchForeground); 38 | background-color: var(--vscode-editor-findMatchHighlightBackground, 'yellow'); 39 | } 40 | 41 | .currentFindMatch { 42 | color: var(--vscode-editor-findMatchHighlightForeground); 43 | background-color: var(--vscode-editor-findMatchBackground, '#8B8000'); 44 | border: 2px solid var(--vscode-editor-findMatchBorder); 45 | padding: 1px; 46 | box-sizing: border-box; 47 | } 48 | `; 49 | -------------------------------------------------------------------------------- /log-viewer/modules/styles/notification.styles.ts: -------------------------------------------------------------------------------- 1 | import { css } from 'lit'; 2 | 3 | export const notificationStyles = css` 4 | --notification-error-background: var(--vscode-editorError-background, rgba(255, 128, 128, 0.2)); 5 | --notification-warning-background: rgba(128, 128, 255, 0.2); 6 | --notification-information-background: rgb(30, 128, 255, 0.2); 7 | `; 8 | -------------------------------------------------------------------------------- /log-viewer/modules/timeline/TimelineKey.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2023 Certinia Inc. All rights reserved. 3 | */ 4 | import { LitElement, css, html } from 'lit'; 5 | import { customElement, property } from 'lit/decorators.js'; 6 | 7 | import { globalStyles } from '../styles/global.styles.js'; 8 | import { type TimelineGroup } from './Timeline.js'; 9 | @customElement('timeline-key') 10 | export class Timelinekey extends LitElement { 11 | @property() 12 | timelineKeys: TimelineGroup[] = []; 13 | 14 | constructor() { 15 | super(); 16 | } 17 | 18 | static styles = [ 19 | globalStyles, 20 | css` 21 | :host { 22 | margin-top: 5px; 23 | } 24 | .timeline-key__entry { 25 | display: inline-block; 26 | font-size: 0.9rem; 27 | padding: 4px; 28 | margin-right: 5px; 29 | color: #ffffff; 30 | font-family: monospace; 31 | } 32 | `, 33 | ]; 34 | 35 | render() { 36 | const keyParts = []; 37 | for (const keyMeta of this.timelineKeys) { 38 | keyParts.push( 39 | html`
40 | ${keyMeta.label} 41 |
`, 42 | ); 43 | } 44 | 45 | return keyParts; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /log-viewer/modules/vscode-ui/VsIconCheckbox.ts: -------------------------------------------------------------------------------- 1 | import { provideVSCodeDesignSystem, vsCodeButton } from '@vscode/webview-ui-toolkit'; 2 | 3 | import { LitElement, css, html, unsafeCSS } from 'lit'; 4 | import { customElement, property } from 'lit/decorators.js'; 5 | import codiconStyles from '../styles/codicon.css'; 6 | import { globalStyles } from '../styles/global.styles.js'; 7 | 8 | provideVSCodeDesignSystem().register(vsCodeButton()); 9 | 10 | @customElement('vs-icon-checkbox') 11 | export class VsIconCheckbox extends LitElement { 12 | static shadowRootOptions = { ...LitElement.shadowRootOptions, delegatesFocus: true }; 13 | 14 | @property() showSelected = false; 15 | @property() checked = false; 16 | @property() title = ''; 17 | 18 | static styles = [ 19 | globalStyles, 20 | unsafeCSS(codiconStyles), 21 | css` 22 | :host { 23 | display: flex; 24 | } 25 | 26 | .icon-checkbox.checked { 27 | color: var(--vscode-inputOption-activeForeground); 28 | border: 1px solid var(--vscode-inputOption-activeBorder); 29 | background: var(--vscode-inputOption-activeBackground); 30 | } 31 | 32 | .icon-checkbox { 33 | border: 1px solid transparent; 34 | cursor: pointer; 35 | user-select: none; 36 | -webkit-user-select: none; 37 | justify-content: center; 38 | box-sizing: border-box; 39 | width: 22px; 40 | height: 22px; 41 | } 42 | `, 43 | ]; 44 | 45 | render() { 46 | return html` 53 | 54 | `; 55 | } 56 | 57 | _toggleChecked() { 58 | this.checked = !this.checked; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /log-viewer/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "description": "LogViewer", 3 | "name": "log-viewer", 4 | "private": true, 5 | "scripts": { 6 | "build": "rollup -c rollup.config.mjs", 7 | "lint": "concurrently 'eslint modules --ext ts' 'npm run tsc:lint'", 8 | "watch": "rollup -w -c rollup.config.mjs", 9 | "web": "http-server", 10 | "debug": "concurrently 'pnpm:web' 'pnpm:watch'", 11 | "prettier-format": "prettier 'modules/**/*.ts' --write", 12 | "tsc:lint": "tsc --noemit --skipLibCheck" 13 | }, 14 | "version": "0.1.0", 15 | "dependencies": { 16 | "@apexdevtools/apex-parser": "^4.4.0", 17 | "@vscode/codicons": "^0.0.36", 18 | "@vscode/webview-ui-toolkit": "^1.4.0", 19 | "lit": "^3.3.0", 20 | "tabulator-tables": "^6.3.1" 21 | }, 22 | "devDependencies": { 23 | "@types/tabulator-tables": "^6.2.6", 24 | "@typescript-eslint/eslint-plugin": "^8.30.1", 25 | "@typescript-eslint/parser": "^8.30.1", 26 | "concurrently": "^9.1.2", 27 | "http-server": "^14.1.1", 28 | "postcss": "^8.5.3", 29 | "sass": "~1.78.0", 30 | "typescript": "^5.8.3" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /log-viewer/rollup.config.mjs: -------------------------------------------------------------------------------- 1 | // Rollup plugins 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 4 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 5 | import postcss from 'rollup-plugin-postcss'; 6 | import { 7 | defineRollupSwcMinifyOption, 8 | defineRollupSwcOption, 9 | minify, 10 | swc, 11 | } from 'rollup-plugin-swc3'; 12 | 13 | const production = !process.env.ROLLUP_WATCH; 14 | export default { 15 | input: 'modules/Main.ts', 16 | output: { 17 | file: 'out/bundle.js', 18 | sourcemap: false, 19 | }, 20 | plugins: [ 21 | nodeResolve({ browser: true, preferBuiltins: false }), 22 | commonjs(), 23 | nodePolyfills(), 24 | swc( 25 | defineRollupSwcOption({ 26 | exclude: 'node_modules', 27 | tsconfig: production ? 'tsconfig.json' : 'tsconfig-dev.json', 28 | jsc: {}, 29 | }) 30 | ), 31 | postcss({ 32 | extensions: ['.css', '.scss'], 33 | minimize: true, 34 | }), 35 | production && 36 | minify( 37 | defineRollupSwcMinifyOption({ 38 | // swc's minify option here 39 | mangle: true, 40 | compress: true, 41 | }) 42 | ), 43 | ], 44 | }; 45 | -------------------------------------------------------------------------------- /log-viewer/tsconfig-dev.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { "skipLibCheck": true } 4 | } 5 | -------------------------------------------------------------------------------- /log-viewer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["ES2022", "dom", "DOM.Iterable"], 4 | 5 | "outDir": "out", 6 | "rootDir": "./modules", 7 | 8 | "esModuleInterop": true, 9 | "skipLibCheck": true, 10 | "target": "es2022", 11 | "verbatimModuleSyntax": true, 12 | "allowJs": true, 13 | "resolveJsonModule": true, 14 | "moduleDetection": "force", 15 | 16 | "strict": true, 17 | "noUncheckedIndexedAccess": true, 18 | 19 | "moduleResolution": "Bundler", 20 | "module": "ESNext", 21 | "noEmit": true, 22 | 23 | "composite": true, 24 | "declarationMap": true, 25 | 26 | "strictFunctionTypes": true, 27 | 28 | "allowSyntheticDefaultImports": true, 29 | "experimentalDecorators": true, 30 | "useDefineForClassFields": false, 31 | "forceConsistentCasingInFileNames": true, 32 | "importHelpers": true, 33 | "isolatedModules": true, 34 | "noEmitOnError": true, 35 | "noUnusedLocals": false, 36 | "noUnusedParameters": false, 37 | "removeComments": true, 38 | "sourceMap": false 39 | }, 40 | "include": ["./modules/**/*.ts", "declarations.d.ts"], 41 | "exclude": ["**/node_modules", "**/.*/"] 42 | } 43 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "lana-ws", 3 | "private": true, 4 | "devDependencies": { 5 | "@eslint/eslintrc": "^3.3.1", 6 | "@eslint/js": "^9.24.0", 7 | "@rollup/plugin-commonjs": "^28.0.3", 8 | "@rollup/plugin-json": "^6.1.0", 9 | "@rollup/plugin-node-resolve": "^16.0.1", 10 | "@rollup/plugin-terser": "^0.4.4", 11 | "@swc/core": "^1.11.21", 12 | "@swc/helpers": "^0.5.17", 13 | "@swc/jest": "^0.2.37", 14 | "@types/jest": "^29.5.14", 15 | "@typescript-eslint/eslint-plugin": "^8.30.1", 16 | "@typescript-eslint/parser": "^8.30.1", 17 | "concurrently": "^9.1.2", 18 | "eslint": "^9.24.0", 19 | "eslint-config-prettier": "^10.1.2", 20 | "husky": "^9.1.7", 21 | "jest": "^29.7.0", 22 | "jest-environment-jsdom": "^29.7.0", 23 | "lint-staged": "^15.5.1", 24 | "prettier": "^3.5.3", 25 | "prettier-plugin-organize-imports": "^4.1.0", 26 | "rollup": "^4.40.0", 27 | "rollup-plugin-copy": "^3.5.0", 28 | "rollup-plugin-polyfill-node": "^0.13.0", 29 | "rollup-plugin-postcss": "^4.0.2", 30 | "rollup-plugin-swc3": "^0.12.1" 31 | }, 32 | "scripts": { 33 | "bump-prerelease": "node ./scripts/pre-release.js", 34 | "preinstall": "npx only-allow pnpm", 35 | "build": "NODE_ENV=production pnpm run build:dev", 36 | "build:dev": "rm -rf lana/out && concurrently -r -g 'rollup -c rollup.config.mjs' 'tsc --noemit --skipLibCheck -p log-viewer/tsconfig.json' 'tsc --noemit --skipLibCheck -p lana/tsconfig.json'", 37 | "watch": "rm -rf lana/out && rollup -w -c rollup.config.mjs", 38 | "prepare": "husky", 39 | "lint": "concurrently -r -g \"eslint '**/*.ts/'\" \"prettier --cache **/*.{ts,css,md,scss} --check\" \"tsc --noemit --skipLibCheck -p log-viewer/tsconfig.json\" \"tsc --noemit --skipLibCheck -p lana/tsconfig.json\"", 40 | "test": "jest", 41 | "test:ci": "jest --runInBand", 42 | "prettier-format": "prettier '**/*.ts' --cache --write" 43 | }, 44 | "lint-staged": { 45 | "*.{ts,css,md,scss}": "prettier --cache --write" 46 | }, 47 | "pnpm": { 48 | "patchedDependencies": { 49 | "@salesforce/bunyan@2.0.0": "patches/@salesforce__bunyan@2.0.0.patch" 50 | }, 51 | "overrides": { 52 | "@salesforce/core>jsforce": "^2.0.0-beta.27", 53 | "@salesforce/apex-node>@salesforce/core": "^4.3.11", 54 | "@apexdevtools/sfdx-auth-helper>jsforce": "^2.0.0-beta.27", 55 | "@apexdevtools/sfdx-auth-helper>@salesforce/core": "^4.3.11" 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - 'lana' 3 | - 'lana-docs-site' 4 | - 'log-viewer' 5 | -------------------------------------------------------------------------------- /rollup.config.mjs: -------------------------------------------------------------------------------- 1 | // Rollup plugins 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import json from '@rollup/plugin-json'; 4 | import { nodeResolve } from '@rollup/plugin-node-resolve'; 5 | import copy from 'rollup-plugin-copy'; 6 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 7 | import postcss from 'rollup-plugin-postcss'; 8 | import { defineRollupSwcOption, swc } from 'rollup-plugin-swc3'; 9 | 10 | const production = process.env.NODE_ENV === 'production'; 11 | console.log('Package mode:', production ? 'production' : 'development'); 12 | export default [ 13 | { 14 | input: './lana/src/Main.ts', 15 | output: { 16 | format: 'cjs', 17 | dir: './lana/out', 18 | chunkFileNames: 'lana-[name].js', 19 | sourcemap: false, 20 | }, 21 | 22 | external: ['vscode'], 23 | plugins: [ 24 | nodeResolve({ preferBuiltins: true, dedupe: ['@salesforce/core'] }), 25 | commonjs(), 26 | json(), 27 | swc( 28 | defineRollupSwcOption({ 29 | include: /\.[mc]?[jt]sx?$/, 30 | exclude: 'node_modules', 31 | tsconfig: production ? './lana/tsconfig.json' : './lana/tsconfig-dev.json', 32 | jsc: { 33 | minify: { 34 | compress: production, 35 | mangle: production 36 | ? { 37 | keep_classnames: true, 38 | } 39 | : false, 40 | }, 41 | }, 42 | }), 43 | ), 44 | ], 45 | }, 46 | { 47 | input: { bundle: './log-viewer/modules/Main.ts' }, 48 | output: [ 49 | { 50 | format: 'es', 51 | dir: './log-viewer/out', 52 | chunkFileNames: 'log-viewer-[name].js', 53 | sourcemap: false, 54 | }, 55 | ], 56 | plugins: [ 57 | nodeResolve({ browser: true, preferBuiltins: false }), 58 | commonjs(), 59 | nodePolyfills(), 60 | swc( 61 | defineRollupSwcOption({ 62 | // All options are optional 63 | include: /\.[mc]?[jt]sx?$/, 64 | exclude: 'node_modules', 65 | tsconfig: production ? './log-viewer/tsconfig.json' : './log-viewer/tsconfig-dev.json', 66 | jsc: { 67 | transform: { useDefineForClassFields: false }, 68 | minify: { 69 | compress: production, 70 | mangle: production 71 | ? { 72 | keep_classnames: true, 73 | } 74 | : false, 75 | }, 76 | }, 77 | }), 78 | ), 79 | postcss({ 80 | extensions: ['.css', '.scss'], 81 | minimize: true, 82 | }), 83 | copy({ 84 | hook: 'closeBundle', 85 | targets: [ 86 | { 87 | src: [ 88 | 'log-viewer/out/*', 89 | 'log-viewer/index.html', 90 | 'lana/certinia-icon-color.png', 91 | 'node_modules/@vscode/codicons/dist/codicon.ttf', 92 | ], 93 | dest: 'lana/out', 94 | }, 95 | { src: ['CHANGELOG.md', 'LICENSE.txt', 'README.md'], dest: 'lana' }, 96 | ], 97 | }), 98 | ], 99 | }, 100 | ]; 101 | -------------------------------------------------------------------------------- /scripts/pre-release.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line 2 | const fs = require('fs'); 3 | 4 | // Update version for pre release 5 | // get version string e.g 1.9.0 remove the last patch number (.0) and replace with yyyymmdd e.g 1.9.20230810 6 | const today = new Date(); 7 | const preReleaseTag = `${today.getFullYear()}${String(today.getMonth() + 1).padStart( 8 | 2, 9 | '0' 10 | )}${String(today.getDate()).padStart(2, '0')}`; 11 | 12 | // eslint-disable-next-line 13 | const packageJSON = require('../lana/package.json'); 14 | const versionParts = packageJSON.version.split('.'); 15 | // The minor number (major.minor.patch) will always be even for stable so +1 will move to the next odd for pre release. 16 | const version = `${versionParts[0]}.${Number(versionParts[1]) + 1}.${preReleaseTag}`; 17 | const newpackageJSON = 18 | JSON.stringify( 19 | { 20 | ...packageJSON, 21 | version: version, 22 | }, 23 | null, 24 | 2 25 | ) + '\n'; 26 | fs.writeFileSync('./lana/package.json', newpackageJSON); 27 | --------------------------------------------------------------------------------