├── .dockerignore ├── .editorconfig ├── .env ├── .github └── workflows │ └── check.yaml ├── .gitignore ├── .npmrc ├── .tool-versions ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── bun.lock ├── eslint.config.js ├── jsconfig.json ├── package.json ├── playwright.config.js ├── src ├── app.d.ts ├── app.html ├── hooks.server.js ├── lib │ ├── api │ │ └── index.js │ ├── components │ │ ├── Chart.svelte │ │ ├── DeploymentStatusIcon.svelte │ │ ├── ErrorRow.svelte │ │ ├── LoadingRow.svelte │ │ ├── NoDataRow.svelte │ │ ├── Secret.svelte │ │ └── StatusIcon.svelte │ ├── format │ │ └── index.js │ ├── hc │ │ └── index.js │ └── modal │ │ └── index.js ├── routes │ ├── (auth) │ │ ├── (project) │ │ │ ├── +layout.js │ │ │ ├── +layout.server.js │ │ │ ├── +layout.svelte │ │ │ ├── +page.js │ │ │ ├── +page.svelte │ │ │ ├── deployment │ │ │ │ ├── (detail) │ │ │ │ │ ├── +layout.js │ │ │ │ │ ├── +layout.svelte │ │ │ │ │ ├── detail │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── events │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── logs │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ ├── metrics │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ └── revision │ │ │ │ │ │ ├── +page.js │ │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── _components │ │ │ │ │ └── Header.svelte │ │ │ │ └── deploy │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── disk │ │ │ │ ├── (detail) │ │ │ │ │ ├── +layout.js │ │ │ │ │ ├── +layout.svelte │ │ │ │ │ ├── detail │ │ │ │ │ │ └── +page.svelte │ │ │ │ │ └── metrics │ │ │ │ │ │ └── +page.svelte │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── domain │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── cdn-downgrade │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── create │ │ │ │ │ └── +page.svelte │ │ │ │ └── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── dropbox │ │ │ │ ├── +layout.js │ │ │ │ └── +page.svelte │ │ │ ├── email │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── pull-secret │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── create │ │ │ │ │ └── +page.svelte │ │ │ │ └── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── registry │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ └── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── role │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── bind │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ ├── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ └── users │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ ├── route │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ └── create │ │ │ │ │ └── +page.svelte │ │ │ ├── service-account │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── create │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ │ └── detail │ │ │ │ │ ├── +page.js │ │ │ │ │ └── +page.svelte │ │ │ └── workload-identity │ │ │ │ ├── +layout.js │ │ │ │ ├── +page.js │ │ │ │ ├── +page.svelte │ │ │ │ ├── create │ │ │ │ └── +page.svelte │ │ │ │ └── detail │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ ├── +layout.js │ │ ├── +layout.svelte │ │ ├── ModalSelectProject.svelte │ │ ├── Navbar.svelte │ │ ├── Sidebar.svelte │ │ ├── billing │ │ │ ├── +page.js │ │ │ ├── +page.svelte │ │ │ ├── create │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ ├── detail │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ │ └── report │ │ │ │ ├── +page.js │ │ │ │ └── +page.svelte │ │ └── project │ │ │ ├── +page.js │ │ │ ├── +page.svelte │ │ │ └── create │ │ │ ├── +page.js │ │ │ └── +page.svelte │ ├── +layout.js │ ├── +layout.svelte │ ├── api │ │ ├── [fn] │ │ │ └── +server.js │ │ ├── dropbox │ │ │ └── +server.js │ │ └── registry │ │ │ └── [fn] │ │ │ └── +server.js │ └── auth │ │ ├── callback │ │ └── +server.js │ │ ├── signin │ │ └── +server.js │ │ └── signout │ │ └── +server.js ├── style │ ├── _theme.scss │ └── main.scss └── types │ └── api.d.ts ├── static ├── favicon.png ├── favicon.webp └── images │ ├── logo.png │ └── logo.webp ├── svelte.config.js ├── vite.config.js └── wrangler.toml /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .svelte-kit/ 3 | build/ 4 | .idea/ -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.svelte] 4 | indent_style = tab 5 | 6 | [*.{js,cjs}] 7 | indent_style = tab 8 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | API_ENDPOINT=https://api.deploys.app 2 | OAUTH2_CLIENT_ID=localhost 3 | OAUTH2_CLIENT_SECRET=localhost 4 | PUBLIC_API_ENDPOINT=https://api.deploys.app 5 | PUBLIC_SENTRY_DSN= 6 | -------------------------------------------------------------------------------- /.github/workflows/check.yaml: -------------------------------------------------------------------------------- 1 | name: Check 2 | on: 3 | push: 4 | pull_request: 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | name: Lint 9 | steps: 10 | - uses: actions/checkout@v3 11 | - uses: asdf-vm/actions/install@v2 12 | - run: bun install --frozen-lockfile 13 | - run: bun run lint 14 | - run: bun run check 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /build 4 | /.svelte-kit 5 | /package 6 | .env.* 7 | !.env.example 8 | .pnp.* 9 | .yarn/* 10 | !.yarn/patches 11 | !.yarn/plugins 12 | !.yarn/releases 13 | !.yarn/sdks 14 | !.yarn/versions 15 | .idea 16 | .vscode 17 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true 2 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | bun 1.2.15 2 | nodejs 22.15.1 3 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM registry.moonrhythm.io/builder 2 | 3 | WORKDIR /workspace 4 | ADD .tool-versions ./ 5 | RUN asdf install 6 | 7 | ENV ADAPTER=node 8 | 9 | ADD package.json bun.lock ./ 10 | ADD svelte.config.js ./ 11 | RUN bun install --frozen-lockfile 12 | ADD . . 13 | RUN bun -b run build 14 | #RUN sed -i'' -e "s/import http from 'http'/import http from 'http2'/g" build/index.js 15 | 16 | FROM gcr.io/distroless/nodejs22-debian11 17 | 18 | ENV NODE_ENV=production 19 | ENV BODY_SIZE_LIMIT=Infinity 20 | ENV ADDRESS_HEADER=X-Real-Ip 21 | 22 | WORKDIR /app 23 | ADD package.json ./ 24 | COPY --from=0 /workspace/build . 25 | CMD ["index.js"] 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 deploys-app 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | REGISTRY=registry.deploys.app/deploys-app/console 2 | TAG=$(shell git rev-parse HEAD) 3 | 4 | .PHONY: build 5 | build: 6 | buildctl build \ 7 | --frontend dockerfile.v0 \ 8 | --local dockerfile=. \ 9 | --local context=. \ 10 | --output type=image,name=$(REGISTRY):$(TAG),push=true 11 | 12 | deploy: build 13 | deploys deployment set image console -project=deploys-app -image=$(REGISTRY):$(TAG) 14 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Console 2 | 3 | Web console for manage resources on deploys.app. 4 | 5 | ## Developing 6 | 7 | Install dependencies with `bun install`. 8 | 9 | Start development server: 10 | 11 | ```bash 12 | bun -b run dev 13 | 14 | # or start the server and open the app in a new browser tab 15 | bun -b run dev --open 16 | ``` 17 | 18 | ## Building 19 | 20 | To create a production version: 21 | 22 | ```bash 23 | bun -b run build 24 | ``` 25 | 26 | ## License 27 | 28 | MIT 29 | 30 | This project uses font-awesome pro, you need font-awesome pro license to editing source code. 31 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import js from '@eslint/js' 2 | import svelte from 'eslint-plugin-svelte' 3 | import globals from 'globals' 4 | 5 | /** @type {import('eslint').Linter.FlatConfig[]} */ 6 | export default [ 7 | js.configs.recommended, 8 | ...svelte.configs['flat/recommended'], 9 | { 10 | languageOptions: { 11 | globals: { 12 | ...globals.browser, 13 | ...globals.node 14 | } 15 | } 16 | }, 17 | { 18 | ignores: ['build/', '.svelte-kit/', 'package/'] 19 | }, 20 | { 21 | rules: { 22 | indent: ['error', 'tab'], 23 | 'no-var': 'warn', 24 | 'object-shorthand': ['warn', 'properties'], 25 | 26 | 'accessor-pairs': ['error', { setWithoutGet: true, enforceForClassMembers: true }], 27 | 'array-bracket-spacing': ['error', 'never'], 28 | 'array-callback-return': ['error', { 29 | allowImplicit: false, 30 | checkForEach: false 31 | }], 32 | 'arrow-spacing': ['error', { before: true, after: true }], 33 | 'block-spacing': ['error', 'always'], 34 | 'brace-style': ['error', '1tbs', { allowSingleLine: true }], 35 | camelcase: ['error', { 36 | allow: ['^UNSAFE_'], 37 | properties: 'never', 38 | ignoreGlobals: true, 39 | ignoreImports: true 40 | }], 41 | 'comma-dangle': ['error', { 42 | arrays: 'never', 43 | objects: 'never', 44 | imports: 'never', 45 | exports: 'never', 46 | functions: 'never' 47 | }], 48 | 'comma-spacing': ['error', { before: false, after: true }], 49 | 'comma-style': ['error', 'last'], 50 | 'computed-property-spacing': ['error', 'never', { enforceForClassMembers: true }], 51 | 'constructor-super': 'error', 52 | curly: ['error', 'multi-line'], 53 | 'default-case-last': 'error', 54 | 'dot-location': ['error', 'property'], 55 | 'dot-notation': ['error', { allowKeywords: true }], 56 | 'eol-last': 'error', 57 | eqeqeq: ['error', 'always', { null: 'ignore' }], 58 | 'func-call-spacing': ['error', 'never'], 59 | 'generator-star-spacing': ['error', { before: true, after: true }], 60 | 'key-spacing': ['error', { beforeColon: false, afterColon: true }], 61 | 'keyword-spacing': ['error', { before: true, after: true }], 62 | 'lines-between-class-members': ['error', 'always', { exceptAfterSingleLine: true }], 63 | 'multiline-ternary': ['error', 'always-multiline'], 64 | 'new-cap': ['error', { newIsCap: true, capIsNew: false, properties: true }], 65 | 'new-parens': 'error', 66 | 'no-array-constructor': 'error', 67 | 'no-async-promise-executor': 'error', 68 | 'no-caller': 'error', 69 | 'no-case-declarations': 'error', 70 | 'no-class-assign': 'error', 71 | 'no-compare-neg-zero': 'error', 72 | 'no-cond-assign': 'error', 73 | 'no-const-assign': 'error', 74 | 'no-constant-condition': ['error', { checkLoops: false }], 75 | 'no-control-regex': 'error', 76 | 'no-debugger': 'error', 77 | 'no-delete-var': 'error', 78 | 'no-dupe-args': 'error', 79 | 'no-dupe-class-members': 'error', 80 | 'no-dupe-keys': 'error', 81 | 'no-duplicate-case': 'error', 82 | 'no-useless-backreference': 'error', 83 | 'no-empty': ['error', { allowEmptyCatch: true }], 84 | 'no-empty-character-class': 'error', 85 | 'no-empty-pattern': 'error', 86 | 'no-eval': 'error', 87 | 'no-ex-assign': 'error', 88 | 'no-extend-native': 'error', 89 | 'no-extra-bind': 'error', 90 | 'no-extra-boolean-cast': 'error', 91 | 'no-extra-parens': ['error', 'functions'], 92 | 'no-fallthrough': 'error', 93 | 'no-floating-decimal': 'error', 94 | 'no-func-assign': 'error', 95 | 'no-global-assign': 'error', 96 | 'no-implied-eval': 'error', 97 | 'no-import-assign': 'error', 98 | 'no-invalid-regexp': 'error', 99 | 'no-irregular-whitespace': 'error', 100 | 'no-iterator': 'error', 101 | 'no-labels': ['error', { allowLoop: false, allowSwitch: false }], 102 | 'no-lone-blocks': 'error', 103 | 'no-loss-of-precision': 'error', 104 | 'no-misleading-character-class': 'error', 105 | 'no-prototype-builtins': 'error', 106 | 'no-useless-catch': 'error', 107 | 'no-mixed-operators': ['error', { 108 | groups: [ 109 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 110 | ['&&', '||'], 111 | ['in', 'instanceof'] 112 | ], 113 | allowSamePrecedence: true 114 | }], 115 | 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], 116 | 'no-multi-spaces': 'error', 117 | 'no-multi-str': 'error', 118 | 'no-new': 'error', 119 | 'no-new-func': 'error', 120 | 'no-new-object': 'error', 121 | 'no-new-symbol': 'error', 122 | 'no-new-wrappers': 'error', 123 | 'no-obj-calls': 'error', 124 | 'no-octal': 'error', 125 | 'no-octal-escape': 'error', 126 | 'no-proto': 'error', 127 | 'no-redeclare': ['error', { builtinGlobals: false }], 128 | 'no-regex-spaces': 'error', 129 | 'no-self-assign': ['error', { props: true }], 130 | 'no-self-compare': 'error', 131 | 'no-shadow-restricted-names': 'error', 132 | 'no-sparse-arrays': 'error', 133 | 'no-template-curly-in-string': 'error', 134 | 'no-this-before-super': 'error', 135 | 'no-throw-literal': 'error', 136 | 'no-trailing-spaces': 'error', 137 | 'no-undef': 'error', 138 | 'no-undef-init': 'error', 139 | 'no-unexpected-multiline': 'error', 140 | 'no-unmodified-loop-condition': 'error', 141 | 'no-unneeded-ternary': ['error', { defaultAssignment: false }], 142 | 'no-unreachable': 'error', 143 | 'no-unreachable-loop': 'error', 144 | 'no-unsafe-finally': 'error', 145 | 'no-unsafe-negation': 'error', 146 | 'no-unused-vars': ['error', { 147 | args: 'none', 148 | caughtErrors: 'none', 149 | ignoreRestSiblings: true, 150 | vars: 'all' 151 | }], 152 | 'no-use-before-define': ['error', { functions: false, classes: false, variables: false }], 153 | 'no-useless-call': 'error', 154 | 'no-useless-computed-key': 'error', 155 | 'no-useless-constructor': 'error', 156 | 'no-useless-escape': 'error', 157 | 'no-useless-rename': 'error', 158 | 'no-useless-return': 'error', 159 | 'no-void': 'error', 160 | 'no-whitespace-before-property': 'error', 161 | 'no-with': 'error', 162 | 'object-curly-newline': ['error', { multiline: true, consistent: true }], 163 | 'object-curly-spacing': ['error', 'always'], 164 | 'object-property-newline': ['error', { allowMultiplePropertiesPerLine: true }], 165 | 'one-var': ['error', { initialized: 'never' }], 166 | 'operator-linebreak': ['error', 'after', { overrides: { '?': 'before', ':': 'before', '|>': 'before' } }], 167 | 'padded-blocks': ['error', { blocks: 'never', switches: 'never', classes: 'never' }], 168 | 'prefer-const': ['error', { destructuring: 'all' }], 169 | 'prefer-promise-reject-errors': 'error', 170 | 'prefer-regex-literals': ['error', { disallowRedundantWrapping: true }], 171 | 'quote-props': ['error', 'as-needed'], 172 | quotes: ['error', 'single', { avoidEscape: true, allowTemplateLiterals: false }], 173 | 'rest-spread-spacing': ['error', 'never'], 174 | semi: ['error', 'never'], 175 | 'semi-spacing': ['error', { before: false, after: true }], 176 | 'space-before-blocks': ['error', 'always'], 177 | 'space-before-function-paren': ['error', 'always'], 178 | 'space-in-parens': ['error', 'never'], 179 | 'space-infix-ops': 'error', 180 | 'space-unary-ops': ['error', { words: true, nonwords: false }], 181 | 'spaced-comment': ['error', 'always', { 182 | line: { markers: ['*package', '!', '/', ',', '='] }, 183 | block: { 184 | balanced: true, 185 | markers: ['*package', '!', ',', ':', '::', 'flow-include'], 186 | exceptions: ['*'] 187 | } 188 | }], 189 | 'symbol-description': 'error', 190 | 'template-curly-spacing': ['error', 'never'], 191 | 'template-tag-spacing': ['error', 'never'], 192 | 'unicode-bom': ['error', 'never'], 193 | 'use-isnan': ['error', { 194 | enforceForSwitchCase: true, 195 | enforceForIndexOf: true 196 | }], 197 | 'valid-typeof': ['error', { requireStringLiterals: true }], 198 | 'wrap-iife': ['error', 'any', { functionPrototypeMethods: true }], 199 | 'yield-star-spacing': ['error', 'both'], 200 | yoda: ['error', 'never'] 201 | } 202 | } 203 | ] 204 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.svelte-kit/tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true, 5 | "checkJs": true, 6 | "strict": true, 7 | "noImplicitAny": false 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "console", 3 | "version": "0.0.1", 4 | "scripts": { 5 | "dev": "vite dev", 6 | "build": "vite build", 7 | "preview": "vite preview", 8 | "prepare": "svelte-kit sync", 9 | "check": "svelte-check --tsconfig ./jsconfig.json", 10 | "check:watch": "svelte-check --tsconfig ./jsconfig.json --watch", 11 | "test": "playwright test", 12 | "lint": "eslint ." 13 | }, 14 | "devDependencies": { 15 | "@nomimono/nomimono-css": "^0.6.0", 16 | "@sveltejs/adapter-cloudflare": "^5.0.3", 17 | "@sveltejs/adapter-node": "^5.2.12", 18 | "@sveltejs/kit": "=2.17.2", 19 | "@typescript-eslint/eslint-plugin": "^8.24.1", 20 | "@typescript-eslint/parser": "^8.24.1", 21 | "clipboard": "^2.0.11", 22 | "dayjs": "^1.11.13", 23 | "eslint": "^9.20.1", 24 | "eslint-plugin-svelte": "^2.46.1", 25 | "global": "^4.4.0", 26 | "gravatar-url": "^4.0.1", 27 | "highcharts": "=11.4.8", 28 | "js-cookie": "^3.0.5", 29 | "sass": "^1.84.0", 30 | "svelte": "^5.20.2", 31 | "svelte-check": "^4.1.4", 32 | "svelte-eslint-parser": "^0.43.0", 33 | "svelte-preprocess": "^6.0.3", 34 | "sweetalert2": "^11.17.2", 35 | "typescript": "~5.7.3", 36 | "valid-url": "^1.0.9" 37 | }, 38 | "type": "module" 39 | } 40 | -------------------------------------------------------------------------------- /playwright.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@playwright/test').PlaywrightTestConfig} */ 2 | const config = { 3 | webServer: { 4 | command: 'npm run build && npm run preview', 5 | port: 3000 6 | } 7 | } 8 | 9 | export default config 10 | -------------------------------------------------------------------------------- /src/app.d.ts: -------------------------------------------------------------------------------- 1 | // See https://kit.svelte.dev/docs/types#app 2 | // for information about these interfaces 3 | declare global { 4 | namespace App { 5 | interface Locals { 6 | token: string 7 | project: string 8 | } 9 | } 10 | } 11 | 12 | export {} 13 | -------------------------------------------------------------------------------- /src/app.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Console | Deploys.app 6 | 7 | 8 | 9 | %sveltekit.head% 10 | 11 | 12 | 13 |
%sveltekit.body%
14 | 15 | 16 | -------------------------------------------------------------------------------- /src/hooks.server.js: -------------------------------------------------------------------------------- 1 | import { sequence } from '@sveltejs/kit/hooks' 2 | 3 | const allowTheme = { 4 | dark: true, 5 | light: true 6 | } 7 | 8 | /** @type {import('@sveltejs/kit').Handle} */ 9 | async function theme ({ event, resolve }) { 10 | let t = event.cookies.get('theme') 11 | if (!allowTheme[t]) { 12 | t = 'dark' 13 | } 14 | return resolve(event, { 15 | transformPageChunk: ({ html }) => html.replace('data-theme=""', `data-theme="${t}"`) 16 | }) 17 | } 18 | 19 | /** @type {import('@sveltejs/kit').Handle} */ 20 | async function handleCookie ({ event, resolve }) { 21 | const { cookies, locals } = event 22 | 23 | locals.token = cookies.get('token') || '' 24 | locals.project = cookies.get('project') || '' 25 | 26 | return resolve(event) 27 | } 28 | 29 | /** @type {import('@sveltejs/kit').Handle} */ 30 | function storeProject ({ event, resolve }) { 31 | const { url, locals } = event 32 | 33 | if (url.pathname.startsWith('/api/')) { 34 | return resolve(event) 35 | } 36 | 37 | const project = url.searchParams.get('project') 38 | if (project && project !== locals.project) { 39 | locals.project = project 40 | } 41 | 42 | return resolve(event) 43 | } 44 | 45 | export const handle = sequence( 46 | theme, 47 | handleCookie, 48 | storeProject 49 | ) 50 | -------------------------------------------------------------------------------- /src/lib/api/index.js: -------------------------------------------------------------------------------- 1 | import { invalidate } from '$app/navigation' 2 | 3 | const endpoint = '/api' 4 | 5 | /** @type {Function} */ 6 | let onUnauth 7 | 8 | /** 9 | * @template T 10 | * @param {string} fn 11 | * @param {Object} args 12 | * @param {fetch} fetch 13 | * @returns {Promise>} 14 | */ 15 | async function invoke (fn, args, fetch) { 16 | const resp = await fetch(`${endpoint}/${fn}`, { 17 | method: 'POST', 18 | body: JSON.stringify(args || {}), 19 | headers: { 20 | 'content-type': 'application/json' 21 | } 22 | }) 23 | 24 | const body = await resp.json() 25 | if (!body.ok) { 26 | const msg = body.error?.message || '' 27 | switch (msg) { 28 | case 'api: unauthorized': 29 | body.error.unauth = true 30 | onUnauth?.() 31 | break 32 | case 'api: forbidden': 33 | body.error.forbidden = true 34 | break 35 | case 'iam: forbidden': 36 | body.error.forbidden = true 37 | break 38 | case 'api: validate error': 39 | body.error.validate = body.error.items 40 | break 41 | default: 42 | if (msg.includes('api: ') && msg.includes('not found')) { 43 | body.error.notFound = true 44 | } 45 | break 46 | } 47 | } 48 | return body 49 | } 50 | 51 | /** 52 | * @param {Function} callback 53 | */ 54 | function setOnUnauth (callback) { 55 | onUnauth = callback 56 | } 57 | 58 | /** 59 | * @param {string} fn 60 | * @returns {Promise} 61 | */ 62 | function wrapInvalidate (fn) { 63 | return invalidate(`${endpoint}/${fn}`) 64 | } 65 | 66 | /** 67 | * intervalInvalidate calls callback every interval milliseconds 68 | * must be called in onMount 69 | * callback can return a number to override the interval for only the next call 70 | * @param {() => Promise} callback 71 | * @param {number} interval 72 | * @returns {() => void} 73 | */ 74 | function intervalInvalidate (callback, interval) { 75 | let p 76 | 77 | const f = async () => { 78 | let newInterval = await callback() 79 | if (!newInterval) { 80 | newInterval = interval 81 | } 82 | p = setTimeout(f, newInterval) 83 | } 84 | p = setTimeout(f, interval) 85 | 86 | return () => { 87 | clearTimeout(p) 88 | } 89 | } 90 | 91 | export default { 92 | invoke, 93 | setOnUnauth, 94 | invalidate: wrapInvalidate, 95 | intervalInvalidate 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/components/Chart.svelte: -------------------------------------------------------------------------------- 1 | 117 | 118 |
119 | -------------------------------------------------------------------------------- /src/lib/components/DeploymentStatusIcon.svelte: -------------------------------------------------------------------------------- 1 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/lib/components/ErrorRow.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if error} 13 | 14 | 15 | {#if error.forbidden} 16 | You don't have permission to view data 17 | {:else if error.message} 18 | {error.message} 19 | {:else} 20 | {error} 21 | {/if} 22 | 23 | 24 | {/if} 25 | -------------------------------------------------------------------------------- /src/lib/components/LoadingRow.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | Loading... 13 | 14 | -------------------------------------------------------------------------------- /src/lib/components/NoDataRow.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 | {#if !list?.length} 13 | 14 | 15 | No data 16 | 17 | 18 | {/if} 19 | -------------------------------------------------------------------------------- /src/lib/components/Secret.svelte: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 |
20 | {#if isHidden} 21 | 22 | {:else} 23 | 24 | {/if} 25 |
26 | {#if isHidden} 27 | ••••••••••••••• 28 | {:else} 29 | {value} 30 | {/if} 31 |
32 | 33 | 43 | -------------------------------------------------------------------------------- /src/lib/components/StatusIcon.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/lib/format/index.js: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | /** 4 | * @param {string} v 5 | * @returns {string} 6 | */ 7 | export function cpu (v) { 8 | if (v === '0') { 9 | return 'Shared' 10 | } 11 | return `${v} vCPU` 12 | } 13 | 14 | /** 15 | * @param {string} v 16 | * @returns {string} 17 | */ 18 | export function cpuLimited (v) { 19 | if (v === '0' || !v) { 20 | return 'Cluster Default' 21 | } 22 | return `${v} vCPU` 23 | } 24 | 25 | /** 26 | * @param {string} v 27 | * @returns {string} 28 | */ 29 | export function memory (v) { 30 | if (v === '0') { 31 | return 'Shared' 32 | } 33 | const m = v.match(/^(\d+)(\w+)$/) ?? [] 34 | if (m.length !== 3) { 35 | return v 36 | } 37 | return `${m[1]} ${m[2]}B` 38 | } 39 | 40 | /** 41 | * @param {number} v 42 | * @returns {string} 43 | */ 44 | export function storage (v) { 45 | return (v / 1024 / 1024 / 1024).toLocaleString(undefined, { maximumFractionDigits: 2 }) + ' GiB' 46 | } 47 | 48 | /** 49 | * @param {string} v 50 | * @returns {string} 51 | */ 52 | export function datetime (v) { 53 | if (!v) { 54 | return '' 55 | } 56 | return dayjs(v).format('YYYY-MM-DD HH:mm:ss') 57 | } 58 | 59 | /** 60 | * @param {string} project 61 | * @param {string} name 62 | * @param {string} gsa 63 | * @param {string} locationProject 64 | * @returns {string} 65 | */ 66 | export function gsaBinding (project, name, gsa, locationProject) { 67 | const namespace = 'deploys' 68 | const matched = gsa.match(/^.*@([^.]+)\.iam.gserviceaccount.com$/) ?? [] 69 | const googleProject = matched.length > 1 ? `\n --project ${matched[1]} \\` : '' 70 | return `gcloud iam service-accounts add-iam-policy-binding \\ 71 | --role roles/iam.workloadIdentityUser \\ 72 | --member "serviceAccount:${locationProject}.svc.id.goog[${namespace}/${name}-${project}]" \\${googleProject} 73 | ${gsa}` 74 | } 75 | 76 | /** 77 | * @param {Api.DeploymentType} t 78 | * @returns {string} 79 | */ 80 | export function deploymentType (t) { 81 | return { 82 | WebService: 'Web Service', 83 | TCPService: 'TCP Service', 84 | InternalTCPService: 'Internal TCP Service', 85 | Worker: 'Worker', 86 | CronJob: 'Cron Job' 87 | }[t] || t 88 | } 89 | 90 | /** 91 | * @param {string} s 92 | * @returns {string} 93 | */ 94 | export function shortDigest (s) { 95 | return s.replace(/^sha256:/, '').slice(0, 12) 96 | } 97 | -------------------------------------------------------------------------------- /src/lib/hc/index.js: -------------------------------------------------------------------------------- 1 | import Highcharts from 'highcharts' 2 | 3 | let inited = false 4 | 5 | export function init () { 6 | if (inited) { 7 | return 8 | } 9 | inited = true 10 | 11 | Highcharts.setOptions({ 12 | accessibility: { 13 | enabled: false 14 | }, 15 | colors: ['#2b908f', '#90ee7e', '#f45b5b', '#7798bf', '#aaeeee', '#ff0066', 16 | '#eeaaee', '#55bf3b', '#df5353', '#7798bf', '#aaeeee'], 17 | chart: { 18 | backgroundColor: 'hsl(var(--hsl-base-200))', 19 | plotBorderColor: '#606063' 20 | }, 21 | title: { 22 | style: { 23 | color: 'hsl(var(--hsl-content))', 24 | fontSize: '20px' 25 | } 26 | }, 27 | subtitle: { 28 | style: { 29 | color: 'hsl(var(--hsl-content))', 30 | textTransform: 'uppercase' 31 | } 32 | }, 33 | xAxis: { 34 | gridLineColor: '#707073', 35 | labels: { 36 | style: { 37 | color: 'hsl(var(--hsl-content))' 38 | } 39 | }, 40 | lineColor: '#707073', 41 | minorGridLineColor: '#505053', 42 | tickColor: '#707073', 43 | title: { 44 | style: { 45 | color: '#a0a0a3' 46 | } 47 | }, 48 | gridLineWidth: 1 49 | }, 50 | yAxis: { 51 | gridLineColor: '#707073', 52 | labels: { 53 | style: { 54 | color: 'hsl(var(--hsl-content))' 55 | } 56 | }, 57 | lineColor: '#707073', 58 | minorGridLineColor: '#505053', 59 | tickColor: '#707073', 60 | tickWidth: 1, 61 | title: { 62 | text: '', 63 | style: { 64 | color: '#a0a0a3' 65 | } 66 | } 67 | }, 68 | tooltip: { 69 | backgroundColor: 'rgba(0, 0, 0, 0.85)', 70 | style: { 71 | color: '#f0f0f0' 72 | } 73 | }, 74 | plotOptions: { 75 | series: { 76 | dataLabels: { 77 | color: '#f0f0f3', 78 | style: { 79 | fontSize: '13px' 80 | } 81 | }, 82 | marker: { 83 | lineColor: '#333' 84 | } 85 | }, 86 | boxplot: { 87 | fillColor: '#505053' 88 | }, 89 | candlestick: { 90 | lineColor: 'white' 91 | }, 92 | errorbar: { 93 | color: 'white' 94 | } 95 | }, 96 | legend: { 97 | itemStyle: { 98 | color: 'hsl(var(--hsl-content))' 99 | }, 100 | itemHoverStyle: { 101 | color: 'hsl(var(--hsl-content)/0.8)' 102 | }, 103 | itemHiddenStyle: { 104 | color: 'hsl(var(--hsl-content)/0.4)' 105 | }, 106 | title: { 107 | style: { 108 | color: '#c0c0c0' 109 | } 110 | } 111 | }, 112 | drilldown: { 113 | activeAxisLabelStyle: { 114 | color: '#f0f0f3' 115 | }, 116 | activeDataLabelStyle: { 117 | color: '#f0f0f3' 118 | } 119 | }, 120 | navigation: { 121 | buttonOptions: { 122 | symbolStroke: '#dddddd', 123 | theme: { 124 | fill: '#505053' 125 | } 126 | } 127 | }, 128 | // scroll charts 129 | rangeSelector: { 130 | buttonTheme: { 131 | fill: '#505053', 132 | stroke: '#000000', 133 | style: { 134 | color: '#ccc' 135 | }, 136 | states: { 137 | hover: { 138 | fill: '#707073', 139 | stroke: '#000000', 140 | style: { 141 | color: 'white' 142 | } 143 | }, 144 | select: { 145 | fill: '#000003', 146 | stroke: '#000000', 147 | style: { 148 | color: 'white' 149 | } 150 | } 151 | } 152 | }, 153 | inputBoxBorderColor: '#505053', 154 | inputStyle: { 155 | backgroundColor: '#333', 156 | color: 'silver' 157 | }, 158 | labelStyle: { 159 | color: 'silver' 160 | } 161 | }, 162 | navigator: { 163 | handles: { 164 | backgroundColor: '#666', 165 | borderColor: '#aaa' 166 | }, 167 | outlineColor: '#ccc', 168 | maskFill: 'rgba(255,255,255,0.1)', 169 | series: { 170 | color: '#7798bf', 171 | lineColor: '#a6c7ed' 172 | }, 173 | xAxis: { 174 | gridLineColor: '#505053' 175 | } 176 | }, 177 | scrollbar: { 178 | barBackgroundColor: '#808083', 179 | barBorderColor: '#808083', 180 | buttonArrowColor: '#ccc', 181 | buttonBackgroundColor: '#606063', 182 | buttonBorderColor: '#606063', 183 | rifleColor: '#fff', 184 | trackBackgroundColor: '#404043', 185 | trackBorderColor: '#404043' 186 | }, 187 | time: { 188 | useUTC: false 189 | }, 190 | credits: { 191 | enabled: false 192 | } 193 | }) 194 | } 195 | -------------------------------------------------------------------------------- /src/lib/modal/index.js: -------------------------------------------------------------------------------- 1 | import Swal from 'sweetalert2' 2 | import 'sweetalert2/dist/sweetalert2.min.css' 3 | 4 | /** 5 | * @typedef {Object} ModalConfirmOptions 6 | * @property {string} [title] 7 | * @property {string} [html] 8 | * @property {string} [yes] 9 | * @property {function?} [callback] 10 | */ 11 | 12 | /** 13 | * @param {ModalConfirmOptions} options 14 | * @returns {Promise} 15 | */ 16 | export async function confirm ({ title, html, yes, callback }) { 17 | const result = await Swal.fire({ 18 | title: 'Are you sure ?', 19 | text: title, 20 | html, 21 | icon: 'warning', 22 | showCancelButton: true, 23 | buttonsStyling: false, 24 | background: 'var(--modal-panel-background)', 25 | color: 'var(--modal-panel-color)', 26 | confirmButtonText: yes || 'Yes', 27 | customClass: { 28 | confirmButton: 'nm-button is-variant-negative _mgr-6', 29 | cancelButton: 'nm-button is-variant-tertiary', 30 | actions: '_mgt-7' 31 | } 32 | }) 33 | if (!result.value) { 34 | return false 35 | } 36 | 37 | callback?.() 38 | return true 39 | } 40 | 41 | /** 42 | * @typedef {Object} ModalErrorOptions 43 | * @property {string | Api.Error | unknown} [error] 44 | * @property {Function} [callback] 45 | */ 46 | 47 | /** 48 | * @param {ModalErrorOptions} options 49 | * @returns {Promise} 50 | */ 51 | export async function error ({ error, callback }) { 52 | if (!error) { 53 | callback?.() 54 | return 55 | } 56 | 57 | let msg = '' 58 | if (typeof error === 'string') { 59 | msg = error 60 | } else if (error instanceof Error) { 61 | msg = error.message 62 | } else if (typeof error === 'object') { 63 | if ('message' in error && typeof error.message === 'string') { 64 | msg = error.message 65 | } 66 | if ('validate' in error && Array.isArray(error.validate)) { 67 | msg = error.validate.join('
') 68 | } 69 | } 70 | 71 | await Swal.fire({ 72 | title: 'Oops...', 73 | html: msg, 74 | icon: 'error', 75 | background: 'var(--modal-panel-background)', 76 | color: 'var(--modal-panel-color)', 77 | customClass: { 78 | actions: '_mgt-7', 79 | confirmButton: 'nm-button is-variant-negative' 80 | } 81 | }) 82 | callback?.() 83 | } 84 | 85 | /** 86 | * @typedef {Object} ModalSuccessOptions 87 | * @property {string} [content] 88 | */ 89 | 90 | /** 91 | * @param {ModalSuccessOptions} options 92 | * @returns {Promise} 93 | */ 94 | export async function success ({ content }) { 95 | await Swal.fire({ 96 | title: 'Success', 97 | text: content, 98 | icon: 'success', 99 | background: 'var(--modal-panel-background)', 100 | color: 'var(--modal-panel-color)', 101 | customClass: { 102 | actions: '_mgt-7', 103 | confirmButton: 'nm-button is-variant-negative' 104 | } 105 | }) 106 | } 107 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+layout.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | import { browser } from '$app/environment' 4 | 5 | /** 6 | * @typedef {Object} BrowserCache 7 | * @property {string} project 8 | * @property {Api.Project} projectInfo 9 | * @property {Api.Location[]} locations 10 | */ 11 | 12 | /** @type {BrowserCache | null} */ 13 | let browserCache = null 14 | 15 | export async function load ({ url, data, fetch }) { 16 | const project = url.searchParams.get('project') 17 | const { restoreProject } = data || {} 18 | 19 | if (!project && restoreProject) { 20 | const q = new URLSearchParams(url.search) 21 | q.set('project', restoreProject) 22 | redirect(302, `?${q.toString()}`) 23 | } 24 | if (!project) redirect(302, '/project') 25 | 26 | if (browser && browserCache?.project === project) { 27 | return { 28 | project, 29 | projectInfo: browserCache.projectInfo, 30 | locations: browserCache.locations 31 | } 32 | } 33 | 34 | /** @type {Api.Response} */ 35 | const projectInfo = await api.invoke('project.get', { project }, fetch) 36 | if (!projectInfo.ok) { 37 | // not allow to access if user don't have permission 'project.get' 38 | if (projectInfo.error?.forbidden || projectInfo.error?.notFound) { 39 | redirect(302, '/project') 40 | } 41 | error(500, `project: ${projectInfo.error?.message}`) 42 | } 43 | if (!projectInfo.result) redirect(302, '/project') 44 | 45 | /** @type {Api.Response>} */ 46 | const locations = await api.invoke('location.list', { project }, fetch) 47 | if (!locations.ok) error(500, `locations: ${locations.error?.message}`) 48 | if (!locations.result) redirect(302, '/project') 49 | 50 | if (browser) { 51 | browserCache = { 52 | project, 53 | projectInfo: projectInfo.result, 54 | locations: locations.result.items ?? [] 55 | } 56 | } 57 | 58 | return { 59 | project, 60 | projectInfo: projectInfo.result, 61 | locations: locations.result.items ?? [] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+layout.server.js: -------------------------------------------------------------------------------- 1 | export function load ({ locals }) { 2 | return { 3 | restoreProject: locals.project || '' 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 | {@render children?.()} 15 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | const [ 7 | usage, 8 | price 9 | ] = await Promise.all([ 10 | api.invoke('project.usage', { project }, fetch), 11 | api.invoke('billing.project', { project }, fetch) 12 | ]) 13 | return { 14 | menu: 'dashboard', 15 | usage: usage.result ?? {}, 16 | price: price.result ?? {} 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/+page.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
Dashboard
38 |
39 |
40 |
41 |
42 | 43 | Project Info 44 |
45 |
46 |
47 | 48 |
49 | 50 |
51 |
52 |
53 | 54 |
55 | 56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 | 68 |
69 |
70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 |
78 | 79 |
80 |
81 | 82 | Billing 83 |
84 |
85 |
86 |
87 |

CPU

88 |
89 | {billing?.cpu} 90 |  seconds 91 |
92 |
93 |
94 |

Memory

95 |
96 | {billing?.memory} 97 |  GiB-s 98 |
99 |
100 |
101 |

Egress

102 |
103 | {billing?.egress} 104 |  GiB 105 |
106 |
107 |
108 |

Disk

109 |
110 | {billing?.disk} 111 |  GiB-s 112 |
113 |
114 |
115 |

Replica

116 |
117 | {billing?.replica} 118 |
119 |
120 |
121 |

Price

122 |
123 | {billing?.price} 124 |  THB 125 |
126 |
127 |
128 |
129 |
130 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/+layout.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | import { redirect, error } from '@sveltejs/kit' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | /** @type {Api.Response} */ 9 | const deployment = await api.invoke('deployment.get', { project, location, name }, fetch) 10 | if (!deployment.ok) { 11 | if (deployment.error?.notFound) { 12 | redirect(302, `/deployment?project=${project}`) 13 | } 14 | error(500, deployment.error?.message) 15 | } 16 | 17 | return { 18 | location, 19 | name, 20 | deployment: deployment.result 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 |
23 |
24 |
Deployments
25 |
26 |
27 |
{deployment.name}
28 |
29 |
30 | 31 |
32 | 33 |
34 |
api.invalidate('deployment.get')} /> 35 | 36 | {@render children?.()} 37 |
38 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ parent, fetch }) { 5 | const { 6 | project, 7 | location, 8 | deployment 9 | } = await parent() 10 | 11 | /** @type {Api.Response} */ 12 | const locationInfo = await api.invoke('location.get', { project, id: location }, fetch) 13 | if (!locationInfo.ok) { 14 | error(500, `location: ${locationInfo.error?.message}`) 15 | } 16 | if (!locationInfo.result) redirect(302, `/deployment?project=${project}`) 17 | 18 | return { 19 | location: locationInfo.result, 20 | deployment 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
Deployment details
47 |
48 | 49 | 50 | {#if deployment.type === 'WebService'} 51 | {#if !deployment.internal} 52 | 53 | 54 | 62 | 63 | {/if} 64 | 65 | 66 | 72 | 73 | {/if} 74 | {#if hasExternalTCPAddress} 75 | 76 | 77 | 78 | 79 | {/if} 80 | {#if hasInternalTCPAddress} 81 | 82 | 83 | 89 | 90 | {/if} 91 | 92 | 93 | 99 | 100 | 101 | 102 | 108 | 109 | 110 | 111 | 117 | 118 | {#if deployment.type === 'WebService'} 119 | 120 | 121 | 122 | 123 | {:else if deployment.type === 'TCPService'} 124 | 125 | 126 | 127 | 128 | {/if} 129 | {#if location.features.disk} 130 | 131 | 132 | 140 | 141 | {/if} 142 | {#if deployment.minReplicas > 0} 143 | 144 | 145 | 156 | 157 | {/if} 158 | {#if deployment.type === 'CronJob'} 159 | 160 | 161 | 162 | 163 | {/if} 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | {#if location.features.workloadIdentity} 177 | 178 | 179 | 180 | 181 | {/if} 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 |
URL 55 | 56 | {`https://${deployment.url}`} 57 | 58 | 59 | 60 | 61 |
Internal URL 67 | http://{deployment.internalUrl} 68 | 69 | 70 | 71 |
Address{deployment.address}:{deployment.nodePort}
Internal Address 84 | {deployment.internalAddress}:{deployment.port} 85 | 86 | 87 | 88 |
Type 94 | {format.deploymentType(deployment.type)} 95 | {#if deployment.internal} 96 | (Internal) 97 | {/if} 98 |
Location 103 | {deployment.location} 104 | 105 | 106 | 107 |
Image 112 | {deployment.image} 113 | 114 | 115 | 116 |
Port{deployment.port}{deployment.protocol ? `:${deployment.protocol}` : ''}
Port{deployment.port}:{deployment.nodePort}
Disk 133 | {#if deployment.disk} 134 | {deployment.disk.name} 135 | (mount: {deployment.disk.mountPath || '-'}, sub: {deployment.disk.subPath || '-'}) 136 | {:else} 137 | - 138 | {/if} 139 |
Replicas 146 | {#if deployment.minReplicas > 0} 147 | {#if deployment.minReplicas === deployment.maxReplicas} 148 | {deployment.minReplicas} 149 | {:else} 150 | {deployment.minReplicas} - {deployment.maxReplicas} 151 | {/if} 152 | {:else} 153 | - 154 | {/if} 155 |
Schedule{deployment.schedule}
Command{deployment.command.join(' ') || '-'}
Args{deployment.args.join(' ') || '-'}
Pull Secret{deployment.pullSecret || '-'}
Workload Identity{deployment.workloadIdentity || '-'}
CPU limited{format.cpuLimited(deployment.resources.limits.cpu)}
Memory allocated{format.memory(deployment.resources.requests.memory)}
Sidecars{deployment.sidecars?.length || 0}
Deployed At{format.datetime(deployment.createdAt)}
Deployed By{deployment.createdBy}
Allocated Price{deployment.allocatedPrice.toLocaleString(undefined, { maximumFractionDigits: 2 })} THB/month/replica
212 |
213 | 214 |
215 |
216 | 217 |
218 |
219 | 220 |
Environment variables
221 |
222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | {#each Object.entries(deployment.env || {}) as [k, v]} 231 | 232 | 233 | 236 | 237 | {:else} 238 | 239 | {/each} 240 | 241 |
EnvValue
{k} 234 | 235 |
242 |
243 | 244 |
Mount Data
245 |
246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | {#each Object.entries(deployment.mountData || {}) as [k, v]} 255 | 256 | 257 | 258 | 259 | {:else} 260 | 261 | {/each} 262 | 263 |
PathData
{k}{v}
264 |
265 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/events/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { 3 | deployment 4 | } = await parent() 5 | 6 | return { 7 | deployment 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/events/+page.svelte: -------------------------------------------------------------------------------- 1 | 30 | 31 |
Events
32 | 33 |
34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | {#each events as it} 45 | 46 | 47 | 48 | 49 | 50 | 51 | {:else} 52 | 53 | {/each} 54 | 55 |
Last SeenTypeReasonMessage
{format.datetime(it.lastSeen)}{it.type}{it.reason}{it.message}
56 |
57 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/logs/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { 3 | deployment 4 | } = await parent() 5 | 6 | return { 7 | deployment 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/logs/+page.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
Logs
38 | Stream Raw Logs 39 |
{text}
40 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/metrics/+page.svelte: -------------------------------------------------------------------------------- 1 | 78 | 79 |
Metric
80 | 81 |
82 |
83 | 97 |
98 |
99 | 100 | 101 | 102 | {#if deployment.type === 'WebService'} 103 | 104 | {/if} 105 | 106 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/revision/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ parent, fetch }) { 5 | const { 6 | project, 7 | location, 8 | name, 9 | deployment 10 | } = await parent() 11 | 12 | const revisions = await api.invoke('deployment.revisions', { project, location, name }, fetch) 13 | if (!revisions.ok) error(500, revisions.error?.message) 14 | 15 | return { 16 | deployment, 17 | revisions: revisions.result.items ?? [] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/(detail)/revision/+page.svelte: -------------------------------------------------------------------------------- 1 | 33 | 34 |
Revision
35 |
36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | {#each revisions as it, index} 48 | 49 | 50 | 51 | 52 | 53 | 58 | 59 | {/each} 60 | 61 |
RevisionImageDeployed AtDeployed By
{it.revision}{it.image}{format.datetime(it.createdAt)}{it.createdBy} 54 | {#if index > 0} 55 | 56 | {/if} 57 |
62 |
63 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'deployment', 4 | overrideRedirect: '/deployment' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('deployment.list', { project }, fetch) 8 | return { 9 | deployments: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
Deployments
15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | {#each deployments as it (`${it.name}-${it.location}`)} 41 | 42 | 48 | 49 | 50 | 51 | 62 | 63 | 64 | 65 | 66 | {/each} 67 | 68 | 69 | 70 |
NameTypeMemoryReplicasLocationLast deployed
43 | 44 | 45 | {it.name} 46 | 47 | {format.deploymentType(it.type)}{format.memory(it.resources.requests.memory)} 52 | {#if it.minReplicas > 0} 53 | {#if it.minReplicas === it.maxReplicas} 54 | {it.minReplicas} 55 | {:else} 56 | {it.minReplicas} - {it.maxReplicas} 57 | {/if} 58 | {:else} 59 | - 60 | {/if} 61 | {it.location}{format.datetime(it.createdAt)}
71 |
72 |
73 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/_components/Header.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
61 |
62 |

63 |
64 | 65 |
66 |
67 | {deployment.name} 68 |
{deployment.image}
69 |
70 |

71 |
72 | 74 | Deploy New Revision 75 | 76 | {#if canPause} 77 |
78 | 81 |
82 | {/if} 83 | {#if canResume} 84 |
85 | 88 |
89 | {/if} 90 |
91 |
92 |
93 | 94 |
95 | 96 | 123 | 124 | 129 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/deployment/deploy/+page.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | 9 | /** @type {Api.Response | null} */ 10 | let deployment = null 11 | if (location && name) { 12 | deployment = await api.invoke('deployment.get', { project, location, name }, fetch) 13 | if (!deployment.ok) { 14 | if (deployment.error?.notFound) redirect(302, `/deployment?project=${project}`) 15 | error(500, deployment.error?.message) 16 | } 17 | } 18 | return { 19 | deployment: deployment?.result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/(detail)/+layout.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | import { redirect, error } from '@sveltejs/kit' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | 9 | const disk = await api.invoke('disk.get', { project, location, name }, fetch) 10 | if (!disk.ok) { 11 | if (disk.error?.notFound) redirect(302, `/disk?project=${project}`) 12 | error(500, disk.error?.message) 13 | } 14 | if (!disk.result) redirect(302, `/disk?project=${project}`) 15 | 16 | return { 17 | location, 18 | name, 19 | disk: disk.result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/(detail)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
21 |
22 |
Disks
23 |
24 |
25 |
{disk.name}
26 |
27 |
28 | 29 |
30 | 31 |
32 |
33 |

34 | 35 | Disk: {disk.name} 36 |

37 |
38 | 39 |
40 | 41 | 53 | 54 | {@render children?.()} 55 |
56 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/(detail)/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 29 | 30 |
31 |
32 | 33 |
34 | 35 |
36 |
37 |
38 | 39 |
40 | 41 |
42 |
43 |
44 | 45 |
46 | 47 |
48 |
49 |
50 | 51 |
52 | {format.datetime(disk.createdAt)} 53 |
54 |
55 |
56 | 57 |
58 | {disk.createdBy} 59 |
60 |
61 |
62 | 63 |
64 | 65 |
66 | Update 67 | 68 | 71 |
72 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/(detail)/metrics/+page.svelte: -------------------------------------------------------------------------------- 1 | 59 | 60 |
Metric
61 | 62 |
63 |
64 | 73 |
74 |
75 | 76 | 77 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'disk', 4 | overrideRedirect: '/disk' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('disk.list', { project }, fetch) 8 | return { 9 | disks: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
Disks
15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | {#each disks as it (`${it.name}-${it.location}`)} 38 | 39 | 45 | 46 | 47 | 48 | 55 | 56 | {/each} 57 | 58 | 59 | 60 |
Disk nameSizeLocationCreated at
40 | 41 | 42 | {it.name} 43 | 44 | {it.size} GiB{it.location}{format.datetime(it.createdAt)} 49 | 50 |
51 | 52 |
53 |
54 |
61 |
62 |
63 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/create/+page.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | 9 | let disk 10 | if (location && name) { 11 | disk = await api.invoke('disk.get', { project, location, name }, fetch) 12 | if (!disk.ok) { 13 | if (disk.error?.notFound) redirect(302, `/disk?project=${project}`) 14 | error(500, disk.error?.message) 15 | } 16 | } 17 | 18 | return { 19 | location, 20 | name, 21 | disk: disk?.result 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/disk/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 55 | 56 |
57 |
58 |
Disks
59 |
60 | {#if disk} 61 |
62 |
{disk.name}
63 |
64 | {/if} 65 |
66 |
{#if disk}Update{:else}Create{/if}
67 |
68 |
69 | 70 |
71 | 72 |
73 |
74 |
75 |

76 | {#if disk} 77 | Update Disk {disk.name} 78 | {:else} 79 | Create Disk 80 | {/if} 81 |

82 |
83 |
84 | 85 |
86 | 87 |
88 |
89 | 90 |
91 | 92 |
93 |
94 |
95 | 96 | {#if disk} 97 |
98 | 99 |
100 | {:else} 101 |
102 | 112 |
113 | {/if} 114 |
115 |
116 | 117 |
118 | 119 |
120 |
121 | 122 | 129 |
130 |
131 | 132 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'domain', 4 | overrideRedirect: '/domain' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | const res = await api.invoke('domain.list', { project }, fetch) 7 | return { 8 | domains: res.result?.items ?? [], 9 | error: res.error 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/+page.svelte: -------------------------------------------------------------------------------- 1 | 32 | 33 |
Domains
34 |
35 |
36 |
37 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | {#each domains as it (`${it.domain}-${it.location}`)} 59 | 60 | 64 | 71 | 78 | 79 | 80 | 81 | 86 | 87 | {/each} 88 | 89 | 90 | 91 |
DomainWildcardCDNLocation
61 | 62 | {it.domain} 63 | 65 | {#if it.wildcard} 66 | 67 | {:else} 68 | 69 | {/if} 70 | 72 | {#if it.cdn} 73 | 74 | {:else} 75 | 76 | {/if} 77 | {it.location} 82 | 85 |
92 |
93 |
94 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/cdn-downgrade/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const domainName = url.searchParams.get('domain') 7 | 8 | /** @type {Api.Response} */ 9 | const domain = await api.invoke('domain.get', { project, domain: domainName }, fetch) 10 | if (!domain.ok) { 11 | if (domain.error?.notFound) redirect(302, `/domain?project=${project}`) 12 | error(500, domain.error?.message) 13 | } 14 | if (!domain.result) redirect(302, `/domain?project=${project}`) 15 | if (!domain.result.cdn) redirect(302, `/domain/detail?project=${project}&domain=${domainName}`) 16 | 17 | /** @type {Api.Response} */ 18 | const location = await api.invoke('location.get', { id: domain.result.location }, fetch) 19 | if (!location.ok) { 20 | error(500, location.error?.message) 21 | } 22 | if (!location.result) redirect(302, `/domain?project=${project}`) 23 | 24 | return { 25 | domain: domain.result, 26 | location: location.result 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/cdn-downgrade/+page.svelte: -------------------------------------------------------------------------------- 1 | 42 | 43 |
44 |
45 |
Domains
46 |
47 |
48 |
{domain.domain}
49 |
50 |
51 |
CDN Downgrade
52 |
53 |
54 | 55 |
56 |
57 |
58 |

59 | CDN Downgrade 60 |

61 |
62 | 63 |
64 | 65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 | 74 |
75 | 76 |
77 |
78 |
79 |
80 | 81 | 82 |
83 |
84 | 85 |
86 | {#if location.endpoint} 87 |
88 | 89 | {#each [location.endpoint] as ip} 90 |
91 | 92 | 94 | 95 | 96 |
97 | {/each} 98 |
99 | {/if} 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | {#if location.cname} 115 |
116 | 117 | {#each [location.cname] as cname} 118 |
119 | 120 | 122 | 123 | 124 |
125 | {/each} 126 |
127 | {/if} 128 |
129 | 130 |
131 |
132 |
133 | 134 |
135 |
136 |
137 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 51 | 52 |
53 |
54 |
Domains
55 |
56 |
57 |
Create
58 |
59 |
60 | 61 |
62 | 63 |
64 |
65 |
66 |

Create domain

67 |
68 |
69 |
70 |
71 |
72 | 73 |
74 | 75 |
76 |
77 |
78 | 79 |
80 | 86 |
87 |
88 | 89 |
90 |
Advanced Settings
91 |
92 | 93 |
94 |
95 | 96 | 97 |
98 |
99 |
100 |
101 | 102 | 103 |
104 |
105 | 106 |
107 | 108 | 109 |
110 |
111 | 112 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/domain/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const domainName = url.searchParams.get('domain') 7 | 8 | /** @type {Api.Response} */ 9 | const domain = await api.invoke('domain.get', { project, domain: domainName }, fetch) 10 | if (!domain.ok) { 11 | if (domain.error?.notFound) redirect(302, `/domain?project=${project}`) 12 | error(500, domain.error?.message) 13 | } 14 | if (!domain.result) redirect(302, `/domain?project=${project}`) 15 | 16 | return { 17 | domain: domain.result 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/dropbox/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'dropbox', 4 | overrideRedirect: '/dropbox' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/dropbox/+page.svelte: -------------------------------------------------------------------------------- 1 | 44 | 45 |
Dropbox (Alpha)
46 |
47 |
48 |
49 |
50 | 51 |
52 |
53 | 54 |
55 |
56 | 57 | {#if downloadUrl} 58 |
59 | Download URL 60 |
{downloadUrl}
61 |
62 | {/if} 63 |
64 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/email/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'email', 4 | overrideRedirect: '/email' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/email/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('email.list', { project }, fetch) 8 | return { 9 | domains: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/email/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
Emails
13 |
14 |
15 |
16 |
17 | Contact us to request access 18 |
19 |
20 | 21 |
22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | {#each domains as it (it.domain)} 32 | 33 | 34 | 35 | 36 | 37 | {/each} 38 | 39 | 40 | 41 |
DomainQuotaCreated at
{it.domain}-{format.datetime(it.createdAt)}
42 |
43 |
44 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'pull-secret', 4 | overrideRedirect: '/pull-secret' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('pullSecret.list', { project }, fetch) 8 | return { 9 | pullSecrets: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
Pull Secrets
15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | {#each pullSecrets as it (`${it.name}-${it.location}`)} 37 | 38 | 44 | 45 | 46 | 47 | 48 | {/each} 49 | 50 | 51 | 52 |
NameLocationCreated atCreated by
39 | 40 | 41 | {it.name} 42 | 43 | {it.location}{format.datetime(it.createdAt)}{it.createdBy}
53 |
54 |
55 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 60 | 61 |
62 |
63 |
Pull Secrets
64 |
65 |
66 |
Create
67 |
68 |
69 | 70 |
71 |
72 |
73 |
74 |

Create pull secret

75 |
76 |
77 | 78 |
79 | 80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 |
88 | 89 |
90 | 96 |
97 |
98 |
99 | 100 |
101 | 102 |
103 |
104 |
105 | 106 |
107 | 108 |
109 |
110 |
111 | 112 |
113 | 114 |
115 |
116 | 117 |
118 | 119 | 120 |
121 |
122 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | 9 | const pullSecret = await api.invoke('pullSecret.get', { project, location, name }, fetch) 10 | if (!pullSecret.ok) { 11 | if (pullSecret.error?.notFound) redirect(302, `/pull-secret?project=${project}`) 12 | error(500, pullSecret.error?.message) 13 | } 14 | if (!pullSecret.result) redirect(302, `/pull-secret?project=${project}`) 15 | 16 | return { 17 | location, 18 | name, 19 | pullSecret: pullSecret.result 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/pull-secret/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 37 | 38 |
39 |
40 |
Pull Secrets
41 |
42 |
43 |
{pullSecret.name}
44 |
45 |
46 | 47 |
48 |
49 |
50 |

Pull secret "{pullSecret.name}"

51 |
52 | 53 |
54 | 55 |
56 |
57 | 58 |
59 | 60 |
61 |
62 |
63 | 64 |
65 | 66 |
67 |
68 |
69 | 70 |
71 | 72 | 74 | 75 | 76 |
77 |
78 |
79 | 80 |
81 | 82 | 84 | 85 | 86 |
87 |
88 |
89 | 90 |
91 | 92 | 94 | 95 | 96 |
97 |
98 | 99 |
100 | 101 |
102 | 103 |
104 |
105 |
106 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'registry', 4 | overrideRedirect: '/registry' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('registry/list', { project }, fetch) 8 | return { 9 | repositories: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/+page.svelte: -------------------------------------------------------------------------------- 1 | 11 | 12 |
Registry (Alpha)
13 |
14 |
15 |

registry.deploys.app/{project}/{'{repository}'}:{'{tag}'}

16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | {#each repositories as repo (repo.name)} 25 | 26 | 32 | 33 | {/each} 34 | 35 | 36 | 37 |
Repository
27 | 29 | {repo.name} 30 | 31 |
38 |
39 |
40 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { error, redirect } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const id = url.searchParams.get('repository') 7 | 8 | const repository = await api.invoke('registry/get', { 9 | project, 10 | repository: id 11 | }, fetch) 12 | if (!repository.ok) { 13 | if (repository.error?.message === 'repository not found') { 14 | redirect(302, `/registry?project=${project}`) 15 | } 16 | error(500, repository.error?.message) 17 | } 18 | 19 | /** @type {Api.Response} */ 20 | const res = await api.invoke('registry/getTags', { 21 | project, 22 | repository: id 23 | }, fetch) 24 | 25 | return { 26 | id, 27 | repository: repository.result, 28 | tags: res.result?.items ?? [], 29 | error: res.error 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/registry/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 22 | 23 |
24 |
25 |
Registry
26 |
27 |
28 |
{repository.name}
29 |
30 |
31 | 32 |
33 | 34 |
35 |
36 |

37 | {repository.name} 38 |
registry.deploys.app/{project}/{repository.name}
39 |
{format.storage(repository.size)}
40 |

41 |
42 | 43 |
44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {#each tags as tag} 54 | 55 | 61 | 67 | 68 | 69 | {/each} 70 | 71 | 72 | 73 |
TagDigestCreated At
56 | {tag.tag} 57 | 58 | 59 | 60 | 62 | {format.shortDigest(tag.digest)} 63 | 64 | 65 | 66 | {format.datetime(tag.createdAt)}
74 |
75 |
76 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'role', 4 | overrideRedirect: '/role' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('role.list', { project }, fetch) 8 | return { 9 | roles: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/+page.svelte: -------------------------------------------------------------------------------- 1 | 19 | 20 |
Roles
21 |
22 |
23 |
24 | 29 |
30 | 31 |
32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | {#each roles as it (it.role)} 44 | 45 | 54 | 55 | 56 | 57 | 66 | 67 | {/each} 68 | 69 | 70 | 71 |
RoleNameCreated AtCreated By
46 | {#if roleCanUpdate(it.role)} 47 | 48 | {it.role} 49 | 50 | {:else} 51 | {it.role} 52 | {/if} 53 | {it.name}{format.datetime(it.createdAt)}{it.createdBy} 58 | {#if roleCanUpdate(it.role)} 59 | 60 |
61 | 62 |
63 |
64 | {/if} 65 |
72 |
73 |
74 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/bind/+page.js: -------------------------------------------------------------------------------- 1 | import { error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const email = url.searchParams.get('email') 7 | 8 | const roles = await api.invoke('role.list', { project }, fetch) 9 | if (!roles.ok) error(500, roles.error?.message) 10 | 11 | let selected 12 | if (email) { 13 | const users = await api.invoke('role.users', { project }, fetch) 14 | if (!users.ok) error(500, users.error?.message) 15 | 16 | selected = users.result?.items?.find((x) => x.email === email)?.roles 17 | } 18 | 19 | return { 20 | menu: 'role.users', 21 | overrideRedirect: '/role/users', 22 | email, 23 | roles: roles.result.items ?? [], 24 | selected: selected ?? [] 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/bind/+page.svelte: -------------------------------------------------------------------------------- 1 | 67 | 68 |
69 |
70 |
Users
71 |
72 |
73 |
Add
74 |
75 |
76 | 77 |
78 |
79 |
80 |
81 |

Add member to "{project}" project

82 |
83 |
84 | 85 |
86 | 87 |
88 |
89 |
90 | 91 |
92 |
93 |
94 |
95 | 103 |
104 |
105 | 106 |
107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | {#each form.roles as it} 116 | 117 | 118 | 124 | 125 | {:else} 126 | 127 | {/each} 128 | 129 |
Role
{it} 119 |
removeRole(it)} onkeypress={() => removeRole(it)}> 121 | 122 |
123 |
130 |
131 | 132 | 135 |
136 |
137 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/create/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const roleId = url.searchParams.get('role') 7 | 8 | /** @type {Api.Response} */ 9 | const permissions = await api.invoke('role.permissions', {}, fetch) 10 | if (!permissions.ok) error(500, permissions.error?.message) 11 | 12 | let role = null 13 | if (roleId) { 14 | /** @type {Api.Response} */ 15 | const roleInfo = await api.invoke('role.get', { project, role: roleId }, fetch) 16 | if (!roleInfo.ok) { 17 | if (roleInfo.error?.notFound) redirect(302, `/role?project=${project}`) 18 | error(500, roleInfo.error?.message) 19 | } 20 | if (!roleInfo.result) redirect(302, `/role?project=${project}`) 21 | role = roleInfo.result 22 | } 23 | 24 | return { 25 | menu: 'role', 26 | role, 27 | permissions: permissions.result ?? [] 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 88 | 89 |
90 |
91 |
Roles
92 |
93 | {#if role} 94 |
95 |
{role.role}
96 |
97 |
98 |
Update
99 |
100 | {:else} 101 |
102 |
Create
103 |
104 | {/if} 105 |
106 | 107 |
108 | 109 |
110 |
111 |

112 | {#if role} 113 | Update role "{form.role}" 114 | {:else} 115 | Create new role 116 | {/if} 117 |

118 |
119 | 120 |
121 | 122 |
123 |
124 | 125 |
126 | 127 |
128 |
129 | 130 |
131 | 132 |
133 | 134 |
135 |
136 | 137 |
138 |
139 |
140 | 141 |
142 |
Permissions
143 | 144 |
145 |
146 | 152 |
153 |
154 | 155 |
156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | {#each form.permissions as it} 165 | 166 | 167 | 173 | 174 | {:else} 175 | 176 | {/each} 177 | 178 |
Permission
{it} 168 |
removePermission(it)} onkeypress={() => removePermission(it)}> 170 | 171 |
172 |
179 |
180 |
181 | 182 |
183 | 184 |
185 | 188 | {#if role} 189 | 190 | {/if} 191 |
192 |
193 |
194 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/users/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('role.users', { project }, fetch) 8 | return { 9 | menu: 'role.users', 10 | overrideRedirect: '/role/users', 11 | users: res.result?.items ?? [], 12 | error: res.error 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/role/users/+page.svelte: -------------------------------------------------------------------------------- 1 | 35 | 36 |
Users
37 |
38 |
39 |
40 |
41 | 42 | Add 43 | 44 |
45 |
46 | 47 |
48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | {#each users as it (it.email)} 58 | 59 | 60 | 65 | 75 | 76 | {/each} 77 | 78 | 79 | 80 |
EmailRoles
{it.email} 61 | {#each it.roles as r} 62 | {r}
63 | {/each} 64 |
66 | 67 |
68 | 69 |
70 |
71 | 74 |
81 |
82 |
83 | 84 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/route/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'route', 4 | overrideRedirect: '/route' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/route/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('route.list', { project }, fetch) 8 | return { 9 | routes: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/route/+page.svelte: -------------------------------------------------------------------------------- 1 | 36 | 37 |
Routes
38 |
39 |
40 |
41 | 46 |
47 | 48 |
49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | {#each routes as it (`${it.domain}${it.path}-${it.location}`)} 63 | 64 | 69 | 70 | 71 | 76 | 77 | 78 | 83 | 84 | {/each} 85 | 86 | 87 | 88 |
RouteTargetLocationConfig
65 | https://{it.domain}{it.path} 68 | {it.target}{it.location} 72 | {#if it.config.basicAuth} 73 | 74 | {/if} 75 | 79 | 82 |
89 |
90 |
91 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/route/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 123 | 124 |
125 |
126 |
Routes
127 |
128 |
129 |
Create
130 |
131 |
132 | 133 |
134 |
135 |
136 |
137 |

Create route

138 |
139 |
140 |
141 |
142 |
143 | 144 |
145 | 151 |
152 |
153 | 154 | {#if form.location} 155 |
156 | 157 |
158 | 164 |
165 |
166 | 167 | {#if selectedDomain?.wildcard} 168 |
169 | 170 |
171 | 172 | 173 |
174 |
175 | {/if} 176 | 177 |
178 | 179 |
180 | 181 |
182 |
183 | 184 |
185 | 186 |
187 | 195 |
196 |
197 | 198 | {#if form.targetPrefix === 'deployment://'} 199 |
200 | 201 |
202 | 208 |
209 |
210 | {:else if form.targetPrefix && form.targetPrefix !== 'dnslink://'} 211 |
212 | 213 |
214 | 215 |
216 |
217 | {/if} 218 | 219 |
220 |
Advanced Settings
221 |
222 | 223 |
224 |
225 | 226 | 227 |
228 |
229 | 230 | {#if form.config.enableBasicAuth} 231 |
232 | 233 |
234 | 235 |
236 |
237 | 238 |
239 | 240 |
241 | 242 |
243 |
244 | {/if} 245 | 246 |
247 | 248 | 249 | {/if} 250 |
251 |
252 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'service-account', 4 | overrideRedirect: '/service-account' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('serviceAccount.list', { project }, fetch) 8 | return { 9 | serviceAccounts: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/+page.svelte: -------------------------------------------------------------------------------- 1 | 12 | 13 |
Service Accounts
14 |
15 |
16 |
17 | 22 |
23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {#each serviceAccounts as it (it.email)} 36 | 37 | 42 | 43 | 44 | 51 | 52 | {/each} 53 | 54 | 55 | 56 |
EmailNameCreated At
38 | 39 | {it.email} 40 | 41 | {it.name}{format.datetime(it.createdAt)} 45 | 46 |
47 | 48 |
49 |
50 |
57 |
58 |
59 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/create/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const id = url.searchParams.get('id') 7 | 8 | let serviceAccount 9 | if (id) { 10 | serviceAccount = await api.invoke('serviceAccount.get', { project, id }, fetch) 11 | if (!serviceAccount.ok) { 12 | if (serviceAccount.error?.notFound) { 13 | redirect(302, `/service-account?project=${project}`) 14 | } 15 | error(500, serviceAccount.error?.message) 16 | } 17 | } 18 | 19 | return { 20 | id, 21 | serviceAccount: serviceAccount?.result 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 |
48 | {#if id} 49 | 52 | {:else} 53 |
54 |
Service Accounts
55 |
56 | {/if} 57 |
58 |
59 | {#if id} 60 | Update 61 | {:else} 62 | Create 63 | {/if} 64 |
65 |
66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 |

74 | {#if id} 75 | Update service account "{serviceAccount.sid}" 76 | {:else} 77 | Create service account 78 | {/if} 79 |

80 |
81 |
82 | 83 |
84 | 85 |
86 | {#if id} 87 |
88 | 89 |
90 | 91 |
92 |
93 | {:else} 94 |
95 | 96 |
97 | 98 |
99 |
100 | {/if} 101 | 102 |
103 | 104 |
105 | 106 |
107 |
108 | 109 |
110 | 111 |
112 | 113 |
114 |
115 | 116 | 117 |
118 |
119 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const id = url.searchParams.get('id') 7 | const serviceAccount = await api.invoke('serviceAccount.get', { project, id }, fetch) 8 | if (!serviceAccount.ok) { 9 | if (serviceAccount.error?.message === 'api: service account not found') { 10 | redirect(302, `/service-account?project=${project}`) 11 | } 12 | error(500, serviceAccount.error?.message) 13 | } 14 | return { 15 | id, 16 | serviceAccount: serviceAccount.result 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/service-account/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 65 | 66 |
67 |
68 |
Service Accounts
69 |
70 |
71 |
{serviceAccount.name}
72 |
73 |
74 | 75 |
76 | 77 |
78 |
79 |

80 | {serviceAccount.name} 81 |

82 |
83 | 84 |
85 | 86 |
87 |
88 | 89 |
90 | 91 |
92 |
93 |
94 | 95 |
96 | 97 |
98 |
99 |
100 | 101 |
102 | 103 |
104 |
105 |
106 | 107 |
108 | 109 |
110 |
111 |
112 | 113 |
114 | 115 |
116 |
117 | 118 |
119 | 120 |
Keys
121 |
122 | {#each (serviceAccount.keys ?? []) as key} 123 |
124 | 125 | 128 |
129 | {/each} 130 | 131 |
132 | 136 |
137 |
138 | 139 |
140 | 141 |
142 | 145 |
146 |
147 |
148 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/+layout.js: -------------------------------------------------------------------------------- 1 | export function load () { 2 | return { 3 | menu: 'workload-identity', 4 | overrideRedirect: '/workload-identity' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ parent, fetch }) { 4 | const { project } = await parent() 5 | 6 | /** @type {Api.Response>} */ 7 | const res = await api.invoke('workloadIdentity.list', { project }, fetch) 8 | return { 9 | workloadIdentities: res.result?.items ?? [], 10 | error: res.error 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/+page.svelte: -------------------------------------------------------------------------------- 1 | 13 | 14 |
Workload Identities
15 |
16 |
17 |
18 | 23 |
24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | {#each workloadIdentities as it} 36 | 37 | 43 | 44 | 45 | 46 | {/each} 47 | 48 | 49 | 50 |
NameLocationCreated at
38 | 39 | 40 | {it.name} 41 | 42 | {it.location}{format.datetime(it.createdAt)}
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 48 | 49 |
50 | 53 |
54 |
Create
55 |
56 |
57 | 58 |
59 |
60 |
61 |
62 |

Create

63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 | 71 |
72 |
73 |
74 | 75 |
76 | 84 |
85 |
86 |
87 | 88 |
89 | 90 |
91 |
92 |
93 | 94 |
95 |
96 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, parent, fetch }) { 5 | const { project } = await parent() 6 | const location = url.searchParams.get('location') 7 | const name = url.searchParams.get('name') 8 | 9 | const [locationData, workloadIdentity] = await Promise.all([ 10 | api.invoke('location.get', { project, id: location }, fetch), 11 | api.invoke('workloadIdentity.get', { project, location, name }, fetch) 12 | ]) 13 | 14 | if (!locationData.ok || !workloadIdentity.ok) { 15 | if (locationData.error?.notFound || workloadIdentity.error?.notFound) { 16 | redirect(302, `/workload-identity?project=${project}`) 17 | } 18 | error(500, `location: ${locationData.error?.message}, workloadIdentity: ${workloadIdentity.error?.message}`) 19 | } 20 | return { 21 | // location: locationData.result, 22 | name, 23 | workloadIdentity: workloadIdentity.result 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/(auth)/(project)/workload-identity/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 |
43 | 46 |
47 |
{workloadIdentity.name}
48 |
49 |
50 | 51 |
52 |
53 |
54 |

Workload Identity: {workloadIdentity.name}

55 |
56 |
57 |
58 |
59 | 60 |
61 | 62 |
63 |
64 |
65 | 66 |
67 | 68 |
69 |
70 |
71 | 72 |
73 | {format.datetime(workloadIdentity.createdAt)} 74 |
75 |
76 |
77 | 78 |
79 | {workloadIdentity.createdBy} 80 |
81 |
82 | 83 |
84 | 85 |
86 | 87 |
 88 | 				
 89 | 				{#if projectInfo}
 90 | 					{format.gsaBinding(projectInfo.id, workloadIdentity.name, workloadIdentity.gsa, 'acoshift-1362')}
 91 | 				{/if}
 92 | 			
93 |
94 | 95 |
96 | 97 |
98 | 99 |
100 |
101 |
102 | -------------------------------------------------------------------------------- /src/routes/(auth)/+layout.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ fetch }) { 5 | /** @type {[Api.Response, Api.Response>]} */ 6 | const [ 7 | me, 8 | projects 9 | ] = await Promise.all([ 10 | api.invoke('me.get', {}, fetch), 11 | api.invoke('project.list', {}, fetch) 12 | ]) 13 | if (!me.ok) { 14 | if (me.error?.unauth) redirect(302, '/auth/signin') 15 | error(500, me.error?.message) 16 | } 17 | if (!projects.ok) error(500, projects.error?.message) 18 | 19 | return { 20 | profile: me.result, 21 | projects: projects.result?.items ?? [] 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/(auth)/+layout.svelte: -------------------------------------------------------------------------------- 1 | 34 | 35 |
37 | 40 | 41 | 46 | 47 |
48 | {@render children?.()} 49 |
50 |
51 | 52 | 53 | 54 | 125 | -------------------------------------------------------------------------------- /src/routes/(auth)/ModalSelectProject.svelte: -------------------------------------------------------------------------------- 1 | 41 | 42 | 80 | 81 | 98 | -------------------------------------------------------------------------------- /src/routes/(auth)/Navbar.svelte: -------------------------------------------------------------------------------- 1 | 46 | 47 | 91 | 92 | 175 | -------------------------------------------------------------------------------- /src/routes/(auth)/Sidebar.svelte: -------------------------------------------------------------------------------- 1 | 98 | 99 | 186 | 187 | 264 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/+page.js: -------------------------------------------------------------------------------- 1 | import api from '$lib/api' 2 | 3 | export async function load ({ fetch }) { 4 | /** @type {Api.Response>} */ 5 | const res = await api.invoke('billing.list', {}, fetch) 6 | return { 7 | billingAccounts: res.result?.items ?? [], 8 | error: res.error 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/+page.svelte: -------------------------------------------------------------------------------- 1 | 10 | 11 |
Billing
12 |
13 |
14 |
15 | 20 |
21 |
22 |
23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {#each billingAccounts as it (it.id)} 33 | 34 | 37 | 38 | 45 | 46 | {/each} 47 | 48 | 49 | 50 |
Billing nameBilling IDActive
35 | {it.name} 36 | {it.id} 39 | {#if it.active} 40 | 41 | {:else} 42 | 43 | {/if} 44 |
51 |
52 |
53 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/create/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, fetch }) { 5 | const id = url.searchParams.get('id') 6 | 7 | let billingAccount = null 8 | if (id) { 9 | /** @type {Api.Response} */ 10 | const res = await api.invoke('billing.get', { id }, fetch) 11 | if (!res.ok) { 12 | if (res.error?.notFound) redirect(302, '/billing') 13 | error(500, res.error?.message) 14 | } 15 | if (!res.result) redirect(302, '/billing') 16 | 17 | billingAccount = res.result 18 | } 19 | 20 | return { 21 | billingAccount 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 49 | 50 |
51 |
52 |
Billing
53 |
54 | {#if billingAccount} 55 | 58 |
59 |
Update
60 |
61 | {:else} 62 |
63 |
Create
64 |
65 | {/if} 66 |
67 | 68 |
69 | 70 |
71 |
72 |
73 |

Account information

74 |
75 |
76 |
77 |
78 |
79 | 80 |
81 | 82 |
83 |
84 | 85 |

Billing Information

86 | 87 |
88 | 89 |
90 | 91 |
92 |
93 | 94 |
95 | 96 |
97 | 98 |
99 |
100 | 101 |
102 | 103 |
104 | 105 |
106 |
107 | 108 |
109 | 110 | 111 |
112 |
113 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/detail/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, fetch }) { 5 | const id = url.searchParams.get('id') 6 | 7 | /** @type {Api.Response} */ 8 | const billingAccount = await api.invoke('billing.get', { id }, fetch) 9 | if (!billingAccount.ok) { 10 | if (billingAccount.error?.notFound) redirect(302, '/billing') 11 | error(500, billingAccount.error?.message) 12 | } 13 | if (!billingAccount.result) redirect(302, '/billing') 14 | 15 | return { 16 | billingAccount: billingAccount?.result 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/detail/+page.svelte: -------------------------------------------------------------------------------- 1 | 25 | 26 |
27 |
28 |
Billing
29 |
30 |
31 |
{billingAccount.name}
32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 |

"{billingAccount.name}" account information

40 |
41 | 42 |
43 | 44 |
45 |
46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 |
Account name{billingAccount.name}
Tax ID{billingAccount.taxId}
Name{billingAccount.taxName}
Address{billingAccount.taxAddress}
66 |
67 | 68 |
69 | Edit 70 | Report 71 | 72 |
73 |
74 |
75 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/report/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, fetch }) { 5 | const id = url.searchParams.get('id') 6 | 7 | const billingAccount = await api.invoke('billing.get', { id }, fetch) 8 | if (!billingAccount.ok) { 9 | if (billingAccount.error?.notFound) redirect(302, '/billing') 10 | error(500, billingAccount.error?.message) 11 | } 12 | if (!billingAccount.result) redirect(302, '/billing') 13 | 14 | return { 15 | billingAccount: billingAccount?.result 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/routes/(auth)/billing/report/+page.svelte: -------------------------------------------------------------------------------- 1 | 106 | 107 |
108 |
109 |
Billing
110 |
111 |
112 |
{billingAccount.name}
113 |
114 |
115 |
Report
116 |
117 |
118 | 119 |
120 | 121 |
122 |
123 |
124 |
125 | 132 |
133 |
134 |
135 | 136 |
137 |
138 | {#each (report?.projectList ?? []) as it} 139 |
140 | 141 | 142 |
143 | {/each} 144 |
145 |
146 | 147 |
148 | 149 |
150 | 151 |
Billings
152 |
153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | {#each (report?.list ?? []) as it} 164 | 165 | 166 | 167 | 168 | 169 | 170 | {:else} 171 | 172 | {/each} 173 | 174 |
ProjectNameUsage ValueBilling Value
{it.projectSid}{it.name}{it.usageValue}{it.billingValue}
175 |
176 |
177 | -------------------------------------------------------------------------------- /src/routes/(auth)/project/+page.js: -------------------------------------------------------------------------------- 1 | export async function load ({ parent }) { 2 | const { projects } = await parent() 3 | 4 | return { 5 | projects 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/routes/(auth)/project/+page.svelte: -------------------------------------------------------------------------------- 1 | 45 | 46 |
Projects
47 |
48 |
49 |
50 | 55 |
56 | 57 |
58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | {#each projects as it (it.project)} 69 | 70 | 75 | 76 | 77 | 87 | 88 | {:else} 89 | 90 | {/each} 91 | 92 |
NameIDNumber
71 | 72 | {it.name} 73 | 74 | {it.project}{it.id} 78 | 79 |
80 | 81 |
82 |
83 | 86 |
93 |
94 |
95 | -------------------------------------------------------------------------------- /src/routes/(auth)/project/create/+page.js: -------------------------------------------------------------------------------- 1 | import { redirect, error } from '@sveltejs/kit' 2 | import api from '$lib/api' 3 | 4 | export async function load ({ url, fetch }) { 5 | const project = url.searchParams.get('project') 6 | 7 | /** @type {Api.Response | null} */ 8 | let projectInfo = null 9 | if (project) { 10 | projectInfo = await api.invoke('project.get', { project }, fetch) 11 | if (!projectInfo.ok) { 12 | if (projectInfo.error?.notFound) redirect(302, '/project') 13 | error(500, projectInfo.error?.message) 14 | } 15 | } 16 | 17 | /** @type {Api.Response>} */ 18 | const billingAccounts = await api.invoke('billing.list', {}, fetch) 19 | if (!billingAccounts.ok) error(500, billingAccounts.error?.message) 20 | 21 | return { 22 | project: projectInfo?.result, 23 | billingAccounts: billingAccounts.result?.items ?? [] 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/routes/(auth)/project/create/+page.svelte: -------------------------------------------------------------------------------- 1 | 53 | 54 |
55 |
56 |
Projects
57 |
58 | {#if project} 59 |
60 |
{project.name}
61 |
62 | {/if} 63 |
64 |
{#if project}Update{:else}Create{/if}
65 |
66 |
67 | 68 |
69 |
70 |
71 | {#if project} 72 |
Update Project: {project.name}
73 | {:else} 74 |
Create Project
75 | {/if} 76 |
77 | 78 |
79 | 80 |
81 |
82 | 83 |
84 | 85 |
86 |
87 | 88 |
89 | 90 |
91 | 92 |
93 |
94 | 95 |
96 | 97 |
98 | 104 |
105 |
106 | 107 | 114 |
115 |
116 | -------------------------------------------------------------------------------- /src/routes/+layout.js: -------------------------------------------------------------------------------- 1 | // export const ssr = false 2 | -------------------------------------------------------------------------------- /src/routes/+layout.svelte: -------------------------------------------------------------------------------- 1 | 21 | 22 | {@render children?.()} 23 | -------------------------------------------------------------------------------- /src/routes/api/[fn]/+server.js: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private' 2 | 3 | const endpoint = env.API_ENDPOINT 4 | 5 | /** @type {import('@sveltejs/kit').RequestHandler} */ 6 | export async function POST ({ locals, params, request }) { 7 | const token = locals.token 8 | 9 | // fast-path to reject unauthorized requests 10 | if (!token) { 11 | return new Response(JSON.stringify({ 12 | ok: false, 13 | error: { 14 | message: 'api: unauthorized' 15 | } 16 | }), { 17 | headers: { 18 | 'content-type': 'application/json' 19 | } 20 | }) 21 | } 22 | 23 | const resp = await fetch(`${endpoint}/${params.fn}`, { 24 | method: 'POST', 25 | body: request.body, 26 | // @ts-expect-error workaround for missing type 27 | duplex: 'half', 28 | headers: { 29 | accept: 'application/json', 30 | 'content-type': request.headers.get('content-type') ?? 'application/json', 31 | authorization: `bearer ${token}` 32 | } 33 | }) 34 | return new Response(resp.body, { 35 | status: resp.status, 36 | headers: { 37 | 'content-type': resp.headers.get('content-type') ?? '' 38 | } 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/routes/api/dropbox/+server.js: -------------------------------------------------------------------------------- 1 | /** @type {import('./$types').RequestHandler} */ 2 | export async function POST ({ locals, request, url }) { 3 | const token = locals.token 4 | const project = url.searchParams.get('project') 5 | 6 | if (!token || !project) { 7 | return new Response(JSON.stringify({ 8 | ok: false, 9 | error: { 10 | message: 'api: unauthorized' 11 | } 12 | }), { 13 | headers: { 14 | 'content-type': 'application/json' 15 | } 16 | }) 17 | } 18 | 19 | const resp = await fetch('https://dropbox.deploys.app/', { 20 | method: 'POST', 21 | body: request.body, 22 | // @ts-expect-error workaround for missing type 23 | duplex: 'half', 24 | headers: { 25 | accept: 'application/json', 26 | 'content-type': request.headers.get('content-type') ?? 'application/octet-stream', 27 | 'param-project': project, 28 | authorization: `bearer ${token}` 29 | } 30 | }) 31 | return new Response(resp.body, { 32 | status: resp.status, 33 | headers: { 34 | 'content-type': resp.headers.get('content-type') ?? '' 35 | } 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/routes/api/registry/[fn]/+server.js: -------------------------------------------------------------------------------- 1 | const endpoint = 'https://registry.deploys.app/api' 2 | 3 | /** @type {import('./$types').RequestHandler} */ 4 | export async function POST ({ locals, params, request }) { 5 | const token = locals.token 6 | 7 | // fast-path to reject unauthorized requests 8 | if (!token) { 9 | return new Response(JSON.stringify({ 10 | ok: false, 11 | error: { 12 | message: 'api: unauthorized' 13 | } 14 | }), { 15 | headers: { 16 | 'content-type': 'application/json' 17 | } 18 | }) 19 | } 20 | 21 | const resp = await fetch(`${endpoint}/${params.fn}`, { 22 | method: 'POST', 23 | body: request.body, 24 | // @ts-expect-error workaround for missing type 25 | duplex: 'half', 26 | headers: { 27 | accept: 'application/json', 28 | 'content-type': request.headers.get('content-type') ?? 'application/json', 29 | authorization: `bearer ${token}` 30 | } 31 | }) 32 | return new Response(resp.body, { 33 | status: resp.status, 34 | headers: { 35 | 'content-type': resp.headers.get('content-type') ?? '' 36 | } 37 | }) 38 | } 39 | -------------------------------------------------------------------------------- /src/routes/auth/callback/+server.js: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private' 2 | 3 | /** @type {import('@sveltejs/kit').RequestHandler} */ 4 | export async function GET ({ cookies, url }) { 5 | const state = url.searchParams.get('state') ?? '' 6 | const code = url.searchParams.get('code') ?? '' 7 | 8 | if (state !== cookies.get('state')) { 9 | return new Response('invalid state', { status: 400 }) 10 | } 11 | 12 | // exchange token 13 | const resp = await fetch('https://auth.deploys.app/token', { 14 | method: 'POST', 15 | body: new URLSearchParams({ 16 | grant_type: 'authorization_code', 17 | code, 18 | client_id: env.OAUTH2_CLIENT_ID, 19 | client_secret: env.OAUTH2_CLIENT_SECRET 20 | }), 21 | headers: { 22 | 'content-type': 'application/x-www-form-urlencoded' 23 | } 24 | }) 25 | if (resp.status < 200 || resp.status > 299) { 26 | return resp 27 | } 28 | const respBody = await resp.json() 29 | const token = respBody.refresh_token 30 | if (!token) { 31 | return new Response('unknown error', { status: 500 }) 32 | } 33 | 34 | cookies.set('token', token, { 35 | httpOnly: true, 36 | maxAge: 60 * 60 * 24 * 7, 37 | sameSite: 'lax', 38 | path: '/', 39 | secure: import.meta.env.PROD 40 | }) 41 | cookies.delete('state', { 42 | httpOnly: true, 43 | sameSite: 'lax', 44 | path: '/', 45 | secure: import.meta.env.PROD 46 | }) 47 | 48 | return new Response(undefined, { 49 | status: 302, 50 | headers: { 51 | location: '/' 52 | } 53 | }) 54 | } 55 | -------------------------------------------------------------------------------- /src/routes/auth/signin/+server.js: -------------------------------------------------------------------------------- 1 | import { env } from '$env/dynamic/private' 2 | 3 | /** @type {Crypto} */ 4 | let webcrypto 5 | 6 | // workaround for dev 7 | if (typeof crypto === 'undefined') { 8 | import('node:crypto') 9 | .then((imp) => { 10 | // @ts-expect-error workaround to use Crypto while dev 11 | webcrypto = imp.webcrypto 12 | }) 13 | .catch(() => { 14 | // don't throw error on compile time 15 | }) 16 | } else { 17 | webcrypto = crypto 18 | } 19 | 20 | /** 21 | * randomState generates a random string for OAuth2 state 22 | * @returns {string} 23 | */ 24 | function randomState () { 25 | const x = new Uint8Array(16) 26 | webcrypto.getRandomValues(x) 27 | return Array.from(x, (d) => d.toString(16).padStart(2, '0')).join('') 28 | } 29 | 30 | /** @type {import('@sveltejs/kit').RequestHandler} */ 31 | export async function GET ({ cookies, url }) { 32 | const state = randomState() 33 | 34 | const callback = new URL(url.toString()) 35 | callback.pathname = '/auth/callback' 36 | 37 | const q = new URLSearchParams() 38 | q.set('response_type', 'code') 39 | q.set('client_id', env.OAUTH2_CLIENT_ID) 40 | q.set('redirect_uri', callback.toString()) 41 | q.set('state', state) 42 | 43 | cookies.set('state', state, { 44 | httpOnly: true, 45 | maxAge: 60 * 60, 46 | sameSite: 'lax', 47 | path: '/', 48 | secure: import.meta.env.PROD 49 | }) 50 | 51 | return new Response(undefined, { 52 | status: 302, 53 | headers: { 54 | location: `https://auth.deploys.app/?${q.toString()}` 55 | } 56 | }) 57 | } 58 | -------------------------------------------------------------------------------- /src/routes/auth/signout/+server.js: -------------------------------------------------------------------------------- 1 | const landing = 'https://www.deploys.app/' 2 | 3 | /** @type {import('@sveltejs/kit').RequestHandler} */ 4 | export async function POST ({ locals }) { 5 | const token = locals.token 6 | 7 | if (token) { 8 | const q = new URLSearchParams() 9 | q.set('token', token) 10 | q.set('callback', landing) 11 | 12 | return new Response(undefined, { 13 | status: 302, 14 | headers: { 15 | location: `https://auth.deploys.app/revoke?${q.toString()}` 16 | } 17 | }) 18 | } 19 | 20 | return new Response(undefined, { 21 | status: 302, 22 | headers: { 23 | location: landing 24 | } 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /src/style/main.scss: -------------------------------------------------------------------------------- 1 | @use 'theme'; 2 | 3 | @use '@nomimono/nomimono-css/reset.css'; 4 | @use '@nomimono/nomimono-css/atomic.css'; 5 | @use '@nomimono/nomimono-css/layout.css'; 6 | @use '@nomimono/nomimono-css/component.css'; 7 | 8 | html, body { 9 | background-color: hsl(var(--hsl-base-200)); 10 | } 11 | 12 | body, h1, h2, h3, h4, h4, h5, h6 { 13 | line-height: 1.25; 14 | } 15 | 16 | [disabled] { 17 | cursor: not-allowed; 18 | } 19 | 20 | ul, ol { 21 | list-style: none; 22 | } 23 | 24 | .lo-grid-span-horizontal { 25 | display: grid; 26 | grid-auto-flow: column; 27 | grid-gap: 1rem; 28 | justify-content: start; 29 | } 30 | 31 | input:-webkit-autofill, 32 | input:-webkit-autofill:hover, 33 | input:-webkit-autofill:focus, 34 | textarea:-webkit-autofill, 35 | textarea:-webkit-autofill:hover, 36 | textarea:-webkit-autofill:focus, 37 | select:-webkit-autofill, 38 | select:-webkit-autofill:hover, 39 | select:-webkit-autofill:focus { 40 | -webkit-text-fill-color: hsl(var(--hsl-content)); 41 | -webkit-box-shadow: 0 0 0 1000px hsl(var(--hsl-base-400)/0.2) inset; 42 | transition: background-color 5000s ease-in-out 0s; 43 | } 44 | 45 | .nm-panel { 46 | padding: 1.75rem 1.5rem; 47 | 48 | form, .content { 49 | max-width: 768px; 50 | } 51 | } 52 | 53 | h1, 54 | h2, 55 | h3, 56 | h4, 57 | h5, 58 | h6 { 59 | font-family: var(--ffml-secondary); 60 | font-weight: 400; 61 | line-height: 1.25; 62 | } 63 | 64 | h1 { 65 | font-size: var(--fs-10); 66 | 67 | @media (min-width: 768px) { 68 | font-size: var(--fs-10) 69 | } 70 | 71 | @media (min-width: 1024px) { 72 | font-size: var(--fs-11); 73 | } 74 | 75 | @media (min-width: 1280px) { 76 | font-size: var(--fs-11); 77 | } 78 | } 79 | 80 | h2 { 81 | font-size: var(--fs-9); 82 | 83 | @media (min-width: 768px) { 84 | font-size: var(--fs-9) 85 | } 86 | 87 | @media (min-width: 1024px) { 88 | font-size: var(--fs-10); 89 | } 90 | 91 | @media (min-width: 1280px) { 92 | font-size: var(--fs-10); 93 | } 94 | } 95 | 96 | h3 { 97 | font-size: var(--fs-8); 98 | 99 | @media (min-width: 768px) { 100 | font-size: var(--fs-8) 101 | } 102 | 103 | @media (min-width: 1024px) { 104 | font-size: var(--fs-9); 105 | } 106 | 107 | @media (min-width: 1280px) { 108 | font-size: var(--fs-9); 109 | } 110 | } 111 | 112 | h4 { 113 | font-size: var(--fs-7); 114 | } 115 | 116 | h5 { 117 | font-size: var(--fs-6); 118 | } 119 | 120 | h6 { 121 | font-size: var(--fs-5); 122 | } 123 | 124 | p { 125 | font-size: var(--fs-4); 126 | margin: 0; 127 | line-height: 1.65; 128 | } 129 | 130 | .nm-field > .helper { 131 | color: hsl(var(--hsl-content)/0.75); 132 | } 133 | 134 | *::placeholder, 135 | *::-moz-placeholder, 136 | *::webkit-input-placeholder { 137 | color: rgba(255, 255, 255, .3); 138 | } 139 | 140 | .nm-input.-has-icon-left, 141 | .nm-input.-has-icon-right { 142 | position: relative; 143 | 144 | > .icon { 145 | position: absolute; 146 | top: 0; 147 | 148 | display: flex; 149 | align-items: center; 150 | justify-content: center; 151 | 152 | width: 2.625rem; 153 | height: 100%; 154 | } 155 | } 156 | 157 | .icon { 158 | margin-left: var(--spc-5); 159 | border: 0; 160 | background-color: transparent; 161 | color: var(--color-gray-400); 162 | cursor: pointer; 163 | font-size: var(--fs-4); 164 | user-select: none; 165 | 166 | &.copy { 167 | font-size: var(--fs-6); 168 | } 169 | 170 | &:hover { 171 | color: white; 172 | } 173 | } 174 | 175 | .nm-input.-has-icon-left { 176 | > input { 177 | padding-left: 2.625rem; 178 | } 179 | 180 | > .icon:not(.-is-right) { 181 | left: 0; 182 | } 183 | } 184 | 185 | .nm-input.-has-icon-right { 186 | > input { 187 | padding-right: 2.625rem; 188 | } 189 | 190 | > .icon.-is-right { 191 | right: 0; 192 | } 193 | } 194 | 195 | input[type=number].-no-arrow { 196 | -moz-appearance: textfield; 197 | 198 | &::-webkit-outer-spin-button, 199 | &::-webkit-inner-spin-button { 200 | -webkit-appearance: none; 201 | margin: 0; 202 | } 203 | } 204 | 205 | .icon-button { 206 | display: inline-flex; 207 | align-items: center; 208 | justify-content: center; 209 | 210 | width: 2rem; 211 | height: 2rem; 212 | 213 | border: 0; 214 | background-color: transparent; 215 | color: hsl(var(--hsl-content)); 216 | 217 | border-radius: 50%; 218 | font-size: 12px; 219 | cursor: pointer; 220 | 221 | transition: all var(--timing-faster) ease; 222 | 223 | &:hover { 224 | transform: scale(1.1); 225 | } 226 | } 227 | 228 | pre { 229 | position: relative; 230 | border: none; 231 | padding: 20px; 232 | border-radius: 3px; 233 | margin-bottom: 30px; 234 | white-space: pre-wrap; 235 | word-wrap: break-word; 236 | background-color: hsl(var(--hsl-base-200)); 237 | 238 | .copy { 239 | position: absolute; 240 | top: 0; 241 | right: 0; 242 | background-color: inherit; 243 | padding: 6px 10px; 244 | font-size: 16px; 245 | font-weight: 600; 246 | cursor: pointer; 247 | user-select: none; 248 | border: 0; 249 | color: hsl(var(--hsl-content)); 250 | outline: 0; 251 | 252 | &:hover { 253 | background-color: hsl(var(--hsl-base-100)); 254 | } 255 | 256 | &:hover:active { 257 | background-color: hsl(var(--hsl-base-100)); 258 | } 259 | } 260 | 261 | code { 262 | white-space: pre-wrap; 263 | display: block; 264 | } 265 | 266 | &.pre-scoll { 267 | min-height: 800px; 268 | max-height: 800px; 269 | overflow-y: auto; 270 | } 271 | } 272 | -------------------------------------------------------------------------------- /static/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/ad30df05d9462ff395f5683e0db85389dbcec82f/static/favicon.png -------------------------------------------------------------------------------- /static/favicon.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/ad30df05d9462ff395f5683e0db85389dbcec82f/static/favicon.webp -------------------------------------------------------------------------------- /static/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/ad30df05d9462ff395f5683e0db85389dbcec82f/static/images/logo.png -------------------------------------------------------------------------------- /static/images/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/deploys-app/console/ad30df05d9462ff395f5683e0db85389dbcec82f/static/images/logo.webp -------------------------------------------------------------------------------- /svelte.config.js: -------------------------------------------------------------------------------- 1 | import adapterCloudflare from '@sveltejs/adapter-cloudflare' 2 | import adapterNode from '@sveltejs/adapter-node' 3 | import preprocess from 'svelte-preprocess' 4 | 5 | /** @type {import('@sveltejs/kit').Config} */ 6 | const config = { 7 | preprocess: preprocess({ 8 | sass: true 9 | }), 10 | kit: { 11 | adapter: process.env.ADAPTER === 'node' ? adapterNode() : adapterCloudflare(), 12 | alias: { 13 | $style: './src/style', 14 | $types: './src/types' 15 | }, 16 | version: { 17 | pollInterval: 60 * 1000 // 1 min 18 | } 19 | } 20 | } 21 | 22 | export default config 23 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { sveltekit } from '@sveltejs/kit/vite' 2 | import { defineConfig } from 'vite' 3 | 4 | export default defineConfig({ 5 | plugins: [ 6 | sveltekit() 7 | ] 8 | }) 9 | -------------------------------------------------------------------------------- /wrangler.toml: -------------------------------------------------------------------------------- 1 | name = "deploys-app--console" 2 | pages_build_output_dir = ".svelte-kit/cloudflare" 3 | compatibility_date = "2024-05-13" 4 | 5 | [vars] 6 | API_ENDPOINT = "https://api.deploys.app" 7 | OAUTH2_CLIENT_ID = "deploys-console-dev" 8 | PUBLIC_API_ENDPOINT = "https://api.deploys.app" 9 | 10 | [env.production.vars] 11 | API_ENDPOINT = "https://api.deploys.app" 12 | OAUTH2_CLIENT_ID = "deploys-console" 13 | PUBLIC_API_ENDPOINT = "https://api.deploys.app" 14 | --------------------------------------------------------------------------------