├── .eslintrc.js ├── .github └── workflows │ ├── publish.yml │ └── test.yml ├── .gitignore ├── .npmrc ├── .prettierrc.yaml ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── LICENSE ├── assets ├── bad.png ├── brave.png ├── chrome.png ├── edge.png ├── firefox.png ├── good.png ├── hero.png ├── preview.png ├── storybook.png └── stubs.png ├── good.png ├── jest.config.js ├── jest.setup.test.ts ├── jest.setup.ts ├── package-lock.json ├── package.json ├── patches └── iso-url+0.4.7.patch ├── pnpm-lock.yaml ├── readme.md ├── rollup.config.js ├── src ├── api │ ├── actors.ts │ ├── idl │ │ ├── all.d.ts │ │ ├── all.ts │ │ ├── candid-ui.d.ts │ │ ├── candid-ui.ts │ │ ├── dab-canisters.did.d.ts │ │ ├── dab-canisters.did.ts │ │ ├── dab-nfts.did.d.ts │ │ ├── dab-nfts.did.ts │ │ ├── dab-tokens.did.d.ts │ │ └── dab-tokens.did.ts │ └── readme.md ├── assets │ ├── 128.png │ ├── 16.png │ ├── 48.png │ ├── icons.svg │ └── logo.png ├── declaration.d.ts ├── entries │ ├── devtools │ │ ├── app.tsx │ │ ├── index.html │ │ └── styles.css │ └── sandbox │ │ ├── index.html │ │ ├── index.ts │ │ └── readme.md ├── index.html ├── index.ts ├── manifest.json ├── services │ ├── candid │ │ ├── decode-no-interface.ts │ │ ├── decode.ts │ │ ├── index.ts │ │ └── interfaces.ts │ ├── capture │ │ ├── handler.ts │ │ ├── index.ts │ │ ├── request.ts │ │ ├── response.ts │ │ ├── select.test.ts │ │ └── select.ts │ ├── common.test.ts │ ├── common.ts │ ├── dab │ │ ├── canister-details.ts │ │ └── index.ts │ ├── logging │ │ ├── common.ts │ │ ├── index.ts │ │ ├── messages.ts │ │ ├── requests.ts │ │ └── store.ts │ └── sandbox │ │ ├── dab.ts │ │ ├── decode.ts │ │ ├── handler.ts │ │ ├── index.ts │ │ ├── interfaces.test.ts │ │ └── interfaces.ts ├── stubs │ ├── index.test.ts │ ├── index.ts │ ├── interfaces │ │ ├── 27iad-5iaaa-aaaah-qbr5q-cai.ts │ │ ├── index.ts │ │ ├── nprnb-waaaa-aaaaj-qax4a-cai.ts │ │ ├── r5m4o-xaaaa-aaaah-qbpfq-cai.ts │ │ ├── rrkah-fqaaa-aaaaa-aaaaq-cai.ts │ │ └── tzvxm-jqaaa-aaaaj-qabga-cai.ts │ └── messages │ │ ├── distrikt.getFollowCounts.json │ │ ├── distrikt.getLatestUsers.json │ │ ├── distrikt.getSelf.json │ │ ├── distrikt.getSelfUserId.json │ │ ├── distrikt.isFollowing.json │ │ ├── distrikt.isUserTrusted.json │ │ ├── dscvr.get_notifications.json │ │ ├── dscvr.get_self.json │ │ ├── dscvr.list_content.json │ │ ├── dscvr.list_highlighted_portals.json │ │ ├── dscvr.tipsOfContentIDs.json │ │ ├── ghost.transfer.err.json │ │ ├── ghost.transfer.ok.json │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── legends.listings.json │ │ ├── likes.count.json │ │ ├── metascore.getGames.json │ │ ├── metascore.getPlayerCount.json │ │ ├── metascore.getScoreCount.json │ │ └── schema.json └── ui │ ├── common.test.ts │ ├── common.ts │ ├── details.module.css │ ├── details.stories.tsx │ ├── details.tsx │ ├── root.module.css │ ├── root.stories.tsx │ └── root.tsx └── tsconfig.json /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | node: true, 6 | webextensions: true 7 | }, 8 | extends: ['eslint:recommended', 'plugin:react/recommended', 'plugin:@typescript-eslint/recommended', 'plugin:storybook/recommended', 'plugin:storybook/recommended', 'plugin:storybook/recommended'], 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { 12 | jsx: true 13 | }, 14 | ecmaVersion: 12, 15 | sourceType: 'module' 16 | }, 17 | plugins: ['react', '@typescript-eslint'], 18 | rules: {} 19 | }; -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | push: 5 | tags: 6 | - v** 7 | 8 | jobs: 9 | build: 10 | name: Publish webextension 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - name: Install 19 | run: npm ci 20 | - name: Test 21 | run: npm t 22 | - name: Build 23 | run: npm run release 24 | - name: Upload & release 25 | uses: mnao305/chrome-extension-upload@v5.0.0 26 | with: 27 | file-path: releases/*.zip 28 | glob: true 29 | extension-id: ${{ secrets.EXTENSION_ID }} 30 | client-id: ${{ secrets.CLIENT_ID }} 31 | client-secret: ${{ secrets.CLIENT_SECRET }} 32 | refresh-token: ${{ secrets.REFRESH_TOKEN }} 33 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | jobs: 9 | build: 10 | name: Run tests 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v1 15 | - uses: actions/setup-node@v1 16 | with: 17 | node-version: 14 18 | - name: Install 19 | run: npm ci 20 | - name: Test 21 | run: npm t 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Chrome extension release zip files 2 | releases 3 | 4 | # Logs 5 | logs 6 | *.log 7 | npm-debug.log* 8 | yarn-debug.log* 9 | yarn-error.log* 10 | lerna-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Directory for instrumented libs generated by jscoverage/JSCover 22 | lib-cov 23 | 24 | # Coverage directory used by tools like istanbul 25 | coverage 26 | *.lcov 27 | 28 | # nyc test coverage 29 | .nyc_output 30 | 31 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 32 | .grunt 33 | 34 | # Bower dependency directory (https://bower.io/) 35 | bower_components 36 | 37 | # node-waf configuration 38 | .lock-wscript 39 | 40 | # Compiled binary addons (https://nodejs.org/api/addons.html) 41 | build/Release 42 | 43 | # Dependency directories 44 | node_modules/ 45 | jspm_packages/ 46 | 47 | # TypeScript v1 declaration files 48 | typings/ 49 | 50 | # TypeScript cache 51 | *.tsbuildinfo 52 | 53 | # Optional npm cache directory 54 | .npm 55 | 56 | # Optional eslint cache 57 | .eslintcache 58 | 59 | # Microbundle cache 60 | .rpt2_cache/ 61 | .rts2_cache_cjs/ 62 | .rts2_cache_es/ 63 | .rts2_cache_umd/ 64 | 65 | # Optional REPL history 66 | .node_repl_history 67 | 68 | # Output of 'npm pack' 69 | *.tgz 70 | 71 | # Yarn Integrity file 72 | .yarn-integrity 73 | 74 | # dotenv environment variables file 75 | .env 76 | .env.test 77 | 78 | # parcel-bundler cache (https://parceljs.org/) 79 | .cache 80 | 81 | # Next.js build output 82 | .next 83 | 84 | # Nuxt.js build / generate output 85 | .nuxt 86 | dist 87 | 88 | # Gatsby files 89 | .cache/ 90 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 91 | # https://nextjs.org/blog/next-9-1#public-directory-support 92 | # public 93 | 94 | # vuepress build output 95 | .vuepress/dist 96 | 97 | # Serverless directories 98 | .serverless/ 99 | 100 | # FuseBox cache 101 | .fusebox/ 102 | 103 | # DynamoDB Local files 104 | .dynamodb/ 105 | 106 | # TernJS port file 107 | .tern-port 108 | 109 | .DS_Store -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | legacy-peer-deps=true 2 | -------------------------------------------------------------------------------- /.prettierrc.yaml: -------------------------------------------------------------------------------- 1 | semi: true 2 | tabWidth: 4 3 | trailingComma: 'all' 4 | singleQuote: true 5 | arrowParens: 'always' 6 | proseWrap: 'always' -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | const path = require('path') 2 | module.exports = { 3 | "stories": [ 4 | "../src/**/*.stories.mdx", 5 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 6 | ], 7 | "addons": [ 8 | "@storybook/addon-links", 9 | "@storybook/addon-essentials", 10 | "@storybook/addon-interactions" 11 | ], 12 | "framework": "@storybook/react", 13 | "core": { 14 | "builder": "@storybook/builder-webpack5" 15 | }, 16 | webpackFinal: async (config, { configType }) => { 17 | // `configType` has a value of 'DEVELOPMENT' or 'PRODUCTION' 18 | // You can change the configuration based on that. 19 | // 'PRODUCTION' is used when building the static version of storybook. 20 | 21 | // Make whatever fine-grained changes you need 22 | config.resolve.alias['/assets/icons.svg'] = path.resolve(__dirname, '../src/assets/icons.svg'); 23 | 24 | // Return the altered config 25 | return config; 26 | }, 27 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | import "!style-loader!css-loader!../src/entries/devtools/styles.css" 2 | 3 | export const parameters = { 4 | actions: { argTypesRegex: "^on[A-Z].*" }, 5 | controls: { 6 | matchers: { 7 | color: /(background|color)$/i, 8 | date: /Date$/, 9 | }, 10 | }, 11 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 jorgenbuilder 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /assets/bad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/bad.png -------------------------------------------------------------------------------- /assets/brave.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/brave.png -------------------------------------------------------------------------------- /assets/chrome.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/chrome.png -------------------------------------------------------------------------------- /assets/edge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/edge.png -------------------------------------------------------------------------------- /assets/firefox.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/firefox.png -------------------------------------------------------------------------------- /assets/good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/good.png -------------------------------------------------------------------------------- /assets/hero.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/hero.png -------------------------------------------------------------------------------- /assets/preview.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/preview.png -------------------------------------------------------------------------------- /assets/storybook.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/storybook.png -------------------------------------------------------------------------------- /assets/stubs.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/assets/stubs.png -------------------------------------------------------------------------------- /good.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/good.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '.+\\.ts$': 'ts-jest', 4 | }, 5 | testRegex: '(/__tests__/.*|(\\.|/)(test|spec))\\.ts$', 6 | moduleFileExtensions: ['ts', 'js'], 7 | setupFilesAfterEnv: ['./jest.setup.ts'], 8 | } 9 | -------------------------------------------------------------------------------- /jest.setup.test.ts: -------------------------------------------------------------------------------- 1 | // import { chrome as jestChrome } from 'jest-chrome'; 2 | 3 | test('browser is defined in the global scope', () => { 4 | expect(browser).toBeDefined(); 5 | expect(browser.runtime).toBeDefined(); 6 | // This will be undefined if no mock implementation is provided 7 | expect(browser.runtime.sendMessage).toBeUndefined(); 8 | }); 9 | 10 | // test('browser api methods are defined after implementation in chrome api', () => { 11 | // // You need to add an implementation for each Chrome API method you use 12 | // // Methods will be present in the Chrome API without implementations 13 | // // but unused methods in the Browser API will be undefined 14 | // jestChrome.runtime.sendMessage.mockImplementation((message, cb) => { 15 | // cb({ greeting: 'test-response' }); 16 | // }); 17 | 18 | // expect(browser.runtime.sendMessage).toBeDefined(); 19 | // }); 20 | 21 | test('chrome is mocked in the global scope', () => { 22 | expect(chrome).toBeDefined(); 23 | expect(chrome.runtime).toBeDefined(); 24 | expect(chrome.runtime.sendMessage).toBeDefined(); 25 | }); 26 | 27 | test('chrome devtools is mocked in the global scope', async () => { 28 | expect(chrome.devtools).toBeDefined(); 29 | expect(chrome.devtools.network).toBeDefined(); 30 | expect(chrome.devtools.network.onRequestFinished.addListener).toBeDefined(); 31 | }); 32 | -------------------------------------------------------------------------------- /jest.setup.ts: -------------------------------------------------------------------------------- 1 | import { chrome } from 'jest-chrome'; 2 | import { TextEncoder, TextDecoder } from 'util'; 3 | import 'whatwg-fetch'; 4 | 5 | // @ts-expect-error we need to set this to use browser polyfill 6 | chrome.runtime.id = 'test id'; 7 | Object.assign(global, { chrome }); 8 | 9 | // We need to require this after we setup jest chrome 10 | // eslint-disable-next-line @typescript-eslint/no-var-requires 11 | const browser = require('webextension-polyfill'); 12 | Object.assign(global, { browser }); 13 | 14 | global.TextEncoder = TextEncoder; 15 | // @ts-ignore 16 | global.TextDecoder = TextDecoder; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ic-inspector", 3 | "private": false, 4 | "description": "Decode network responses from the Internet Computer blockchain.", 5 | "author": "Jorgen Builder", 6 | "engines": { 7 | "node": "17.5" 8 | }, 9 | "scripts": { 10 | "build": "rollup -c", 11 | "release": "cross-env NODE_ENV=production rollup -c", 12 | "start": "rollup -c -w --verbose", 13 | "test": "jest", 14 | "test:watch": "jest --watch", 15 | "postinstall": "patch-package", 16 | "pretty": "npx prettier --write \"**/*.ts(x)?\"", 17 | "storybook": "start-storybook -p 6006", 18 | "build-storybook": "build-storybook" 19 | }, 20 | "dependencies": { 21 | "@dfinity/agent": "^0.11.0", 22 | "@dfinity/candid": "^0.11.0", 23 | "@dfinity/principal": "^0.11.0", 24 | "@extend-chrome/messages": "^1.2.2", 25 | "@rollup/plugin-replace": "^3.0.0", 26 | "@types/chrome": "^0.0.193", 27 | "@types/firefox-webext-browser": "^82.0.0", 28 | "@types/react": "^17.0.0", 29 | "@types/react-dom": "^17.0.0", 30 | "cbor": "^8.1.0", 31 | "cbor-x": "^1.3.3", 32 | "ictool": "^0.0.6", 33 | "jest-fetch-mock": "^3.0.3", 34 | "object-sizeof": "^1.6.3", 35 | "prettier": "^2.2.1", 36 | "react": "^17.0.2", 37 | "react-dom": "^17.0.2", 38 | "react-json-view-lite": "github:jorgenbuilder/react-json-view-lite#release", 39 | "react-table": "^7.7.0", 40 | "uuid": "^8.3.2", 41 | "webextension-polyfill": "^0.7.0", 42 | "zustand": "^4.0.0" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.18.10", 46 | "@modular-css/rollup": "^28.2.2", 47 | "@rollup/plugin-alias": "^3.1.1", 48 | "@rollup/plugin-commonjs": "^17.0.0", 49 | "@rollup/plugin-json": "^4.1.0", 50 | "@rollup/plugin-node-resolve": "^11.0.1", 51 | "@rollup/plugin-typescript": "^8.1.0", 52 | "@rollup/plugin-wasm": "^5.2.0", 53 | "@storybook/addon-actions": "^6.5.10", 54 | "@storybook/addon-essentials": "^6.5.10", 55 | "@storybook/addon-interactions": "^6.5.10", 56 | "@storybook/addon-links": "^6.5.10", 57 | "@storybook/builder-webpack5": "^6.5.10", 58 | "@storybook/manager-webpack5": "^6.5.10", 59 | "@storybook/react": "^6.5.10", 60 | "@storybook/testing-library": "^0.0.13", 61 | "@types/jest": "^26.0.19", 62 | "@types/react-table": "^7.7.9", 63 | "@types/uuid": "^8.3.4", 64 | "@typescript-eslint/eslint-plugin": "^4.10.0", 65 | "@typescript-eslint/parser": "^4.10.0", 66 | "ajv": "^8.11.0", 67 | "babel-loader": "^8.2.5", 68 | "cross-env": "^7.0.3", 69 | "eslint": "^7.16.0", 70 | "eslint-plugin-react": "^7.21.5", 71 | "eslint-plugin-storybook": "^0.6.4", 72 | "jest": "^26.6.3", 73 | "jest-chrome": "^0.7.0", 74 | "js-sha256": "^0.9.0", 75 | "node-fetch": "^3.2.10", 76 | "patch-package": "^6.4.7", 77 | "rollup": "^2.56.3", 78 | "rollup-plugin-chrome-extension": "^3.6.1", 79 | "rollup-plugin-empty-dir": "^1.0.5", 80 | "rollup-plugin-polyfill-node": "^0.7.0", 81 | "rollup-plugin-postcss": "^4.0.2", 82 | "rollup-plugin-zip": "^1.0.1", 83 | "ts-jest": "^26.4.4", 84 | "tslib": "^2.0.3", 85 | "typescript": "^4.1.3", 86 | "whatwg-fetch": "^3.6.2", 87 | "z-schema": "^5.0.3" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /patches/iso-url+0.4.7.patch: -------------------------------------------------------------------------------- 1 | diff --git a/node_modules/iso-url/src/url.js b/node_modules/iso-url/src/url.js 2 | index a57894b..7d08c83 100644 3 | --- a/node_modules/iso-url/src/url.js 4 | +++ b/node_modules/iso-url/src/url.js 5 | @@ -1,6 +1,6 @@ 6 | 'use strict'; 7 | 8 | -const { URL, URLSearchParams, format } = require('url'); 9 | +const { format } = require('url'); 10 | 11 | // https://github.com/nodejs/node/issues/12682 12 | const defaultBase = 'http://localhost'; 13 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![IC Inspector: browser devtools for the internet computer blockchain](assets/hero.png) 2 | 3 |
4 | 5 | download on chrome webstore 6 | 7 | 8 | download on firefox webstore 9 | 10 | 11 | download for edge on chrome webstore 12 | 13 | 14 | download for brave on chrome webstore 15 | 16 |
17 |
18 | 19 | [The Internet Computer](https://internetcomputer.org/) is a uniquely powerful 20 | blockchain. With this browser extension, you can inspect messages exchanged 21 | between your browser and the IC blockchain. IC Inspector provides developers 22 | with crucial tooling for blockchain projects using the browser devtools paradigm 23 | they already know. 24 | 25 | ![preview](assets/preview.png) 26 | 27 | ## How Does It Work? 28 | 29 | The lifecycle of a message between your browser and any IC smart contract (or 30 | "canister") involves a few layers of encoding. 31 | 32 | 1. Data on the wire is sent in concise binary object representation (CBOR) 33 | format. Decoding this is fairly straightfoward. 34 | 35 | 2. Clients and smart contracts use Candid, a proprietary interface definition 36 | language (IDL), to encode arugment types, return types, and canister 37 | interfaces. IC Inspector automatically retrieves IDL definitions from the 38 | canister itself in order to accurately parse data. 39 | 40 | 3. An agent library called agent-js runs in the browser, sending https requests 41 | to boundary nodes which communicate with the blockchain itself. IC Inspector 42 | monitors these https requests and maps them back into a representation of the 43 | actual blockchain messages for easy reading. 44 | 45 | 4. The blockchain returns a pruned merkle tree containing the subset of 46 | blockchain state which constitutes a particular message response. IC 47 | Inspector picks your message out of this tree. 48 | 49 | ## Development 50 | 51 | For development with automatic reloading: 52 | 53 | ```sh 54 | npm run start 55 | ``` 56 | 57 | Open the [Extensions Dashboard](chrome://extensions), enable "Developer mode", 58 | click "Load unpacked", and choose the `dist` folder. 59 | 60 | You will see the "Dfinity" tab in your devtools window. 61 | 62 | When you make changes in `src` the background script and any content script will 63 | reload automatically. 64 | 65 | ### Storybook 66 | 67 | To simplify testing and development the extension's UIs, storybook is available 68 | with a set of message stubs captured from real IC apps. 69 | 70 | `npm run storybook` 71 | 72 | ![assets/storybook.png](assets/storybook.png) 73 | 74 | ## Releasing 75 | 76 | Pushing a new tag to github will trigger a release to the chrome webstore. 77 | 78 | 1. Bump the version in `manifest.json` 79 | 2. Create a new git tag with the same string 80 | 3. Push code and tags to github 81 | 82 | ```sh 83 | git tag vX.X.X 84 | git push origin main 85 | git push origin --tags 86 | ``` 87 | 88 | ## Funding 89 | 90 | This library was originally incentivized by [ICDevs](https://ICDevs.org). You 91 | can view more about the bounty on the 92 | [forum](https://forum.dfinity.org/t/cbor-plug-in-or-tools/4556/27?u=skilesare) 93 | or [website](https://icdevs.org/bounties/2021/11/23/CBOR-plug-in.html). The 94 | bounty was funded by The ICDevs.org commuity and the award paid to 95 | @jorgenbuilder. If you use this library and gain value from it, please consider 96 | a [donation](https://icdevs.org/donations.html) to ICDevs. 97 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import path from 'path'; 2 | 3 | import nodePolyfills from 'rollup-plugin-polyfill-node'; 4 | import resolve from '@rollup/plugin-node-resolve'; 5 | import commonjs from '@rollup/plugin-commonjs'; 6 | import typescript from '@rollup/plugin-typescript'; 7 | 8 | import { 9 | chromeExtension, 10 | simpleReloader, 11 | } from 'rollup-plugin-chrome-extension'; 12 | import { emptyDir } from 'rollup-plugin-empty-dir'; 13 | import zip from 'rollup-plugin-zip'; 14 | import replace from '@rollup/plugin-replace'; 15 | import { wasm } from '@rollup/plugin-wasm'; 16 | import postcss from 'rollup-plugin-postcss' 17 | import json from '@rollup/plugin-json'; 18 | 19 | const isProduction = process.env.NODE_ENV === 'production'; 20 | 21 | export default { 22 | input: 'src/manifest.json', 23 | output: { 24 | dir: 'dist', 25 | format: 'esm', 26 | chunkFileNames: path.join('chunks', '[name]-[hash].js'), 27 | // sourcemap: 'inline' 28 | }, 29 | plugins: [ 30 | json(), 31 | postcss({ 32 | modules: true, 33 | }), 34 | wasm(), 35 | commonjs(), 36 | replace({ 37 | 'process.env.NODE_ENV': isProduction 38 | ? JSON.stringify('production') 39 | : JSON.stringify('development'), 40 | preventAssignment: true, 41 | }), 42 | chromeExtension(), 43 | // Adds a Chrome extension reloader during watch mode 44 | simpleReloader(), 45 | nodePolyfills(), 46 | resolve(), 47 | typescript(), 48 | // Empties the output dir before a new build 49 | emptyDir(), 50 | // Outputs a zip file in ./releases 51 | isProduction && zip({ dir: 'releases' }), 52 | ], 53 | }; 54 | -------------------------------------------------------------------------------- /src/api/actors.ts: -------------------------------------------------------------------------------- 1 | // A global singleton for our internet computer actors. 2 | import * as Agent from '@dfinity/agent'; 3 | import { InterfaceFactory } from '@dfinity/candid/lib/cjs/idl'; 4 | 5 | import type { All } from './idl/all.d'; 6 | import { idlFactory as allIDL } from './idl/all'; 7 | import type { CandidUI } from './idl/candid-ui.d'; 8 | import { idlFactory as candidIDL } from './idl/candid-ui'; 9 | import DABCanisters from './idl/dab-canisters.did.d'; 10 | import { idlFactory as dabCanistersIDL } from './idl/dab-canisters.did'; 11 | import DABTokens from './idl/dab-tokens.did.d'; 12 | import { idlFactory as dabTokensIDL } from './idl/dab-tokens.did'; 13 | import DABNFTs from './idl/dab-nfts.did.d'; 14 | import { idlFactory as dabNFTsIDL } from './idl/dab-nfts.did'; 15 | 16 | ///////////// 17 | // Config // 18 | /////////// 19 | 20 | const canisters: { [key: string]: string } = { 21 | candidUI: 'a4gq6-oaaaa-aaaab-qaa4q-cai', 22 | dabCanisters: 'curr3-vaaaa-aaaah-abbdq-cai', 23 | dabTokens: 'qwt65-nyaaa-aaaah-qcl4q-cai', 24 | dabNFTs: 'aipdg-waaaa-aaaah-aaq5q-cai', 25 | }; 26 | 27 | const host = 'https://ic0.app'; 28 | const hostLocal = 'http://localhost:8000'; 29 | 30 | //////////// 31 | // Agent // 32 | ////////// 33 | 34 | export const agent = new Agent.HttpAgent({ host }); 35 | const agentLocal = new Agent.HttpAgent({ host: hostLocal }); 36 | agentLocal.fetchRootKey(); 37 | 38 | ///////////// 39 | // Actors // 40 | /////////// 41 | 42 | export const candidUI = actor(canisters.candidUI, candidIDL); 43 | export const dabCanisters = actor( 44 | canisters.dabCanisters, 45 | dabCanistersIDL, 46 | ); 47 | export const dabTokens = actor(canisters.dabTokens, dabTokensIDL); 48 | export const dabNFTs = actor(canisters.dabNFTs, dabNFTsIDL); 49 | export const canister = generic(allIDL); 50 | 51 | ////////// 52 | // Lib // 53 | //////// 54 | 55 | // Map of existing actors 56 | // const actors: { 57 | // [key: string]: { 58 | // actor: Agent.ActorSubclass 59 | // local: Agent.ActorSubclass 60 | // idl: InterfaceFactory 61 | // } 62 | // } = {} 63 | 64 | // Create an actor. 65 | export function actor( 66 | canisterId: string, 67 | idl: InterfaceFactory, 68 | isLocal = false, 69 | config?: Agent.ActorConfig, 70 | ): Agent.ActorSubclass { 71 | const actor = 72 | // (actors[canisterId].actor as Agent.ActorSubclass) || 73 | Agent.Actor.createActor(idl, { canisterId, agent, ...config }); 74 | const local = 75 | // (actors[canisterId].local as Agent.ActorSubclass) || 76 | Agent.Actor.createActor(idl, { 77 | canisterId, 78 | agent: agentLocal, 79 | ...config, 80 | }); 81 | // actors[canisterId] = { actor, idl, local } 82 | return isLocal ? local : actor; 83 | } 84 | 85 | // Create a function that creates an actor using a generic IDL. 86 | function generic(idl: InterfaceFactory, local = false) { 87 | return function (canisterId: string): Agent.ActorSubclass { 88 | return actor(canisterId, idl, local); 89 | }; 90 | } 91 | -------------------------------------------------------------------------------- /src/api/idl/all.d.ts: -------------------------------------------------------------------------------- 1 | export interface All { 2 | __get_candid_interface_tmp_hack: () => string; 3 | } 4 | -------------------------------------------------------------------------------- /src/api/idl/all.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 3 | return IDL.Service({ 4 | __get_candid_interface_tmp_hack: IDL.Func([], [IDL.Text], ['query']), 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/idl/candid-ui.d.ts: -------------------------------------------------------------------------------- 1 | export interface CandidUI { 2 | did_to_js: (string) => [string] | []; 3 | } 4 | -------------------------------------------------------------------------------- /src/api/idl/candid-ui.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 3 | return IDL.Service({ 4 | did_to_js: IDL.Func([IDL.Text], [IDL.Opt(IDL.Text)], ['query']), 5 | }); 6 | }; 7 | -------------------------------------------------------------------------------- /src/api/idl/dab-canisters.did.d.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | export interface CanisterMetadata { 3 | thumbnail: string; 4 | name: string; 5 | frontend: [] | [string]; 6 | description: string; 7 | principal_id: Principal; 8 | details: Array<[string, DetailValue]>; 9 | } 10 | export declare type DetailType = 11 | | bigint 12 | | Array 13 | | Array 14 | | string 15 | | true 16 | | false 17 | | number 18 | | Principal; 19 | export declare type DetailValue = 20 | | { 21 | I64: bigint; 22 | } 23 | | { 24 | U64: bigint; 25 | } 26 | | { 27 | Vec: Array; 28 | } 29 | | { 30 | Slice: Array; 31 | } 32 | | { 33 | Text: string; 34 | } 35 | | { 36 | True: null; 37 | } 38 | | { 39 | False: null; 40 | } 41 | | { 42 | Float: number; 43 | } 44 | | { 45 | Principal: Principal; 46 | }; 47 | export declare type OperationError = 48 | | { 49 | NotAuthorized: null; 50 | } 51 | | { 52 | BadParameters: null; 53 | } 54 | | { 55 | Unknown: string; 56 | } 57 | | { 58 | NonExistentItem: null; 59 | }; 60 | export declare type OperationResponse = 61 | | { 62 | Ok: [] | [string]; 63 | } 64 | | { 65 | Err: OperationError; 66 | }; 67 | export default interface CanisterRegistry { 68 | add: (arg_1: CanisterMetadata) => Promise; 69 | get: (arg_0: Principal) => Promise<[] | [CanisterMetadata]>; 70 | get_all: () => Promise>; 71 | name: () => Promise; 72 | remove: (arg_0: Principal) => Promise; 73 | } 74 | -------------------------------------------------------------------------------- /src/api/idl/dab-canisters.did.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | 3 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 4 | const detail_value = IDL.Rec(); 5 | detail_value.fill( 6 | IDL.Variant({ 7 | I64: IDL.Int64, 8 | U64: IDL.Nat64, 9 | Vec: IDL.Vec(detail_value), 10 | Slice: IDL.Vec(IDL.Nat8), 11 | Text: IDL.Text, 12 | True: IDL.Null, 13 | False: IDL.Null, 14 | Float: IDL.Float64, 15 | Principal: IDL.Principal, 16 | }), 17 | ); 18 | const canister_metadata = IDL.Record({ 19 | thumbnail: IDL.Text, 20 | name: IDL.Text, 21 | frontend: IDL.Opt(IDL.Text), 22 | description: IDL.Text, 23 | principal_id: IDL.Principal, 24 | details: IDL.Vec(IDL.Tuple(IDL.Text, detail_value)), 25 | }); 26 | const operation_error = IDL.Variant({ 27 | NotAuthorized: IDL.Null, 28 | BadParameters: IDL.Null, 29 | Unknown: IDL.Text, 30 | NonExistentItem: IDL.Null, 31 | }); 32 | const operation_response = IDL.Variant({ 33 | Ok: IDL.Opt(IDL.Text), 34 | Err: operation_error, 35 | }); 36 | return IDL.Service({ 37 | add: IDL.Func([canister_metadata], [operation_response], []), 38 | get: IDL.Func([IDL.Principal], [IDL.Opt(canister_metadata)], ['query']), 39 | get_all: IDL.Func([], [IDL.Vec(canister_metadata)], ['query']), 40 | name: IDL.Func([], [IDL.Text], ['query']), 41 | remove: IDL.Func([IDL.Principal], [operation_response], []), 42 | }); 43 | }; 44 | -------------------------------------------------------------------------------- /src/api/idl/dab-nfts.did.d.ts: -------------------------------------------------------------------------------- 1 | import { Principal } from '@dfinity/principal'; 2 | 3 | export type GetAllResult = DABCollection[]; 4 | 5 | export interface DABCollection { 6 | icon: string; 7 | name: string; 8 | description: string; 9 | principal_id: Principal; 10 | standard: string; 11 | } 12 | export interface NFTCanister { 13 | icon: string; 14 | name: string; 15 | description: string; 16 | timestamp: bigint; 17 | principal_id: Principal; 18 | standard: string; 19 | } 20 | export type OperationError = 21 | | { NotAuthorized: null } 22 | | { BadParameters: null } 23 | | { NonExistentItem: null } 24 | | { ParamatersNotPassed: null }; 25 | export type OperationResponse = { Ok: boolean } | { Err: OperationError }; 26 | export default interface _SERVICE { 27 | add: (arg_0: DABCollection) => Promise; 28 | edit: ( 29 | arg_0: string, 30 | arg_1: [] | [Principal], 31 | arg_2: [] | [string], 32 | arg_3: [] | [string], 33 | arg_4: [] | [string], 34 | ) => Promise; 35 | get_all: () => Promise; 36 | get_canister: (arg_0: string) => Promise<[] | [NFTCanister]>; 37 | name: () => Promise; 38 | remove: (arg_0: string) => Promise; 39 | } 40 | -------------------------------------------------------------------------------- /src/api/idl/dab-nfts.did.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | 3 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 4 | const DABCollection = IDL.Record({ 5 | icon: IDL.Text, 6 | name: IDL.Text, 7 | description: IDL.Text, 8 | principal_id: IDL.Principal, 9 | standard: IDL.Text, 10 | }); 11 | const OperationError = IDL.Variant({ 12 | NotAuthorized: IDL.Null, 13 | BadParameters: IDL.Null, 14 | NonExistentItem: IDL.Null, 15 | ParamatersNotPassed: IDL.Null, 16 | }); 17 | const OperationResponse = IDL.Variant({ 18 | Ok: IDL.Bool, 19 | Err: OperationError, 20 | }); 21 | const NFTCanister = IDL.Record({ 22 | icon: IDL.Text, 23 | name: IDL.Text, 24 | description: IDL.Text, 25 | timestamp: IDL.Nat64, 26 | principal_id: IDL.Principal, 27 | standard: IDL.Text, 28 | }); 29 | return IDL.Service({ 30 | add: IDL.Func([DABCollection], [OperationResponse], []), 31 | edit: IDL.Func( 32 | [ 33 | IDL.Text, 34 | IDL.Opt(IDL.Principal), 35 | IDL.Opt(IDL.Text), 36 | IDL.Opt(IDL.Text), 37 | IDL.Opt(IDL.Text), 38 | ], 39 | [OperationResponse], 40 | [], 41 | ), 42 | get_all: IDL.Func([], [IDL.Vec(NFTCanister)], []), 43 | get_canister: IDL.Func([IDL.Text], [IDL.Opt(NFTCanister)], ['query']), 44 | name: IDL.Func([], [IDL.Text], ['query']), 45 | remove: IDL.Func([IDL.Text], [OperationResponse], []), 46 | }); 47 | }; 48 | -------------------------------------------------------------------------------- /src/api/idl/dab-tokens.did.d.ts: -------------------------------------------------------------------------------- 1 | import type { Principal } from '@dfinity/principal'; 2 | export type detail_value = 3 | | { I64: bigint } 4 | | { U64: bigint } 5 | | { Vec: Array } 6 | | { Slice: Array } 7 | | { Text: string } 8 | | { True: null } 9 | | { False: null } 10 | | { Float: number } 11 | | { Principal: Principal }; 12 | export type operation_error = 13 | | { NotAuthorized: null } 14 | | { BadParameters: null } 15 | | { Unknown: string } 16 | | { NonExistentItem: null }; 17 | export type operation_response = 18 | | { Ok: [] | [string] } 19 | | { Err: operation_error }; 20 | export interface token { 21 | thumbnail: string; 22 | name: string; 23 | frontend: [] | [string]; 24 | description: string; 25 | principal_id: Principal; 26 | details: Array<[string, detail_value]>; 27 | } 28 | export default interface TokenRegistry { 29 | add: (arg_1: token) => Promise; 30 | get: (arg_0: Principal) => Promise<[] | [token]>; 31 | get_all: () => Promise>; 32 | name: () => Promise; 33 | remove: (arg_0: Principal) => Promise; 34 | set_controller: (arg_0: Principal) => Promise; 35 | } 36 | -------------------------------------------------------------------------------- /src/api/idl/dab-tokens.did.ts: -------------------------------------------------------------------------------- 1 | import { IDL } from '@dfinity/candid'; 2 | 3 | export const idlFactory: IDL.InterfaceFactory = ({ IDL }) => { 4 | const detail_value = IDL.Rec(); 5 | detail_value.fill( 6 | IDL.Variant({ 7 | I64: IDL.Int64, 8 | U64: IDL.Nat64, 9 | Vec: IDL.Vec(detail_value), 10 | Slice: IDL.Vec(IDL.Nat8), 11 | Text: IDL.Text, 12 | True: IDL.Null, 13 | False: IDL.Null, 14 | Float: IDL.Float64, 15 | Principal: IDL.Principal, 16 | }), 17 | ); 18 | const token = IDL.Record({ 19 | thumbnail: IDL.Text, 20 | name: IDL.Text, 21 | frontend: IDL.Opt(IDL.Text), 22 | description: IDL.Text, 23 | principal_id: IDL.Principal, 24 | details: IDL.Vec(IDL.Tuple(IDL.Text, detail_value)), 25 | }); 26 | const operation_error = IDL.Variant({ 27 | NotAuthorized: IDL.Null, 28 | BadParameters: IDL.Null, 29 | Unknown: IDL.Text, 30 | NonExistentItem: IDL.Null, 31 | }); 32 | const operation_response = IDL.Variant({ 33 | Ok: IDL.Opt(IDL.Text), 34 | Err: operation_error, 35 | }); 36 | return IDL.Service({ 37 | add: IDL.Func([token], [operation_response], []), 38 | get: IDL.Func([IDL.Principal], [IDL.Opt(token)], ['query']), 39 | get_all: IDL.Func([], [IDL.Vec(token)], ['query']), 40 | name: IDL.Func([], [IDL.Text], ['query']), 41 | remove: IDL.Func([IDL.Principal], [operation_response], []), 42 | set_controller: IDL.Func([IDL.Principal], [operation_response], []), 43 | }); 44 | }; 45 | -------------------------------------------------------------------------------- /src/api/readme.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/src/api/readme.md -------------------------------------------------------------------------------- /src/assets/128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/src/assets/128.png -------------------------------------------------------------------------------- /src/assets/16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/src/assets/16.png -------------------------------------------------------------------------------- /src/assets/48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/src/assets/48.png -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jorgenbuilder/ic-inspector/0b6ec9df37a31a247d6c87de1c26e6613099f44f/src/assets/logo.png -------------------------------------------------------------------------------- /src/declaration.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css'; 2 | -------------------------------------------------------------------------------- /src/entries/devtools/app.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/display-name */ 2 | /* eslint-disable @typescript-eslint/ban-ts-comment */ 3 | /* eslint-disable react/jsx-key */ 4 | import React from 'react'; 5 | import ReactDOM from 'react-dom'; 6 | import { captureInternetComputerMessageFromNetworkEvent } from '../../services/capture'; 7 | import { logstore, MessageId } from '../../services/logging'; 8 | import { useStore } from 'zustand'; 9 | import { Root } from '../../ui/root'; 10 | 11 | (window as any).global = window; 12 | 13 | function App() { 14 | const { messages, log, focusedMessage, clear, focus } = useStore(logstore); 15 | const [capturing, setCapturing] = React.useState(true); 16 | 17 | const captureRequest = React.useMemo(() => { 18 | return (request: chrome.devtools.network.Request) => { 19 | captureInternetComputerMessageFromNetworkEvent(request).then( 20 | (r) => r && log(r.request, r.response), 21 | ); 22 | }; 23 | }, []); 24 | 25 | React.useEffect(() => { 26 | if (capturing) { 27 | chrome.devtools.network.onRequestFinished.addListener( 28 | captureRequest, 29 | ); 30 | } else { 31 | chrome.devtools.network.onRequestFinished.removeListener( 32 | captureRequest, 33 | ); 34 | } 35 | }, [capturing]); 36 | 37 | return ( 38 | setCapturing(!capturing)} 43 | handleClear={clear} 44 | handleFocus={(m?: MessageId) => focus(m)} 45 | /> 46 | ); 47 | } 48 | 49 | ReactDOM.render( 50 | 51 | 52 | , 53 | document.getElementById('root'), 54 | ); 55 | -------------------------------------------------------------------------------- /src/entries/devtools/index.html: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/entries/devtools/styles.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --chrome-0: rgb(36 36 36); 3 | --chrome-1: rgb(41 41 41); 4 | --chrome-2: rgb(41 42 45); 5 | --chrome-3: rgb(75, 75, 75); 6 | --chrome-4: rgb(145 145 145); 7 | --chrome-5: rgb(154, 160, 166); 8 | --chrome-6: white; 9 | 10 | --syntax-null: #ff5874; 11 | --syntax-number: #f78c6c; 12 | --syntax-string: #ecc48d; 13 | --syntax-boolean: #ff5874; 14 | --syntax-other: #78ccf0; 15 | } 16 | 17 | @media (prefers-color-scheme: light) { 18 | :root { 19 | --chrome-0: rgb(255, 255, 255); 20 | --chrome-1: rgb(245, 245, 245); 21 | --chrome-2: rgb(241, 243, 244); 22 | --chrome-3: rgb(203, 205, 209); 23 | --chrome-4: rgb(110, 110, 110); 24 | /* --chrome-5: rgb(81, 85, 89); */ 25 | --chrome-6: black; 26 | 27 | --syntax-null: #bc5454; 28 | --syntax-number: #aa0982; 29 | --syntax-string: #4876d6; 30 | --syntax-boolean: #bc5454; 31 | --syntax-other: #78ccf0; 32 | } 33 | } 34 | 35 | a, 36 | a:link, 37 | a:visited { 38 | color: var(--chrome-6); 39 | } 40 | 41 | @media (prefers-color-scheme: dark) { 42 | body { 43 | background-color: var(--chrome-0); 44 | color: var(--chrome-5); 45 | } 46 | thead { 47 | background: var(--chrome-2); 48 | box-shadow: 0 1px 0 var(--chrome-3); 49 | } 50 | 51 | tfoot { 52 | background-color: var(--chrome-2); 53 | border-top: 1px solid var(--chrome-3); 54 | } 55 | 56 | td:not(:last-child), 57 | th:not(:last-child) { 58 | border-right: 1px solid var(--chrome-3); 59 | } 60 | 61 | tbody tr { 62 | background: var(--chrome-0); 63 | } 64 | 65 | tbody tr:nth-child(odd) { 66 | background: var(--chrome-1); 67 | } 68 | 69 | tbody td:first-child:hover { 70 | color: var(--chrome-5); 71 | } 72 | 73 | .controls { 74 | background: var(--chrome-2); 75 | box-shadow: 0 1px 0 var(--chrome-3); 76 | } 77 | 78 | .icon { 79 | background-color: var(--chrome-5); 80 | } 81 | 82 | .icon:hover { 83 | background-color: var(--chrome-5); 84 | } 85 | 86 | .filter { 87 | background: var(--chrome-1); 88 | color: var(--chrome-5); 89 | } 90 | 91 | .clear { 92 | --override-icon-mask-background-color: var(--chrome-4); 93 | } 94 | 95 | .close { 96 | --override-icon-mask-background-color: var(--chrome-4); 97 | } 98 | 99 | .side-by-side .table-container { 100 | border-right: 1px solid var(--chrome-3); 101 | } 102 | 103 | :root { 104 | color-scheme: dark; 105 | } 106 | } 107 | 108 | @media (prefers-color-scheme: light) { 109 | body { 110 | background-color: var(--chrome-0); 111 | color: black; 112 | } 113 | 114 | thead { 115 | background: var(--chrome-2); 116 | box-shadow: 0 1px 0 var(--chrome-3); 117 | } 118 | 119 | tfoot { 120 | background-color: var(--chrome-2); 121 | border-top: 1px solid var(--chrome-3); 122 | } 123 | 124 | td, 125 | th { 126 | box-shadow: inset 1px 0 0 var(--chrome-3); 127 | } 128 | 129 | tbody tr { 130 | background: var(--chrome-0); 131 | } 132 | 133 | tbody tr:nth-child(odd) { 134 | background: var(--chrome-1); 135 | } 136 | 137 | tbody td:first-child:hover { 138 | color: black; 139 | } 140 | 141 | .controls { 142 | background: var(--chrome-2); 143 | box-shadow: 0 1px 0 var(--chrome-3); 144 | } 145 | 146 | .icon { 147 | background-color: var(--chrome-4); 148 | --override-icon-mask-background-color: var(--chrome-4); 149 | } 150 | 151 | .icon:hover { 152 | background-color: black; 153 | } 154 | 155 | .filter { 156 | background: var(--chrome-0); 157 | color: var(--chrome-4); 158 | } 159 | 160 | .side-by-side .table-container { 161 | border-right: 1px solid var(--chrome-3); 162 | } 163 | } 164 | 165 | html, 166 | body { 167 | padding: 0; 168 | margin: 0; 169 | font-family: 'Segoe UI', Tahoma, sans-serif; 170 | } 171 | 172 | * { 173 | font-size: 12px; 174 | text-align: left; 175 | } 176 | 177 | .panel { 178 | position: relative; 179 | } 180 | 181 | table { 182 | position: relative; 183 | width: 100%; 184 | 185 | border-spacing: 0; 186 | border-collapse: collapse; 187 | } 188 | 189 | thead { 190 | position: sticky; 191 | top: 0px; 192 | } 193 | 194 | th { 195 | font-weight: 400; 196 | } 197 | 198 | tfoot { 199 | position: fixed; 200 | bottom: 0; 201 | width: 100%; 202 | } 203 | 204 | tbody tr { 205 | min-height: 25px; 206 | } 207 | 208 | tbody td:first-child:hover { 209 | text-decoration: underline; 210 | cursor: pointer; 211 | } 212 | 213 | td, 214 | th { 215 | padding: 5px; 216 | } 217 | 218 | .controls { 219 | position: sticky; 220 | top: 0; 221 | z-index: 100; 222 | 223 | display: flex; 224 | gap: 5px; 225 | align-items: center; 226 | width: 100%; 227 | } 228 | 229 | .filter { 230 | width: 300px; 231 | height: 18px; 232 | border: none; 233 | outline: none; 234 | } 235 | 236 | .clear { 237 | display: block; 238 | width: 28px; 239 | height: 24px; 240 | /* TODO: replace this */ 241 | -webkit-mask-image: url(/assets/icons.svg); 242 | -webkit-mask-position-x: 0px; 243 | -webkit-mask-position-y: 144px; 244 | } 245 | 246 | .record { 247 | margin: 0 0 0 5px; 248 | width: 14px; 249 | height: 14px; 250 | border-radius: 50%; 251 | } 252 | 253 | .close { 254 | display: block; 255 | width: 28px; 256 | height: 24px; 257 | /* TODO: replace this */ 258 | -webkit-mask-image: url(/assets/icons.svg); 259 | -webkit-mask-position-x: -84px; 260 | -webkit-mask-position-y: 0px; 261 | } 262 | 263 | .record.active { 264 | background-color: rgb(242, 139, 130); 265 | box-shadow: 0 0 5px rgb(242, 139, 130); 266 | } 267 | 268 | .p0 { 269 | padding: 0; 270 | } 271 | 272 | .table-container { 273 | height: calc(100vh - 50px); 274 | overflow-y: scroll; 275 | } 276 | 277 | .side-by-side { 278 | display: flex; 279 | } 280 | 281 | .side-by-side th:not(:first-child), 282 | .side-by-side td:not(:first-child) { 283 | display: none; 284 | } 285 | 286 | .side-by-side td, 287 | .side-by-side th { 288 | border-right: none; 289 | } 290 | 291 | .side-by-side .table-container { 292 | width: 30%; 293 | min-height: 100%; 294 | } 295 | 296 | .side-by-side .details-pane { 297 | display: block; 298 | height: calc(100vh - 50px); 299 | overflow-y: scroll; 300 | width: 70%; 301 | } 302 | -------------------------------------------------------------------------------- /src/entries/sandbox/index.html: -------------------------------------------------------------------------------- 1 |